opinionated-machine 10.5.0 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -0
- package/README.md +513 -52
- package/dist/lib/DIContext.d.ts +13 -1
- package/dist/lib/DIContext.js +28 -8
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/api-contracts/apiRouteBuilder.d.ts +47 -5
- package/dist/lib/api-contracts/apiRouteBuilder.js +57 -8
- package/dist/lib/api-contracts/apiRouteBuilder.js.map +1 -1
- package/dist/lib/api-contracts/apiSseConnectionRegistry.d.ts +182 -0
- package/dist/lib/api-contracts/apiSseConnectionRegistry.js +332 -0
- package/dist/lib/api-contracts/apiSseConnectionRegistry.js.map +1 -0
- package/dist/lib/api-contracts/index.d.ts +1 -0
- package/dist/lib/api-contracts/index.js +1 -0
- package/dist/lib/api-contracts/index.js.map +1 -1
- package/dist/lib/gateway/gatewayMetadata.d.ts +1 -0
- package/dist/lib/gateway/gatewayMetadata.js +11 -0
- package/dist/lib/gateway/gatewayMetadata.js.map +1 -1
- package/dist/lib/gateway/index.d.ts +1 -0
- package/dist/lib/gateway/index.js +1 -0
- package/dist/lib/gateway/index.js.map +1 -1
- package/dist/lib/gateway/manifest/buildManifest.d.ts +18 -0
- package/dist/lib/gateway/manifest/buildManifest.js +31 -0
- package/dist/lib/gateway/manifest/buildManifest.js.map +1 -1
- package/dist/lib/gateway/manifest/manifestSchema.d.ts +18 -0
- package/dist/lib/gateway/manifest/manifestSchema.js +27 -0
- package/dist/lib/gateway/manifest/manifestSchema.js.map +1 -1
- package/dist/lib/gateway/routeStreaming.d.ts +79 -0
- package/dist/lib/gateway/routeStreaming.js +70 -0
- package/dist/lib/gateway/routeStreaming.js.map +1 -0
- package/dist/lib/resolverFunctions.d.ts +7 -0
- package/dist/lib/resolverFunctions.js +13 -0
- package/dist/lib/resolverFunctions.js.map +1 -1
- package/dist/lib/routes/fastifyRouteBuilder.js +9 -2
- package/dist/lib/routes/fastifyRouteBuilder.js.map +1 -1
- package/dist/lib/sse/AbstractSSEController.d.ts +10 -0
- package/dist/lib/sse/AbstractSSEController.js +9 -0
- package/dist/lib/sse/AbstractSSEController.js.map +1 -1
- package/dist/lib/sse/eventIds.d.ts +138 -0
- package/dist/lib/sse/eventIds.js +155 -0
- package/dist/lib/sse/eventIds.js.map +1 -0
- package/dist/lib/sse/index.d.ts +3 -2
- package/dist/lib/sse/index.js +6 -2
- package/dist/lib/sse/index.js.map +1 -1
- package/dist/lib/sse/rooms/SSERoomEventPublisher.d.ts +121 -0
- package/dist/lib/sse/rooms/SSERoomEventPublisher.js +119 -0
- package/dist/lib/sse/rooms/SSERoomEventPublisher.js.map +1 -0
- package/dist/lib/sse/rooms/index.d.ts +1 -0
- package/dist/lib/sse/rooms/index.js +1 -0
- package/dist/lib/sse/rooms/index.js.map +1 -1
- package/dist/lib/testing/apiSseEventValidation.d.ts +1 -1
- package/dist/lib/testing/apiSseInjectHelpers.js +9 -10
- package/dist/lib/testing/apiSseInjectHelpers.js.map +1 -1
- package/dist/lib/testing/sseHttpClient.d.ts +10 -2
- package/dist/lib/testing/sseHttpClient.js +13 -6
- package/dist/lib/testing/sseHttpClient.js.map +1 -1
- package/dist/lib/testing/sseInjectClient.d.ts +1 -1
- package/dist/lib/testing/sseInjectClient.js +1 -1
- package/dist/lib/testing/sseInjectClient.js.map +1 -1
- package/dist/lib/testing/sseTestTypes.d.ts +1 -1
- package/package.json +13 -13
- package/dist/lib/sse/sseParser.d.ts +0 -167
- package/dist/lib/sse/sseParser.js +0 -225
- package/dist/lib/sse/sseParser.js.map +0 -1
package/dist/lib/DIContext.d.ts
CHANGED
|
@@ -50,7 +50,11 @@ export declare class DIContext<Dependencies extends object, Config extends objec
|
|
|
50
50
|
* like `@opinionated-machine/gateway-envoy` or
|
|
51
51
|
* `@opinionated-machine/gateway-krakend` to produce a config.
|
|
52
52
|
*
|
|
53
|
-
* SSE and dual-mode
|
|
53
|
+
* SSE and dual-mode routes declared through `AbstractApiController` are
|
|
54
|
+
* always included and carry a `streaming: 'sse' | 'dual'` marker. Routes
|
|
55
|
+
* from legacy `AbstractSSEController` / `AbstractDualModeController`
|
|
56
|
+
* controllers are included only when `includeStreamingControllers: true`
|
|
57
|
+
* is passed (off by default so existing manifests don't silently grow).
|
|
54
58
|
*
|
|
55
59
|
* @example
|
|
56
60
|
* ```ts
|
|
@@ -116,6 +120,14 @@ export declare class DIContext<Dependencies extends object, Config extends objec
|
|
|
116
120
|
registerDualModeRoutes(app: FastifyInstance<any, any, any, any>, options?: RegisterDualModeRoutesOptions): void;
|
|
117
121
|
private applyDualModeRouteOptions;
|
|
118
122
|
private applySSERouteOptions;
|
|
123
|
+
/**
|
|
124
|
+
* Apply registration-time options to an SSE/dual-mode route before app.route().
|
|
125
|
+
*
|
|
126
|
+
* Shared by the SSE-only and dual-mode registration paths: both apply the same
|
|
127
|
+
* pre-handlers, rate limit and `sse` field defaults. Route-level options
|
|
128
|
+
* (buildHandler / buildApiRoute) take precedence over these.
|
|
129
|
+
*/
|
|
130
|
+
private applyStreamRouteOptions;
|
|
119
131
|
private applyPreHandlers;
|
|
120
132
|
private applyRateLimit;
|
|
121
133
|
destroy(): Promise<void>;
|
package/dist/lib/DIContext.js
CHANGED
|
@@ -159,7 +159,11 @@ export class DIContext {
|
|
|
159
159
|
* like `@opinionated-machine/gateway-envoy` or
|
|
160
160
|
* `@opinionated-machine/gateway-krakend` to produce a config.
|
|
161
161
|
*
|
|
162
|
-
* SSE and dual-mode
|
|
162
|
+
* SSE and dual-mode routes declared through `AbstractApiController` are
|
|
163
|
+
* always included and carry a `streaming: 'sse' | 'dual'` marker. Routes
|
|
164
|
+
* from legacy `AbstractSSEController` / `AbstractDualModeController`
|
|
165
|
+
* controllers are included only when `includeStreamingControllers: true`
|
|
166
|
+
* is passed (off by default so existing manifests don't silently grow).
|
|
163
167
|
*
|
|
164
168
|
* @example
|
|
165
169
|
* ```ts
|
|
@@ -183,6 +187,18 @@ export class DIContext {
|
|
|
183
187
|
const controller = this.diContainer.resolve(name);
|
|
184
188
|
collected.push({ name, kind: 'api', controller });
|
|
185
189
|
}
|
|
190
|
+
if (options.includeStreamingControllers) {
|
|
191
|
+
for (const name of this.sseControllerNames) {
|
|
192
|
+
// biome-ignore lint/suspicious/noExplicitAny: any SSE controller works here
|
|
193
|
+
const controller = this.diContainer.resolve(name);
|
|
194
|
+
collected.push({ name, kind: 'sse-legacy', controller });
|
|
195
|
+
}
|
|
196
|
+
for (const name of this.dualModeControllerNames) {
|
|
197
|
+
// biome-ignore lint/suspicious/noExplicitAny: any dual-mode controller works here
|
|
198
|
+
const controller = this.diContainer.resolve(name);
|
|
199
|
+
collected.push({ name, kind: 'dualmode-legacy', controller });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
186
202
|
return buildGatewayManifestFrom(collected, options);
|
|
187
203
|
}
|
|
188
204
|
/**
|
|
@@ -273,15 +289,19 @@ export class DIContext {
|
|
|
273
289
|
}
|
|
274
290
|
}
|
|
275
291
|
applyDualModeRouteOptions(route, options) {
|
|
276
|
-
|
|
277
|
-
this.applyPreHandlers(route, options.preHandler);
|
|
278
|
-
}
|
|
279
|
-
if (options?.rateLimit) {
|
|
280
|
-
this.applyRateLimit(route, options.rateLimit);
|
|
281
|
-
}
|
|
282
|
-
applyGlobalSSEOptions(route, options);
|
|
292
|
+
this.applyStreamRouteOptions(route, options);
|
|
283
293
|
}
|
|
284
294
|
applySSERouteOptions(route, options) {
|
|
295
|
+
this.applyStreamRouteOptions(route, options);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Apply registration-time options to an SSE/dual-mode route before app.route().
|
|
299
|
+
*
|
|
300
|
+
* Shared by the SSE-only and dual-mode registration paths: both apply the same
|
|
301
|
+
* pre-handlers, rate limit and `sse` field defaults. Route-level options
|
|
302
|
+
* (buildHandler / buildApiRoute) take precedence over these.
|
|
303
|
+
*/
|
|
304
|
+
applyStreamRouteOptions(route, options) {
|
|
285
305
|
if (options?.preHandler) {
|
|
286
306
|
this.applyPreHandlers(route, options.preHandler);
|
|
287
307
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAK9C,OAAO,EAAE,iCAAiC,EAAsB,MAAM,kBAAkB,CAAA;AAGxF,OAAO,EAEL,wBAAwB,GAGzB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,iBAAiB,GAGlB,MAAM,mBAAmB,CAAA;AA8B1B;;;;;;;;;GASG;AACH,SAAS,qBAAqB,CAC5B,KAAmB,EACnB,OAAoE;IAEpE,IAAI,OAAO,EAAE,SAAS,KAAK,SAAS,IAAI,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;QAC1E,OAAM;IACR,CAAC;IAED,MAAM,YAAY,GAAG,KAAyC,CAAA;IAC9D,MAAM,cAAc,GAAG,YAAY,CAAC,GAAG,CAAA;IACvC,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,OAAM;IACR,CAAC;IAED,2FAA2F;IAC3F,MAAM,cAAc,GAClB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,EAAE,IAAI,EAAE,cAA2D,EAAE;QACvE,CAAC,CAAC,OAAO,cAAc,KAAK,QAAQ;YAClC,CAAC,CAAE,cAAuC;YAC1C,CAAC,CAAC,EAAE,CAAA;IAEV,YAAY,CAAC,GAAG,GAAG;QACjB,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;QACxE,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;QAC3E,GAAG,cAAc;KACa,CAAA;AAClC,CAAC;AAED,MAAM,OAAO,SAAS;IAKH,OAAO,CAA4B;IACpC,aAAa,CAAe;IAC5B,WAAW,CAA+B;IAC1D,8EAA8E;IAC7D,mBAAmB,CAAkD;IACtF,mFAAmF;IAClE,kBAAkB,CAAU;IAC7C,yFAAyF;IACxE,uBAAuB,CAAU;IAClD,2FAA2F;IAC1E,kBAAkB,CAAU;IAC5B,SAAS,CAAQ;IAElC,YACE,WAA0C,EAC1C,OAAmC,EACnC,SAAiB,EACjB,aAA6B;QAE7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,aAAa;YAChB,aAAa;gBACb,IAAI,aAAa,CAAC;oBAChB,YAAY,EAAE,IAAI;oBAClB,SAAS,EAAE,IAAI;oBACf,WAAW;oBACX,WAAW,EAAE,IAAI;oBACjB,qBAAqB,EAAE,IAAI;iBAC5B,CAAC,CAAA;QACJ,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAA;QAC7B,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAA;QAC5B,IAAI,CAAC,uBAAuB,GAAG,EAAE,CAAA;QACjC,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAA;IAC9B,CAAC;IAEO,mBAAmB;IACzB,4FAA4F;IAC5F,WAAgC,EAChC,cAAqD;QAErD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAC3D,IAAI,QAAQ,CAAC,oBAAoB,EAAE,CAAC;gBAClC,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACvC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;gBACpC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAClC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;gBACpC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAClC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAA6B,EAAE,CAAC,CAAA;YAClF,CAAC;QACH,CAAC;IACH,CAAC;IAEO,cAAc,CACpB,MAAqD,EACrD,cAAqD,EACrD,oBAA0C,EAC1C,kBAA2B,EAC3B,eAAwB;QAExB,MAAM,gBAAgB,GAAG,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAA;QAEvF,KAAK,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;YACnC,2DAA2D;YAC3D,IAAI,eAAe,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBACpD,2DAA2D;gBAC3D,cAAc,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAA;YAC7C,CAAC;QACH,CAAC;QAED,IAAI,eAAe,IAAI,kBAAkB,EAAE,CAAC;YAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YAE3D,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;IAED,oBAAoB,CAClB,MAA8E,EAC9E,oBAA0C,EAC1C,kBAAkB,GAAG,IAAI;QAEzB,MAAM,eAAe,GAAG,iCAAiC,CACvD,IAAI,CAAC,SAAS,EACd,MAAM,CAAC,kBAAkB,IAAI,QAAQ,EACrC,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,mBAAmB,IAAI,EAAE,CACjC,CAAA;QACD,MAAM,cAAc,GAA0C,EAAE,CAAA;QAEhE,KAAK,MAAM,aAAa,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC3C,IAAI,CAAC,cAAc,CACjB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,EAClB,IAAI,CACL,CAAA;QACH,CAAC;QAED,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC5B,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;gBACtD,IAAI,CAAC,cAAc,CACjB,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,CACN,CAAA;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAmD,CAAC,CAAA;QAE9E,8BAA8B;QAC9B,0CAA0C;QAC1C,KAAK,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,CAAC;YAChF,MAAM,eAAe,GAAG,EAAE,GAAI,gBAAsC,EAAE,CAAA;YAEtE,2CAA2C;YAC3C,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,aAAa,CAAC,CAAA;YACxE,mBAAmB;YACnB,IAAI,eAAe,CAAC,QAAQ,KAAK,gBAAgB,CAAC,QAAQ,EAAE,CAAC;gBAC3D,mBAAmB;gBACnB,eAAe,CAAC,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAA;YACtD,CAAC;YAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;QAC3D,CAAC;IACH,CAAC;IAED,4FAA4F;IAC5F,cAAc,CAAC,GAAwC;QACrD,KAAK,MAAM,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACpD,wEAAwE;YACxE,MAAM,UAAU,GAA4B,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC9E,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,EAAE,CAAA;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC1C,mFAAmF;gBACnF,2EAA2E;gBAC3E,GAAG,CAAC,KAAK,CAAC,KAAkB,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACrD,6EAA6E;YAC7E,MAAM,UAAU,GAA+B,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAEvF,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBACrD,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,oBAAoB,CAAC,OAAoC;QACvD,MAAM,SAAS,GAA0B,EAAE,CAAA;QAE3C,KAAK,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC1D,wEAAwE;YACxE,MAAM,UAAU,GAA4B,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC9E,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;QACpD,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC3C,4EAA4E;YAC5E,MAAM,UAAU,GAA+B,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YAC7E,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,IAAI,OAAO,CAAC,2BAA2B,EAAE,CAAC;YACxC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAC3C,4EAA4E;gBAC5E,MAAM,UAAU,GAA+B,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBAC7E,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC,CAAA;YAC1D,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,uBAAuB,EAAE,CAAC;gBAChD,kFAAkF;gBAClF,MAAM,UAAU,GAAoC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;gBAClF,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,UAAU,EAAE,CAAC,CAAA;YAC/D,CAAC;QACH,CAAC;QAED,OAAO,wBAAwB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACrD,CAAC;IAED;;;OAGG;IACH,iBAAiB;QACf,OAAO,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC,CAAA;IAC3C,CAAC;IAED;;;OAGG;IACH,sBAAsB;QACpB,OAAO,IAAI,CAAC,uBAAuB,CAAC,MAAM,GAAG,CAAC,CAAA;IAChD,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB;IACf,iFAAiF;IACjF,GAAwC,EACxC,OAAkC;QAElC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC9B,OAAM;QACR,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACrD,uDAAuD;YACvD,MAAM,aAAa,GACjB,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAC1C,MAAM,SAAS,GAAG,aAAa,CAAC,cAAc,EAAE,CAAA;YAEhD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;gBACnD,MAAM,KAAK,GAAG,iBAAiB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;gBAC3D,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;gBACzC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,sBAAsB;IACpB,iFAAiF;IACjF,GAAwC,EACxC,OAAuC;QAEvC,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,CAAC;YACnC,OAAM;QACR,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,uBAAuB,EAAE,CAAC;YAC1D,uDAAuD;YACvD,MAAM,kBAAkB,GAEpB,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAC5C,MAAM,cAAc,GAAG,kBAAkB,CAAC,mBAAmB,EAAE,CAAA;YAE/D,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;gBACxD,MAAM,KAAK,GAAG,iBAAiB,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAA;gBAChE,IAAI,CAAC,yBAAyB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;gBAC9C,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAEO,yBAAyB,CAC/B,KAAmB,EACnB,OAAuC;QAEvC,IAAI,CAAC,uBAAuB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;IAEO,oBAAoB,CAAC,KAAmB,EAAE,OAAkC;QAClF,IAAI,CAAC,uBAAuB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;IAED;;;;;;OAMG;IACK,uBAAuB,CAC7B,KAAmB,EACnB,OAAkE;QAElE,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;YACxB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAClD,CAAC;QACD,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QAC/C,CAAC;QACD,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACvC,CAAC;IAEO,gBAAgB,CACtB,KAAmB,EACnB,gBAA4C;QAE5C,MAAM,kBAAkB,GAAG,KAAK,CAAC,UAAU,CAAA;QAC3C,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACxB,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAA;YACnC,OAAM;QACR,CAAC;QACD,2EAA2E;QAC3E,MAAM,QAAQ,GAAU,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC;YACvD,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAA;QACxB,2EAA2E;QAC3E,MAAM,cAAc,GAAU,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC;YAC3D,CAAC,CAAC,gBAAgB;YAClB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAA;QACtB,KAAK,CAAC,UAAU,GAAG,CAAC,GAAG,cAAc,EAAE,GAAG,QAAQ,CAAC,CAAA;IACrD,CAAC;IAEO,cAAc,CACpB,KAAmB,EACnB,SAA6D;QAE7D,2EAA2E;QAC3E,MAAM,eAAe,GAAG,KAAwC,CAAA;QAChE,eAAe,CAAC,MAAM,GAAG;YACvB,GAAG,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,CAAC;YACjC,SAAS;SACV,CAAA;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,CAAA;QACzC,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAA;IACxC,CAAC;CACF"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ApiContract } from '@lokalise/api-contracts';
|
|
2
2
|
import { type ApiRouteOptions as FastifyApiRouteOptions, type InferApiHandler } from '@lokalise/fastify-api-contracts';
|
|
3
3
|
import type { RouteOptions } from 'fastify';
|
|
4
4
|
import type { GatewayMetadata } from '../gateway/gatewayTypes.ts';
|
|
5
|
+
import type { SSERoomBroadcaster } from '../sse/rooms/SSERoomBroadcaster.ts';
|
|
6
|
+
import { type SSERoomsOptions } from './apiSseConnectionRegistry.ts';
|
|
5
7
|
/**
|
|
6
8
|
* Options for configuring an ApiContract route.
|
|
7
9
|
*
|
|
@@ -46,6 +48,42 @@ export type ApiRouteOptions<Contract extends ApiContract> = FastifyApiRouteOptio
|
|
|
46
48
|
* ```
|
|
47
49
|
*/
|
|
48
50
|
gatewayMetadata?: GatewayMetadata<Contract>;
|
|
51
|
+
/**
|
|
52
|
+
* Enable SSE rooms for this route by passing the shared `SSERoomBroadcaster`
|
|
53
|
+
* from the DI container. Sessions opened by the route's SSE handler are
|
|
54
|
+
* registered with the broadcaster (so `broadcastToRoom`/`broadcastMessage`
|
|
55
|
+
* reach them), get room operations via {@link getSessionRooms}, and are
|
|
56
|
+
* cleaned up (rooms left, dedup cache cleared) when the connection closes.
|
|
57
|
+
*
|
|
58
|
+
* Without this option `getSessionRooms()` returns no-ops and the session
|
|
59
|
+
* receives no room broadcasts.
|
|
60
|
+
*
|
|
61
|
+
* Pass an {@link SSERoomsOptions} object instead of the bare broadcaster to
|
|
62
|
+
* declare an `authorizeJoin` scope check (so a room name built from a path
|
|
63
|
+
* param cannot leak across tenants) and a `maxSessionLifetimeMs` bound (so
|
|
64
|
+
* authorization checked at connect cannot stay in force forever).
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* buildApiRoute(contracts.watchProject, this.watch, { sseRooms: this.roomBroadcaster })
|
|
69
|
+
* // inside the handler:
|
|
70
|
+
* const session = sse.start('keepAlive')
|
|
71
|
+
* getSessionRooms(session).join(`project:${request.params.projectId}`)
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* buildApiRoute(contracts.watchProject, this.watch, {
|
|
77
|
+
* sseRooms: {
|
|
78
|
+
* broadcaster: this.roomBroadcaster,
|
|
79
|
+
* authorizeJoin: (session, room) =>
|
|
80
|
+
* this.membership.canRead(session.request.user, room),
|
|
81
|
+
* maxSessionLifetimeMs: 30 * 60 * 1000,
|
|
82
|
+
* },
|
|
83
|
+
* })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
sseRooms?: SSERoomBroadcaster | SSERoomsOptions;
|
|
49
87
|
};
|
|
50
88
|
/**
|
|
51
89
|
* Build a Fastify `RouteOptions` object from an `ApiContract` + handler.
|
|
@@ -56,10 +94,14 @@ export type ApiRouteOptions<Contract extends ApiContract> = FastifyApiRouteOptio
|
|
|
56
94
|
* See its docs for the `(request, reply, context) => { status, body }`
|
|
57
95
|
* handler model and `context.sse` streaming.
|
|
58
96
|
*
|
|
59
|
-
* On top of the package builder this adds
|
|
60
|
-
* per-route gateway policy with header / query keys
|
|
61
|
-
* contract; equivalent to wrapping the result with
|
|
62
|
-
* See `ApiRouteOptions` for full details.
|
|
97
|
+
* On top of the package builder this adds:
|
|
98
|
+
* - `gatewayMetadata` — per-route gateway policy with header / query keys
|
|
99
|
+
* narrowed to the contract; equivalent to wrapping the result with
|
|
100
|
+
* `withGatewayMetadata`. See `ApiRouteOptions` for full details.
|
|
101
|
+
* - `sseRooms` — wires SSE rooms for the route (see `ApiRouteOptions`).
|
|
102
|
+
* - a `streaming: 'sse' | 'dual'` marker on SSE-capable routes, read back by
|
|
103
|
+
* the gateway manifest builder so generators can apply streaming-appropriate
|
|
104
|
+
* timeouts and buffering.
|
|
63
105
|
*
|
|
64
106
|
* @returns Fastify `RouteOptions` ready to pass to `app.route()`
|
|
65
107
|
*/
|
|
@@ -1,7 +1,41 @@
|
|
|
1
|
-
import { getSseSchemaByEventName, } from '@lokalise/api-contracts';
|
|
1
|
+
import { getSseSchemaByEventName, hasAnySuccessSseResponse, isContentResponseEntry, isSseBody, SUCCESSFUL_HTTP_STATUS_CODES, } from '@lokalise/api-contracts';
|
|
2
2
|
import { buildFastifyApiRoute, } from '@lokalise/fastify-api-contracts';
|
|
3
|
+
import { attachRouteStreamingMode } from "../gateway/routeStreaming.js";
|
|
3
4
|
import { attachGatewayMetadata } from "../gateway/withGatewayMetadata.js";
|
|
4
5
|
import { attachSSESendDiagnostics, reportSSEHandlerOutcome } from "../sse/sseSendDiagnostics.js";
|
|
6
|
+
import { withSessionRooms } from "./apiSseConnectionRegistry.js";
|
|
7
|
+
/**
|
|
8
|
+
* True when the contract's success response for a status code has a non-SSE
|
|
9
|
+
* representation as well — i.e. the route can answer a plain HTTP request.
|
|
10
|
+
*/
|
|
11
|
+
function isSuccessResponseDual(value) {
|
|
12
|
+
if (isContentResponseEntry(value)) {
|
|
13
|
+
// A content-map entry offers a non-SSE representation when it allows an empty
|
|
14
|
+
// body or declares any non-SSE media type descriptor.
|
|
15
|
+
if (value.allowNoBody || !value.content)
|
|
16
|
+
return true;
|
|
17
|
+
return Object.values(value.content).some((descriptor) => !isSseBody(descriptor));
|
|
18
|
+
}
|
|
19
|
+
// A bare Zod schema is a JSON response, which always has a non-SSE representation.
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Derive how a contract streams, for the gateway streaming marker.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors `ContractResponseMode` from `@lokalise/api-contracts` at runtime:
|
|
26
|
+
* a contract with no SSE success response is plain, one whose SSE statuses all
|
|
27
|
+
* lack a non-SSE representation is `'sse'`, and one that offers both is `'dual'`.
|
|
28
|
+
*/
|
|
29
|
+
function getContractStreamingMode(contract) {
|
|
30
|
+
if (!hasAnySuccessSseResponse(contract))
|
|
31
|
+
return undefined;
|
|
32
|
+
for (const code of SUCCESSFUL_HTTP_STATUS_CODES) {
|
|
33
|
+
const value = contract.responsesByStatusCode[code];
|
|
34
|
+
if (value && isSuccessResponseDual(value))
|
|
35
|
+
return 'dual';
|
|
36
|
+
}
|
|
37
|
+
return 'sse';
|
|
38
|
+
}
|
|
5
39
|
/**
|
|
6
40
|
* Build a Fastify `RouteOptions` object from an `ApiContract` + handler.
|
|
7
41
|
*
|
|
@@ -11,24 +45,39 @@ import { attachSSESendDiagnostics, reportSSEHandlerOutcome } from "../sse/sseSen
|
|
|
11
45
|
* See its docs for the `(request, reply, context) => { status, body }`
|
|
12
46
|
* handler model and `context.sse` streaming.
|
|
13
47
|
*
|
|
14
|
-
* On top of the package builder this adds
|
|
15
|
-
* per-route gateway policy with header / query keys
|
|
16
|
-
* contract; equivalent to wrapping the result with
|
|
17
|
-
* See `ApiRouteOptions` for full details.
|
|
48
|
+
* On top of the package builder this adds:
|
|
49
|
+
* - `gatewayMetadata` — per-route gateway policy with header / query keys
|
|
50
|
+
* narrowed to the contract; equivalent to wrapping the result with
|
|
51
|
+
* `withGatewayMetadata`. See `ApiRouteOptions` for full details.
|
|
52
|
+
* - `sseRooms` — wires SSE rooms for the route (see `ApiRouteOptions`).
|
|
53
|
+
* - a `streaming: 'sse' | 'dual'` marker on SSE-capable routes, read back by
|
|
54
|
+
* the gateway manifest builder so generators can apply streaming-appropriate
|
|
55
|
+
* timeouts and buffering.
|
|
18
56
|
*
|
|
19
57
|
* @returns Fastify `RouteOptions` ready to pass to `app.route()`
|
|
20
58
|
*/
|
|
21
59
|
export function buildApiRoute(contract, handler, options) {
|
|
22
|
-
// Gateway metadata is stamped via Symbol, not spread into Fastify options
|
|
23
|
-
|
|
60
|
+
// Gateway metadata is stamped via Symbol, not spread into Fastify options;
|
|
61
|
+
// `sseRooms` is framework-level and composed into the SSE lifecycle hooks.
|
|
62
|
+
const { gatewayMetadata, sseRooms, ...passthroughOptions } = options ?? {};
|
|
63
|
+
const fastifyOptions = sseRooms
|
|
64
|
+
? withSessionRooms(sseRooms, passthroughOptions)
|
|
65
|
+
: passthroughOptions;
|
|
24
66
|
const schemaByEventName = getSseSchemaByEventName(contract);
|
|
25
67
|
const built = buildFastifyApiRoute(contract, handler, schemaByEventName ? withSendDiagnostics(fastifyOptions, schemaByEventName) : fastifyOptions);
|
|
26
68
|
// Recording a failed send is only half of the diagnostic: whether the route recovered from
|
|
27
69
|
// it decides whether a test reading the stream should fail on it. That is what the handler's
|
|
28
|
-
// own outcome says, so it is observed here
|
|
70
|
+
// own outcome says, so it is observed here, for scoped requests only.
|
|
29
71
|
const route = schemaByEventName
|
|
30
72
|
? { ...built, handler: reportSSEHandlerOutcome(built.handler) }
|
|
31
73
|
: built;
|
|
74
|
+
// Mark streaming routes (derived from the contract's response mode) so
|
|
75
|
+
// gateway generators can apply streaming-appropriate timeouts/buffering.
|
|
76
|
+
// After the spread above: it drops the non-enumerable symbol the stamp sets.
|
|
77
|
+
const streamingMode = getContractStreamingMode(contract);
|
|
78
|
+
if (streamingMode) {
|
|
79
|
+
attachRouteStreamingMode(route, streamingMode);
|
|
80
|
+
}
|
|
32
81
|
return gatewayMetadata !== undefined ? attachGatewayMetadata(route, gatewayMetadata) : route;
|
|
33
82
|
}
|
|
34
83
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiRouteBuilder.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiRouteBuilder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"apiRouteBuilder.js","sourceRoot":"","sources":["../../../lib/api-contracts/apiRouteBuilder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { SSEEventSchemas } from '@lokalise/api-contracts';
|
|
2
|
+
import type { ApiRouteOptions as FastifyApiRouteOptions, SSESession } from '@lokalise/fastify-api-contracts';
|
|
3
|
+
import type { SSERoomBroadcaster } from '../sse/rooms/SSERoomBroadcaster.ts';
|
|
4
|
+
import type { SSERoomOperations } from '../sse/rooms/types.ts';
|
|
5
|
+
import type { SSEMessage } from '../sse/sseTypes.ts';
|
|
6
|
+
/**
|
|
7
|
+
* A join whose `authorizeJoin` verdict has not resolved yet.
|
|
8
|
+
*
|
|
9
|
+
* An async verdict lands after `join()` has already returned, so anything that
|
|
10
|
+
* revokes access in between (a `leave`, an `evictFromRoom`, an eviction, a
|
|
11
|
+
* closed room, the session closing) has to be able to cancel it. Without that
|
|
12
|
+
* the resolved verdict re-adds the connection to a room it was just removed
|
|
13
|
+
* from, and the revocation silently does not stick.
|
|
14
|
+
*/
|
|
15
|
+
export type PendingJoin = {
|
|
16
|
+
readonly room: string;
|
|
17
|
+
cancelled: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare class ApiSseConnectionRegistry {
|
|
20
|
+
private readonly connections;
|
|
21
|
+
/** In-flight async joins per connection id. Empty for a synchronous verdict. */
|
|
22
|
+
private readonly pendingJoins;
|
|
23
|
+
private readonly broadcaster;
|
|
24
|
+
constructor(broadcaster: SSERoomBroadcaster);
|
|
25
|
+
/**
|
|
26
|
+
* Register an active SSE connection with its send function.
|
|
27
|
+
* The send function must return `false` (not throw) on delivery failure so
|
|
28
|
+
* room broadcasts keep fanning out to the remaining connections.
|
|
29
|
+
*
|
|
30
|
+
* Pass `close` to make the connection evictable — without it {@link evict}
|
|
31
|
+
* can stop delivery but cannot terminate the stream.
|
|
32
|
+
*/
|
|
33
|
+
register(connectionId: string, send: (message: SSEMessage) => Promise<boolean>, close?: () => void): void;
|
|
34
|
+
/**
|
|
35
|
+
* Remove a connection: drops the sender, leaves all rooms, and clears the
|
|
36
|
+
* broadcaster's dedup cache for the connection.
|
|
37
|
+
*/
|
|
38
|
+
unregister(connectionId: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Record an async `authorizeJoin` verdict that is still in flight, so a
|
|
41
|
+
* revocation arriving before it resolves can cancel it.
|
|
42
|
+
*
|
|
43
|
+
* Pair every call with {@link settlePendingJoin}; the room wiring in
|
|
44
|
+
* {@link withSessionRooms} does this for routes.
|
|
45
|
+
*/
|
|
46
|
+
beginPendingJoin(connectionId: string, room: string): PendingJoin;
|
|
47
|
+
/**
|
|
48
|
+
* Retire an in-flight join.
|
|
49
|
+
*
|
|
50
|
+
* @returns `true` when the join is still valid and may be applied, `false`
|
|
51
|
+
* when it was cancelled while the verdict was pending.
|
|
52
|
+
*/
|
|
53
|
+
settlePendingJoin(connectionId: string, pending: PendingJoin): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Cancel in-flight joins for a connection — all of them, or only those for
|
|
56
|
+
* one room. A cancelled join is dropped when its verdict resolves.
|
|
57
|
+
*
|
|
58
|
+
* @returns How many joins were cancelled.
|
|
59
|
+
*/
|
|
60
|
+
cancelPendingJoins(connectionId: string, room?: string): number;
|
|
61
|
+
/**
|
|
62
|
+
* Terminate one connection: leave its rooms, stop delivering to it, and
|
|
63
|
+
* close its stream.
|
|
64
|
+
*
|
|
65
|
+
* This is the revocation path. Authorization is checked when a stream opens
|
|
66
|
+
* and then goes stale, so removing a principal's access has to be able to
|
|
67
|
+
* end the streams that access already opened; without a call like this, a
|
|
68
|
+
* revoked user keeps receiving broadcasts until they close the tab.
|
|
69
|
+
*
|
|
70
|
+
* A client that reconnects (as `@opinionated-machine/sse-fallback` does)
|
|
71
|
+
* comes back through the route's own authorization, so an eviction of a
|
|
72
|
+
* still-authorized principal costs a reconnect, not a broken surface.
|
|
73
|
+
*
|
|
74
|
+
* @returns `true` when a connection was registered under that id.
|
|
75
|
+
*/
|
|
76
|
+
evict(connectionId: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Remove one connection from one room, leaving its other rooms and its
|
|
79
|
+
* stream intact. For revoking access to a single scope.
|
|
80
|
+
*
|
|
81
|
+
* A join whose async `authorizeJoin` verdict is still pending counts as
|
|
82
|
+
* membership here: the connection is not in the room yet, but it is about to
|
|
83
|
+
* be, and letting that land after a revocation would undo it.
|
|
84
|
+
*
|
|
85
|
+
* @returns `true` when the connection was in the room, or was on its way in.
|
|
86
|
+
*/
|
|
87
|
+
evictFromRoom(room: string, connectionId: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Evict every connection currently in a room, terminating their streams.
|
|
90
|
+
* For revoking a whole scope (a deleted project, a disbanded team).
|
|
91
|
+
*
|
|
92
|
+
* Only connections on THIS node are closed — room membership elsewhere in
|
|
93
|
+
* the cluster is another node's registry to evict, so a revocation event
|
|
94
|
+
* has to reach every node.
|
|
95
|
+
*
|
|
96
|
+
* Joins still awaiting an async `authorizeJoin` verdict are cancelled rather
|
|
97
|
+
* than evicted: they hold no membership yet, so the revocation just denies
|
|
98
|
+
* them the room and leaves their other rooms and their stream alone. They
|
|
99
|
+
* are not counted in the return value for the same reason.
|
|
100
|
+
*
|
|
101
|
+
* @returns How many connections were evicted here.
|
|
102
|
+
*/
|
|
103
|
+
closeRoom(room: string): number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get (or lazily create) the shared registry for a broadcaster.
|
|
107
|
+
* Ensures `registerSender` is called exactly once per broadcaster no matter
|
|
108
|
+
* how many routes opt into rooms.
|
|
109
|
+
*/
|
|
110
|
+
export declare function getApiSseConnectionRegistry(broadcaster: SSERoomBroadcaster): ApiSseConnectionRegistry;
|
|
111
|
+
/**
|
|
112
|
+
* Room operations for an SSE session opened by a `buildApiRoute` route.
|
|
113
|
+
*
|
|
114
|
+
* Returns no-ops when the route did not pass `sseRooms` — mirroring the legacy
|
|
115
|
+
* `session.rooms` accessor, which is inert on controllers without a broadcaster.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* const session = sse.start('keepAlive')
|
|
120
|
+
* getSessionRooms(session).join(`project:${request.params.projectId}`)
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export declare function getSessionRooms<Events extends SSEEventSchemas, Context>(session: SSESession<Events, Context>): SSERoomOperations;
|
|
124
|
+
/**
|
|
125
|
+
* Room wiring for a route, when the bare broadcaster is not enough.
|
|
126
|
+
*
|
|
127
|
+
* Pass an `SSERoomBroadcaster` directly for the default behaviour; pass this
|
|
128
|
+
* object to add a scope check on joins and a bounded session lifetime.
|
|
129
|
+
*/
|
|
130
|
+
export type SSERoomsOptions = {
|
|
131
|
+
/** The broadcaster whose rooms this route's sessions join. */
|
|
132
|
+
broadcaster: SSERoomBroadcaster;
|
|
133
|
+
/**
|
|
134
|
+
* Decide whether a session may join a room. Declared once per route, so the
|
|
135
|
+
* scope check is not left to every handler body.
|
|
136
|
+
*
|
|
137
|
+
* Without it, `getSessionRooms(session).join(room)` joins whatever room
|
|
138
|
+
* string the handler names, and nothing checks that the authenticated
|
|
139
|
+
* principal belongs to that room's scope — a path param read straight into
|
|
140
|
+
* a room name is a cross-tenant leak.
|
|
141
|
+
*
|
|
142
|
+
* Return `false` (or reject) to refuse; the refusal is logged on the
|
|
143
|
+
* request logger and the join is dropped. A synchronous verdict is applied
|
|
144
|
+
* before `join()` returns; an async one is applied when it resolves, so the
|
|
145
|
+
* session joins a moment later — the client's reconciliation poll covers
|
|
146
|
+
* anything broadcast in between. A `leave`, an `evictFromRoom`, an `evict`,
|
|
147
|
+
* a `closeRoom` or the session closing while the verdict is pending cancels
|
|
148
|
+
* it, so a revocation cannot be undone by a join that was already in flight.
|
|
149
|
+
*/
|
|
150
|
+
authorizeJoin?: (session: SSESession, room: string) => boolean | Promise<boolean>;
|
|
151
|
+
/**
|
|
152
|
+
* Close the session gracefully after this many milliseconds.
|
|
153
|
+
*
|
|
154
|
+
* Authorization is checked when the stream opens and then goes stale: a
|
|
155
|
+
* principal removed from a scope keeps receiving events for as long as the
|
|
156
|
+
* connection lives. A bounded lifetime forces a reconnect, and the reconnect
|
|
157
|
+
* re-runs authentication and authorization — making this both the
|
|
158
|
+
* token-refresh mechanism and the backstop for a revocation that never
|
|
159
|
+
* reached {@link ApiSseConnectionRegistry.evict}.
|
|
160
|
+
*
|
|
161
|
+
* A client that treats a server close as a routine reconnect (with
|
|
162
|
+
* `Last-Event-ID` and a reconciliation poll, as
|
|
163
|
+
* `@opinionated-machine/sse-fallback` does) makes this invisible to users.
|
|
164
|
+
*/
|
|
165
|
+
maxSessionLifetimeMs?: number;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Compose room wiring into a route's SSE lifecycle hooks.
|
|
169
|
+
*
|
|
170
|
+
* On connect the session is registered with the broadcaster's registry and its
|
|
171
|
+
* room operations are published for {@link getSessionRooms}; on close the
|
|
172
|
+
* connection is unregistered (rooms left, dedup cache cleared). The route's own
|
|
173
|
+
* `onConnect` / `onClose` hooks still run — room wiring is applied first on
|
|
174
|
+
* connect (so a hook can already join rooms) and last on close.
|
|
175
|
+
*
|
|
176
|
+
* `onConnect` is invoked synchronously by the package's `sse.start()`, so the
|
|
177
|
+
* registration is in place before `start()` returns to the handler.
|
|
178
|
+
*
|
|
179
|
+
* Pass {@link SSERoomsOptions} instead of a bare broadcaster to add an
|
|
180
|
+
* `authorizeJoin` scope check or a `maxSessionLifetimeMs` bound.
|
|
181
|
+
*/
|
|
182
|
+
export declare function withSessionRooms<Options extends FastifyApiRouteOptions>(config: SSERoomBroadcaster | SSERoomsOptions, options: Options): Options;
|