diff --git a/src/lib/app.ts b/src/lib/app.ts index ee861a608738..b95e826b64e2 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -113,7 +113,7 @@ export default async function getApp( corsOriginMiddleware(services, config), ); app.options( - `${baseUriPath}/api/client/streaming*`, + `${baseUriPath}/api/streaming/*`, corsOriginMiddleware(services, config), ); diff --git a/src/lib/features/client-feature-toggles/client-feature-streaming.controller.ts b/src/lib/features/client-feature-toggles/client-feature-streaming.controller.ts deleted file mode 100644 index 04965f0bdf62..000000000000 --- a/src/lib/features/client-feature-toggles/client-feature-streaming.controller.ts +++ /dev/null @@ -1,217 +0,0 @@ -import type { Response } from 'express'; -import Controller from '../../routes/controller'; -import type { - IFeatureToggleQuery, - IFlagResolver, - IUnleashConfig, - IUnleashServices, -} from '../../types'; -import type { Logger } from '../../logger'; -import type { IAuthRequest } from '../../routes/unleash-types'; -import { NONE } from '../../types/permissions'; -import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service'; -import { UPDATE_REVISION } from '../feature-toggle/configuration-revision-service'; -import type { ClientFeatureToggleService } from './client-feature-toggle-service'; -import ApiUser from '../../types/api-user'; -import { ALL, isAllProjects } from '../../types/models/api-token'; -import { querySchema } from '../../schema/feature-schema'; -import hashSum from 'hash-sum'; - -type ResponseWithFlush = Response & { flush: Function }; - -type SSEClientResponse = { - req: IAuthRequest; - res: ResponseWithFlush; -}; - -interface QueryOverride { - project?: string[]; - environment?: string; -} - -interface IMeta { - revisionId: number; - etag: string; - queryHash: string; -} - -export class FeatureStreamingController extends Controller { - private readonly logger: Logger; - - private configurationRevisionService: ConfigurationRevisionService; - - private clientFeatureToggleService: ClientFeatureToggleService; - - private flagResolver: IFlagResolver; - - private activeConnections: Set; - - constructor( - { - configurationRevisionService, - clientFeatureToggleService, - }: Pick< - IUnleashServices, - 'configurationRevisionService' | 'clientFeatureToggleService' - >, - config: IUnleashConfig, - ) { - super(config); - this.configurationRevisionService = configurationRevisionService; - this.clientFeatureToggleService = clientFeatureToggleService; - this.flagResolver = config.flagResolver; - this.logger = config.getLogger('client-api/streaming.js'); - - this.activeConnections = new Set(); - this.onUpdateRevisionEvent = this.onUpdateRevisionEvent.bind(this); - this.configurationRevisionService.on( - UPDATE_REVISION, - this.onUpdateRevisionEvent, - ); - - this.route({ - method: 'get', - path: '', - handler: this.getFeatureStream, - permission: NONE, - middleware: [], - }); - } - - async getFeatureStream( - req: IAuthRequest, - res: ResponseWithFlush, - ): Promise { - if (!this.flagResolver.isEnabled('streaming')) { - res.status(403).end(); - return; - } - - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }); - - res.write(`data: CONNECTED\n\n`); - res.flush(); - - const connection = { req, res }; - this.activeConnections.add(connection); - - res.on('close', () => { - this.activeConnections.delete(connection); - }); - } - - private async onUpdateRevisionEvent() { - for (const connection of this.activeConnections) { - const { req, res } = connection; - - if (res.writableEnded) { - this.activeConnections.delete(connection); - continue; - } - - try { - const update = await this.getClientFeaturesResponse(req); - res.write(`data: UPDATE:${JSON.stringify(update)}\n\n`); - res.flush(); - } catch (err) { - this.logger.info('Failed to send event. Dropping connection.'); - this.activeConnections.delete(connection); - } - } - } - - private async getClientFeaturesResponse(req: IAuthRequest) { - const query = await this.resolveQuery(req); - const meta = await this.calculateMeta(query); - - const features = - await this.clientFeatureToggleService.getClientFeatures(query); - const segments = - await this.clientFeatureToggleService.getActiveSegmentsForClient(); - - return { - version: 2, - features, - query, - segments, - meta, - }; - } - - private async resolveQuery( - req: IAuthRequest, - ): Promise { - const { user, query } = req; - - const override: QueryOverride = {}; - if (user instanceof ApiUser) { - if (!isAllProjects(user.projects)) { - override.project = user.projects; - } - if (user.environment !== ALL) { - override.environment = user.environment; - } - } - - return this.prepQuery({ - ...query, - ...override, - inlineSegmentConstraints: false, - }); - } - - private paramToArray(param: any) { - if (!param) { - return param; - } - return Array.isArray(param) ? param : [param]; - } - - private async prepQuery({ - tag, - project, - namePrefix, - environment, - inlineSegmentConstraints, - }: IFeatureToggleQuery): Promise { - if ( - !tag && - !project && - !namePrefix && - !environment && - !inlineSegmentConstraints - ) { - return {}; - } - - const tagQuery = this.paramToArray(tag); - const projectQuery = this.paramToArray(project); - const query = await querySchema.validateAsync({ - tag: tagQuery, - project: projectQuery, - namePrefix, - environment, - inlineSegmentConstraints, - }); - - if (query.tag) { - query.tag = query.tag.map((q) => q.split(':')); - } - - return query; - } - - private async calculateMeta(query: IFeatureToggleQuery): Promise { - const revisionId = - await this.configurationRevisionService.getMaxRevisionId(); - - const queryHash = hashSum(query); - const etag = `"${queryHash}:${revisionId}"`; - return { revisionId, etag, queryHash }; - } -} diff --git a/src/lib/routes/client-api/index.ts b/src/lib/routes/client-api/index.ts index 2c36bff9efe0..4e0ca3098550 100644 --- a/src/lib/routes/client-api/index.ts +++ b/src/lib/routes/client-api/index.ts @@ -3,17 +3,12 @@ import FeatureController from '../../features/client-feature-toggles/client-feat import MetricsController from '../../features/metrics/instance/metrics'; import RegisterController from '../../features/metrics/instance/register'; import type { IUnleashConfig, IUnleashServices } from '../../types'; -import { FeatureStreamingController } from '../../features/client-feature-toggles/client-feature-streaming.controller'; export default class ClientApi extends Controller { constructor(config: IUnleashConfig, services: IUnleashServices) { super(config); this.use('/features', new FeatureController(services, config).router); - this.use( - '/streaming', - new FeatureStreamingController(services, config).router, - ); this.use('/metrics', new MetricsController(services, config).router); this.use('/register', new RegisterController(services, config).router); } diff --git a/src/server-dev.ts b/src/server-dev.ts index 936ca43313e0..1b0d7b0eb7e5 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -57,7 +57,6 @@ process.nextTick(async () => { showUserDeviceCount: true, flagOverviewRedesign: false, licensedUsers: true, - streaming: true, }, }, authentication: {