diff --git a/src/lib/routes/client-api/index.ts b/src/lib/routes/client-api/index.ts index ce3b46984c..74a42737e1 100644 --- a/src/lib/routes/client-api/index.ts +++ b/src/lib/routes/client-api/index.ts @@ -3,12 +3,17 @@ import FeatureController from '../../features/client-feature-toggles/client-feat import MetricsController from './metrics'; import RegisterController from './register'; import { IUnleashConfig, IUnleashServices } from '../../types'; +import FeatureStreamController from './stream'; export default class ClientApi extends Controller { constructor(config: IUnleashConfig, services: IUnleashServices) { super(config); this.use('/features', new FeatureController(services, config).router); + this.use( + '/stream', + new FeatureStreamController(services, config).router, + ); this.use('/metrics', new MetricsController(services, config).router); this.use('/register', new RegisterController(services, config).router); } diff --git a/src/lib/routes/client-api/stream.ts b/src/lib/routes/client-api/stream.ts new file mode 100644 index 0000000000..8b5d4a5827 --- /dev/null +++ b/src/lib/routes/client-api/stream.ts @@ -0,0 +1,80 @@ +import { Response } from 'express'; +import Controller from '../controller'; +import { IUnleashServices } from '../../types'; +import { IUnleashConfig } from '../../types/option'; +import { Logger } from '../../logger'; +import { IAuthRequest, IUser } from '../../server-impl'; +import { NONE } from '../../types/permissions'; +import ConfigurationRevisionService, { + UPDATE_REVISION, +} from '../../features/feature-toggle/configuration-revision-service'; + +export default class FeatureStreamController extends Controller { + logger: Logger; + + clients: any[] = []; + + private configurationRevisionService: ConfigurationRevisionService; + + constructor( + { + configurationRevisionService, + }: Pick, + config: IUnleashConfig, + ) { + super(config); + this.logger = config.getLogger('/api/client/stream'); + this.configurationRevisionService = configurationRevisionService; + this.configurationRevisionService.on( + UPDATE_REVISION, + this.notifyClients.bind(this), + ); + + this.route({ + method: 'get', + path: '', + handler: this.stream, + permission: NONE, + }); + } + + notifyClients(revisionId: number) { + this.logger.debug('Notifying clients of new revision', revisionId); + this.clients.forEach((client) => { + client.res.write(`data: ${JSON.stringify({ revisionId })}\n\n`); + }); + } + + async stream(req: IAuthRequest, res: Response): Promise { + const headers = { + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + }; + res.writeHead(200, headers); + + const revisionId = + await this.configurationRevisionService.getMaxRevisionId(); + + const toggles = { revisionId }; + + const data = `data: ${JSON.stringify(toggles)}\n\n`; + res.write(data); + + const clientId = Date.now(); // Use UUIDs for real-world scenarios + + const newClient = { + id: clientId, + res, + }; + + this.clients.push(newClient); + + req.on('close', () => { + console.log(`${clientId} Connection closed`); + this.clients = this.clients.filter( + (client) => client.id !== clientId, + ); + }); + } +}