@fluojs/http 1.0.0-beta.9 → 1.1.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.
Files changed (34) hide show
  1. package/README.ko.md +78 -11
  2. package/README.md +78 -11
  3. package/dist/context/request-context-node-store.d.ts +25 -0
  4. package/dist/context/request-context-node-store.d.ts.map +1 -0
  5. package/dist/context/request-context-node-store.js +32 -0
  6. package/dist/context/request-context-stack-store.d.ts +8 -0
  7. package/dist/context/request-context-stack-store.d.ts.map +1 -0
  8. package/dist/context/request-context-stack-store.js +29 -0
  9. package/dist/context/request-context-store.d.ts +7 -0
  10. package/dist/context/request-context-store.d.ts.map +1 -0
  11. package/dist/context/request-context-store.js +1 -0
  12. package/dist/context/request-context.d.ts +7 -3
  13. package/dist/context/request-context.d.ts.map +1 -1
  14. package/dist/context/request-context.js +17 -5
  15. package/dist/context/sse.d.ts +20 -0
  16. package/dist/context/sse.d.ts.map +1 -1
  17. package/dist/context/sse.js +19 -0
  18. package/dist/decorators.d.ts +10 -0
  19. package/dist/decorators.d.ts.map +1 -1
  20. package/dist/decorators.js +279 -53
  21. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  22. package/dist/dispatch/dispatcher.js +142 -2
  23. package/dist/dispatch/fast-path/eligibility-checker.d.ts +22 -0
  24. package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -1
  25. package/dist/dispatch/fast-path/eligibility-checker.js +32 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/internal.d.ts +2 -0
  30. package/dist/internal.d.ts.map +1 -1
  31. package/dist/internal.js +1 -0
  32. package/dist/middleware/correlation.d.ts.map +1 -1
  33. package/dist/middleware/correlation.js +8 -2
  34. package/package.json +4 -4
@@ -1,8 +1,11 @@
1
- import { getStandardMetadataBag as readStandardMetadataBag, metadataSymbol } from '@fluojs/core/internal';
1
+ import { defineControllerMetadata, defineDtoFieldBindingMetadata, defineRouteMetadata, ensureMetadataSymbol, getControllerMetadata, getDtoFieldBindingMetadata, getRouteMetadata, getStandardMetadataBag as readStandardMetadataBag } from '@fluojs/core/internal';
2
2
  import { validateRoutePath } from './route-path.js';
3
3
  const standardControllerMetadataKey = Symbol.for('fluo.standard.controller');
4
4
  const standardRouteMetadataKey = Symbol.for('fluo.standard.route');
5
5
  const standardDtoBindingMetadataKey = Symbol.for('fluo.standard.dto-binding');
6
+ const legacyRouteMetadataStore = new WeakMap();
7
+ const legacyDtoBindingMetadataStore = new WeakMap();
8
+ ensureMetadataSymbol();
6
9
  function normalizeProducesMediaTypes(mediaTypes) {
7
10
  const normalized = [];
8
11
  for (const mediaType of mediaTypes) {
@@ -24,8 +27,115 @@ function mergeUnique(existing, values) {
24
27
  return merged;
25
28
  }
26
29
  function getStandardMetadataBag(metadata) {
27
- void metadataSymbol;
28
- return metadata;
30
+ return typeof metadata === 'object' && metadata !== null ? metadata : {};
31
+ }
32
+ function isStandardDecoratorContext(value) {
33
+ return typeof value === 'object' && value !== null && 'kind' in value && 'name' in value;
34
+ }
35
+ function isMetadataPropertyKey(value) {
36
+ return typeof value === 'string' || typeof value === 'symbol';
37
+ }
38
+ function getLegacyRouteRecord(target, propertyKey) {
39
+ let routeMap = legacyRouteMetadataStore.get(target);
40
+ if (!routeMap) {
41
+ routeMap = new Map();
42
+ legacyRouteMetadataStore.set(target, routeMap);
43
+ }
44
+ let record = routeMap.get(propertyKey);
45
+ if (!record) {
46
+ record = {};
47
+ routeMap.set(propertyKey, record);
48
+ }
49
+ return record;
50
+ }
51
+ function flushLegacyRouteMetadata(target, propertyKey, record) {
52
+ if (!record.method || record.path === undefined) {
53
+ return;
54
+ }
55
+ const existing = getRouteMetadata(target, propertyKey);
56
+ const metadata = {
57
+ guards: mergeUnique(existing?.guards, record.guards ?? []),
58
+ headers: record.headers ?? existing?.headers,
59
+ interceptors: mergeUnique(existing?.interceptors, record.interceptors ?? []),
60
+ method: record.method,
61
+ path: record.path,
62
+ redirect: record.redirect ?? existing?.redirect,
63
+ request: record.request ?? existing?.request,
64
+ successStatus: record.successStatus ?? existing?.successStatus,
65
+ version: record.version ?? existing?.version
66
+ };
67
+ defineRouteMetadata(target, propertyKey, metadata);
68
+ }
69
+ function mergeLegacyRouteMetadata(target, propertyKey, partial) {
70
+ const record = getLegacyRouteRecord(target, propertyKey);
71
+ Object.assign(record, partial);
72
+ flushLegacyRouteMetadata(target, propertyKey, record);
73
+ }
74
+ function defineLegacyControllerMetadata(target, partial) {
75
+ const existing = getControllerMetadata(target);
76
+ defineControllerMetadata(target, {
77
+ basePath: partial.basePath ?? existing?.basePath ?? '',
78
+ guards: partial.guards ?? existing?.guards,
79
+ interceptors: partial.interceptors ?? existing?.interceptors,
80
+ version: partial.version ?? existing?.version
81
+ });
82
+ }
83
+ function getLegacyDtoBindingRecord(target, propertyKey) {
84
+ let bindingMap = legacyDtoBindingMetadataStore.get(target);
85
+ if (!bindingMap) {
86
+ bindingMap = new Map();
87
+ legacyDtoBindingMetadataStore.set(target, bindingMap);
88
+ }
89
+ let record = bindingMap.get(propertyKey);
90
+ if (!record) {
91
+ record = {};
92
+ bindingMap.set(propertyKey, record);
93
+ }
94
+ return record;
95
+ }
96
+ function mergeLegacyDtoBinding(target, propertyKey, partial) {
97
+ const record = getLegacyDtoBindingRecord(target, propertyKey);
98
+ Object.assign(record, partial);
99
+ const source = record.source ?? getDtoFieldBindingMetadata(target, propertyKey)?.source;
100
+ if (!source) {
101
+ return;
102
+ }
103
+ defineDtoFieldBindingMetadata(target, propertyKey, {
104
+ converter: record.converter,
105
+ key: record.key,
106
+ optional: record.optional,
107
+ source
108
+ });
109
+ }
110
+ function appendLegacyRouteHeader(target, propertyKey, name, value) {
111
+ const record = getLegacyRouteRecord(target, propertyKey);
112
+ record.headers = [...(record.headers ?? []), {
113
+ name,
114
+ value
115
+ }];
116
+ flushLegacyRouteMetadata(target, propertyKey, record);
117
+ }
118
+ function appendLegacyControllerGuards(target, guards) {
119
+ const existing = getControllerMetadata(target);
120
+ defineLegacyControllerMetadata(target, {
121
+ guards: mergeUnique(existing?.guards, guards)
122
+ });
123
+ }
124
+ function appendLegacyControllerInterceptors(target, interceptors) {
125
+ const existing = getControllerMetadata(target);
126
+ defineLegacyControllerMetadata(target, {
127
+ interceptors: mergeUnique(existing?.interceptors, interceptors)
128
+ });
129
+ }
130
+ function appendLegacyRouteGuards(target, propertyKey, guards) {
131
+ const record = getLegacyRouteRecord(target, propertyKey);
132
+ record.guards = mergeUnique(record.guards, guards);
133
+ flushLegacyRouteMetadata(target, propertyKey, record);
134
+ }
135
+ function appendLegacyRouteInterceptors(target, propertyKey, interceptors) {
136
+ const record = getLegacyRouteRecord(target, propertyKey);
137
+ record.interceptors = mergeUnique(record.interceptors, interceptors);
138
+ flushLegacyRouteMetadata(target, propertyKey, record);
29
139
  }
30
140
  function getStandardControllerRecord(metadata) {
31
141
  const bag = getStandardMetadataBag(metadata);
@@ -74,32 +184,65 @@ function mergeStandardDtoBinding(metadata, propertyKey, partial) {
74
184
  ...partial
75
185
  });
76
186
  }
77
- function createRouteDecorator(method) {
187
+ function createRouteDecorator(method, produces) {
78
188
  return path => {
79
189
  validateRoutePath(path, `@${method}() path`);
80
- const decorator = (_value, context) => {
81
- const route = getStandardRouteRecord(context.metadata, context.name);
82
- route.method = method;
83
- route.path = path;
190
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
191
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
192
+ const route = getStandardRouteRecord(contextOrPropertyKey.metadata, contextOrPropertyKey.name);
193
+ route.method = method;
194
+ route.path = path;
195
+ if (produces) {
196
+ route.produces = normalizeProducesMediaTypes(produces);
197
+ }
198
+ return;
199
+ }
200
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
201
+ const routeMetadata = {
202
+ method,
203
+ path
204
+ };
205
+ if (produces) {
206
+ routeMetadata.produces = normalizeProducesMediaTypes(produces);
207
+ }
208
+ mergeLegacyRouteMetadata(valueOrTarget, contextOrPropertyKey, routeMetadata);
209
+ }
84
210
  };
85
211
  return decorator;
86
212
  };
87
213
  }
88
214
  function createRouteValueDecorator(apply) {
89
215
  return value => {
90
- const decorator = (_target, context) => {
91
- apply(getStandardRouteRecord(context.metadata, context.name), value);
216
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
217
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
218
+ apply(getStandardRouteRecord(contextOrPropertyKey.metadata, contextOrPropertyKey.name), value);
219
+ return;
220
+ }
221
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
222
+ const record = getLegacyRouteRecord(valueOrTarget, contextOrPropertyKey);
223
+ apply(record, value);
224
+ flushLegacyRouteMetadata(valueOrTarget, contextOrPropertyKey, record);
225
+ }
92
226
  };
93
227
  return decorator;
94
228
  };
95
229
  }
96
230
  function createDtoFieldDecorator(source) {
97
231
  return key => {
98
- const decorator = (_value, context) => {
99
- mergeStandardDtoBinding(context.metadata, context.name, {
100
- key,
101
- source
102
- });
232
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
233
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
234
+ mergeStandardDtoBinding(contextOrPropertyKey.metadata, contextOrPropertyKey.name, {
235
+ key,
236
+ source
237
+ });
238
+ return;
239
+ }
240
+ if (isMetadataPropertyKey(contextOrPropertyKey) && valueOrTarget && typeof valueOrTarget === 'object') {
241
+ mergeLegacyDtoBinding(valueOrTarget, contextOrPropertyKey, {
242
+ key,
243
+ source
244
+ });
245
+ }
103
246
  };
104
247
  return decorator;
105
248
  };
@@ -113,8 +256,14 @@ function createDtoFieldDecorator(source) {
113
256
  */
114
257
  export function Controller(basePath = '') {
115
258
  validateRoutePath(basePath, '@Controller() base path');
116
- const decorator = (_target, context) => {
117
- getStandardControllerRecord(context.metadata).basePath = basePath;
259
+ const decorator = (target, context) => {
260
+ if (isStandardDecoratorContext(context)) {
261
+ getStandardControllerRecord(context.metadata).basePath = basePath;
262
+ return;
263
+ }
264
+ defineLegacyControllerMetadata(target, {
265
+ basePath
266
+ });
118
267
  };
119
268
  return decorator;
120
269
  }
@@ -126,12 +275,26 @@ export function Controller(basePath = '') {
126
275
  * @returns A decorator that applies version metadata at class or method scope.
127
276
  */
128
277
  export function Version(version) {
129
- const decorator = (_target, context) => {
130
- if (context.kind === 'class') {
131
- getStandardControllerRecord(context.metadata).version = version;
278
+ const decorator = (target, contextOrPropertyKey) => {
279
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
280
+ if (contextOrPropertyKey.kind === 'class') {
281
+ getStandardControllerRecord(contextOrPropertyKey.metadata).version = version;
282
+ return;
283
+ }
284
+ getStandardRouteRecord(contextOrPropertyKey.metadata, contextOrPropertyKey.name).version = version;
132
285
  return;
133
286
  }
134
- getStandardRouteRecord(context.metadata, context.name).version = version;
287
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
288
+ mergeLegacyRouteMetadata(target, contextOrPropertyKey, {
289
+ version
290
+ });
291
+ return;
292
+ }
293
+ if (typeof target === 'function') {
294
+ defineLegacyControllerMetadata(target, {
295
+ version
296
+ });
297
+ }
135
298
  };
136
299
  return decorator;
137
300
  }
@@ -143,6 +306,16 @@ export function Version(version) {
143
306
  * @returns A method decorator that registers a `GET` handler mapping.
144
307
  */
145
308
  export const Get = createRouteDecorator('GET');
309
+ /**
310
+ * Registers a server-sent events route handler as `GET` with `text/event-stream` produces metadata.
311
+ *
312
+ * @param path Route path relative to the controller base path.
313
+ * @returns A method decorator that registers a `GET` SSE handler mapping.
314
+ *
315
+ * @remarks
316
+ * Handlers may return `SseResponse` for manual control or `AsyncIterable<SseMessage<T> | T>` for managed dispatcher streaming.
317
+ */
318
+ export const Sse = createRouteDecorator('GET', ['text/event-stream']);
146
319
  /**
147
320
  * Registers a `POST` route handler.
148
321
  *
@@ -235,7 +408,7 @@ export const HttpCode = createRouteValueDecorator((record, status) => {
235
408
  export function getRouteProducesMetadata(controllerToken, propertyKey) {
236
409
  const bag = readStandardMetadataBag(controllerToken);
237
410
  const routeMap = bag?.[standardRouteMetadataKey];
238
- const produces = routeMap?.get(propertyKey)?.produces;
411
+ const produces = routeMap?.get(propertyKey)?.produces ?? legacyRouteMetadataStore.get(controllerToken.prototype)?.get(propertyKey)?.produces;
239
412
  return produces ? [...produces] : undefined;
240
413
  }
241
414
 
@@ -281,10 +454,18 @@ export const FromBody = createDtoFieldDecorator('body');
281
454
  * @returns A field decorator that marks the DTO binding as optional.
282
455
  */
283
456
  export function Optional() {
284
- const decorator = (_value, context) => {
285
- mergeStandardDtoBinding(context.metadata, context.name, {
286
- optional: true
287
- });
457
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
458
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
459
+ mergeStandardDtoBinding(contextOrPropertyKey.metadata, contextOrPropertyKey.name, {
460
+ optional: true
461
+ });
462
+ return;
463
+ }
464
+ if (isMetadataPropertyKey(contextOrPropertyKey) && valueOrTarget && typeof valueOrTarget === 'object') {
465
+ mergeLegacyDtoBinding(valueOrTarget, contextOrPropertyKey, {
466
+ optional: true
467
+ });
468
+ }
288
469
  };
289
470
  return decorator;
290
471
  }
@@ -296,10 +477,18 @@ export function Optional() {
296
477
  * @returns A field decorator that stores converter metadata for the DTO field.
297
478
  */
298
479
  export function Convert(converter) {
299
- const decorator = (_value, context) => {
300
- mergeStandardDtoBinding(context.metadata, context.name, {
301
- converter
302
- });
480
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
481
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
482
+ mergeStandardDtoBinding(contextOrPropertyKey.metadata, contextOrPropertyKey.name, {
483
+ converter
484
+ });
485
+ return;
486
+ }
487
+ if (isMetadataPropertyKey(contextOrPropertyKey) && valueOrTarget && typeof valueOrTarget === 'object') {
488
+ mergeLegacyDtoBinding(valueOrTarget, contextOrPropertyKey, {
489
+ converter
490
+ });
491
+ }
303
492
  };
304
493
  return decorator;
305
494
  }
@@ -312,12 +501,18 @@ export function Convert(converter) {
312
501
  * @returns A method decorator that appends route-level response-header metadata.
313
502
  */
314
503
  export function Header(name, value) {
315
- const decorator = (_target, context) => {
316
- const route = getStandardRouteRecord(context.metadata, context.name);
317
- route.headers = [...(route.headers ?? []), {
318
- name,
319
- value
320
- }];
504
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
505
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
506
+ const route = getStandardRouteRecord(contextOrPropertyKey.metadata, contextOrPropertyKey.name);
507
+ route.headers = [...(route.headers ?? []), {
508
+ name,
509
+ value
510
+ }];
511
+ return;
512
+ }
513
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
514
+ appendLegacyRouteHeader(valueOrTarget, contextOrPropertyKey, name, value);
515
+ }
321
516
  };
322
517
  return decorator;
323
518
  }
@@ -330,11 +525,22 @@ export function Header(name, value) {
330
525
  * @returns A method decorator that writes redirect metadata for the route.
331
526
  */
332
527
  export function Redirect(url, statusCode) {
333
- const decorator = (_target, context) => {
334
- getStandardRouteRecord(context.metadata, context.name).redirect = {
335
- url,
336
- statusCode
337
- };
528
+ const decorator = (valueOrTarget, contextOrPropertyKey) => {
529
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
530
+ getStandardRouteRecord(contextOrPropertyKey.metadata, contextOrPropertyKey.name).redirect = {
531
+ url,
532
+ statusCode
533
+ };
534
+ return;
535
+ }
536
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
537
+ mergeLegacyRouteMetadata(valueOrTarget, contextOrPropertyKey, {
538
+ redirect: {
539
+ url,
540
+ statusCode
541
+ }
542
+ });
543
+ }
338
544
  };
339
545
  return decorator;
340
546
  }
@@ -346,14 +552,24 @@ export function Redirect(url, statusCode) {
346
552
  * @returns A decorator applicable to classes and methods.
347
553
  */
348
554
  export function UseGuards(...guards) {
349
- const decorator = (_target, context) => {
350
- if (context.kind === 'class') {
351
- const controller = getStandardControllerRecord(context.metadata);
352
- controller.guards = mergeUnique(controller.guards, guards);
555
+ const decorator = (target, contextOrPropertyKey) => {
556
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
557
+ if (contextOrPropertyKey.kind === 'class') {
558
+ const controller = getStandardControllerRecord(contextOrPropertyKey.metadata);
559
+ controller.guards = mergeUnique(controller.guards, guards);
560
+ return;
561
+ }
562
+ const route = getStandardRouteRecord(contextOrPropertyKey.metadata, contextOrPropertyKey.name);
563
+ route.guards = mergeUnique(route.guards, guards);
564
+ return;
565
+ }
566
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
567
+ appendLegacyRouteGuards(target, contextOrPropertyKey, guards);
353
568
  return;
354
569
  }
355
- const route = getStandardRouteRecord(context.metadata, context.name);
356
- route.guards = mergeUnique(route.guards, guards);
570
+ if (typeof target === 'function') {
571
+ appendLegacyControllerGuards(target, guards);
572
+ }
357
573
  };
358
574
  return decorator;
359
575
  }
@@ -365,14 +581,24 @@ export function UseGuards(...guards) {
365
581
  * @returns A decorator applicable to classes and methods.
366
582
  */
367
583
  export function UseInterceptors(...interceptors) {
368
- const decorator = (_target, context) => {
369
- if (context.kind === 'class') {
370
- const controller = getStandardControllerRecord(context.metadata);
371
- controller.interceptors = mergeUnique(controller.interceptors, interceptors);
584
+ const decorator = (target, contextOrPropertyKey) => {
585
+ if (isStandardDecoratorContext(contextOrPropertyKey)) {
586
+ if (contextOrPropertyKey.kind === 'class') {
587
+ const controller = getStandardControllerRecord(contextOrPropertyKey.metadata);
588
+ controller.interceptors = mergeUnique(controller.interceptors, interceptors);
589
+ return;
590
+ }
591
+ const route = getStandardRouteRecord(contextOrPropertyKey.metadata, contextOrPropertyKey.name);
592
+ route.interceptors = mergeUnique(route.interceptors, interceptors);
593
+ return;
594
+ }
595
+ if (isMetadataPropertyKey(contextOrPropertyKey)) {
596
+ appendLegacyRouteInterceptors(target, contextOrPropertyKey, interceptors);
372
597
  return;
373
598
  }
374
- const route = getStandardRouteRecord(context.metadata, context.name);
375
- route.interceptors = mergeUnique(route.interceptors, interceptors);
599
+ if (typeof target === 'function') {
600
+ appendLegacyControllerInterceptors(target, interceptors);
601
+ }
376
602
  };
377
603
  return decorator;
378
604
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAC;AAQnE,OAAO,KAAK,EACV,MAAM,EACN,yBAAyB,EACzB,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAIjB,cAAc,EAEd,eAAe,EAEf,cAAc,EAId,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAKL,KAAK,aAAa,EAOnB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAE5F,gEAAgE;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpK,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,sDAAsD;IACtD,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,+DAA+D;IAC/D,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qCAAqC;IACrC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,sEAAsE;IACtE,YAAY,CAAC,EAAE;QACb,wDAAwD;QACxD,oBAAoB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;KACjD,CAAC;IACF,qDAAqD;IACrD,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,SAAS,CAAC;IACzB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAoyBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CAuF7E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,UAAU,GAAG,aAAa,GAAG,SAAS,CAE5F;AAED,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAC;AAQnE,OAAO,KAAK,EACV,MAAM,EACN,yBAAyB,EACzB,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAKjB,cAAc,EAEd,eAAe,EAEf,cAAc,EAId,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAKL,KAAK,aAAa,EAOnB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAE5F,gEAAgE;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpK,uDAAuD;AACvD,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,sDAAsD;IACtD,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,+DAA+D;IAC/D,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qCAAqC;IACrC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,sEAAsE;IACtE,YAAY,CAAC,EAAE;QACb,wDAAwD;QACxD,oBAAoB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;KACjD,CAAC;IACF,qDAAqD;IACrD,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,SAAS,CAAC;IACzB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAy8BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CAuF7E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,UAAU,GAAG,aAAa,GAAG,SAAS,CAE5F;AAED,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { getCompiledDtoBindingPlan } from '../adapters/dto-binding-plan.js';
2
2
  import { createRequestContext, runWithRequestContext } from '../context/request-context.js';
3
- import { SseResponse } from '../context/sse.js';
3
+ import { SseResponse, isSseMessage } from '../context/sse.js';
4
4
  import { RequestAbortedError } from '../errors.js';
5
5
  import { runGuardChain } from '../guards.js';
6
6
  import { runInterceptorChain } from '../interceptors.js';
@@ -255,6 +255,140 @@ function ensureRequestNotAborted(request) {
255
255
  function isRequestAborted(request) {
256
256
  return request.isAborted?.() ?? request.signal?.aborted === true;
257
257
  }
258
+ function isSseRoute(handler) {
259
+ return handler.route.produces?.some(mediaType => mediaType.toLowerCase().startsWith('text/event-stream')) === true;
260
+ }
261
+ function isAsyncIterable(value) {
262
+ return typeof value === 'object' && value !== null && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === 'function';
263
+ }
264
+ function createAbortPromise(request) {
265
+ if (!request.signal) {
266
+ return undefined;
267
+ }
268
+ if (request.signal.aborted) {
269
+ return {
270
+ cleanup: () => undefined,
271
+ promise: Promise.resolve('aborted')
272
+ };
273
+ }
274
+ let listener;
275
+ const promise = new Promise(resolve => {
276
+ listener = () => resolve('aborted');
277
+ request.signal?.addEventListener('abort', listener, {
278
+ once: true
279
+ });
280
+ });
281
+ return {
282
+ cleanup: () => {
283
+ if (listener) {
284
+ request.signal?.removeEventListener('abort', listener);
285
+ }
286
+ },
287
+ promise
288
+ };
289
+ }
290
+ function createStreamClosePromise(stream) {
291
+ if (stream.closed) {
292
+ return {
293
+ cleanup: () => undefined,
294
+ promise: Promise.resolve('aborted')
295
+ };
296
+ }
297
+ if (!stream.onClose) {
298
+ return undefined;
299
+ }
300
+ let cleanup;
301
+ const promise = new Promise(resolve => {
302
+ cleanup = stream.onClose?.(() => resolve('aborted')) ?? undefined;
303
+ });
304
+ return {
305
+ cleanup: () => {
306
+ cleanup?.();
307
+ },
308
+ promise
309
+ };
310
+ }
311
+ function createManagedSseStopPromise(request, stream) {
312
+ const stops = [createAbortPromise(request), createStreamClosePromise(stream)].filter(entry => entry !== undefined);
313
+ if (stops.length === 0) {
314
+ return undefined;
315
+ }
316
+ return {
317
+ cleanup: () => {
318
+ for (const stop of stops) {
319
+ stop.cleanup();
320
+ }
321
+ },
322
+ promise: Promise.race(stops.map(stop => stop.promise))
323
+ };
324
+ }
325
+ function resolveManagedSseFrame(value) {
326
+ if (isSseMessage(value)) {
327
+ const {
328
+ data,
329
+ event,
330
+ id,
331
+ retry
332
+ } = value;
333
+ return {
334
+ data,
335
+ options: {
336
+ event,
337
+ id,
338
+ retry
339
+ }
340
+ };
341
+ }
342
+ return {
343
+ data: value,
344
+ options: {}
345
+ };
346
+ }
347
+ function closeAsyncIteratorEventually(iterator) {
348
+ void iterator.return?.().catch(() => undefined);
349
+ }
350
+ async function readManagedSseNext(request, stream, iterator) {
351
+ const abort = createManagedSseStopPromise(request, stream);
352
+ if (!abort) {
353
+ return iterator.next();
354
+ }
355
+ try {
356
+ return await Promise.race([iterator.next(), abort.promise]);
357
+ } finally {
358
+ abort.cleanup();
359
+ }
360
+ }
361
+ async function writeManagedSseIterable(handler, requestContext, source) {
362
+ if (!isSseRoute(handler)) {
363
+ return false;
364
+ }
365
+ const sse = new SseResponse(requestContext);
366
+ const stream = requestContext.response.stream;
367
+ if (!stream) {
368
+ return true;
369
+ }
370
+ const iterator = source[Symbol.asyncIterator]();
371
+ try {
372
+ while (!isRequestAborted(requestContext.request)) {
373
+ const next = await readManagedSseNext(requestContext.request, stream, iterator);
374
+ if (next === 'aborted') {
375
+ closeAsyncIteratorEventually(iterator);
376
+ break;
377
+ }
378
+ if (next.done === true) {
379
+ break;
380
+ }
381
+ const frame = resolveManagedSseFrame(next.value);
382
+ const accepted = sse.send(frame.data, frame.options);
383
+ if (!accepted) {
384
+ await requestContext.response.stream?.waitForDrain?.();
385
+ }
386
+ }
387
+ } finally {
388
+ sse.close();
389
+ }
390
+ return true;
391
+ }
258
392
  function resolveFastPathHandlerRuntimeCache(handler, cache) {
259
393
  const cached = cache.get(handler);
260
394
  if (cached) {
@@ -335,7 +469,9 @@ async function dispatchMatchedHandler(handler, executionPlan, requestContext, co
335
469
  requestContext
336
470
  }, async () => invokeControllerHandler(handler, requestContext, binder, controllerContainer));
337
471
  ensureRequestNotAborted(requestContext.request);
338
- if (!(result instanceof SseResponse) && !requestContext.response.committed) {
472
+ if (isAsyncIterable(result) && (await writeManagedSseIterable(handler, requestContext, result))) {
473
+ // Managed SSE streams are already committed and closed by writeManagedSseIterable.
474
+ } else if (!(result instanceof SseResponse) && !requestContext.response.committed) {
339
475
  await writeSuccessResponse(handler, requestContext.request, requestContext.response, result, contentNegotiation);
340
476
  }
341
477
  await notifyObserversSafely(observers, requestContext, async (observer, context) => {
@@ -356,6 +492,10 @@ async function dispatchNativeFastRoute(match, request, response, options, conten
356
492
  if (!shouldUseFastPathForRequest(eligibility, request)) {
357
493
  return false;
358
494
  }
495
+ if (options.fastPathDebugHeaders === true && eligibility && !response.committed) {
496
+ const debugInfo = createPathDebugInfo(eligibility);
497
+ addPathDebugHeader(response.setHeader.bind(response), debugInfo);
498
+ }
359
499
  const dispatchRequest = request;
360
500
  const dispatchScope = createRootDispatchScope(options.rootContainer);
361
501
  let phaseContext;
@@ -6,13 +6,35 @@ interface CompiledEligibilityPlan {
6
6
  eligibility: FastPathEligibility;
7
7
  isEligible: boolean;
8
8
  }
9
+ /**
10
+ * Compiles the conservative fast-path eligibility decision for one handler.
11
+ *
12
+ * @param handler Handler descriptor being analyzed.
13
+ * @param options Dispatcher options that can introduce full-path requirements.
14
+ * @param adapter Human-readable adapter label used in observability metadata.
15
+ * @returns The compiled eligibility metadata and boolean eligibility flag.
16
+ */
9
17
  export declare function compileFastPathEligibility(handler: HandlerDescriptor, options: CreateDispatcherOptions, adapter: string): CompiledEligibilityPlan;
18
+ /**
19
+ * Reads fast-path eligibility metadata attached to a handler descriptor.
20
+ *
21
+ * @param handler Handler descriptor previously analyzed by the dispatcher.
22
+ * @returns The attached eligibility metadata, when present.
23
+ */
10
24
  export declare function getHandlerFastPathEligibility(handler: HandlerDescriptor): FastPathEligibility | undefined;
25
+ /**
26
+ * Attaches fast-path eligibility metadata to a handler descriptor.
27
+ *
28
+ * @param handler Handler descriptor to annotate.
29
+ * @param eligibility Eligibility metadata to expose through dispatcher observability.
30
+ */
11
31
  export declare function setHandlerFastPathEligibility(handler: HandlerDescriptor, eligibility: FastPathEligibility): void;
32
+ /** Options shared by fast-path executor helpers. */
12
33
  export interface FastPathExecutorOptions {
13
34
  binder?: Binder;
14
35
  rootContainer: Container;
15
36
  }
37
+ /** Result returned after attempting fast-path handler execution. */
16
38
  export interface FastPathExecutionResult {
17
39
  executed: boolean;
18
40
  result?: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"eligibility-checker.d.ts","sourceRoot":"","sources":["../../../src/dispatch/fast-path/eligibility-checker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EAElB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,KAAK,mBAAmB,EAAgC,MAAM,kBAAkB,CAAC;AAM1F,UAAU,uBAAuB;IAC/B,WAAW,EAAE,mBAAmB,CAAC;IACjC,UAAU,EAAE,OAAO,CAAC;CACrB;AA0DD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,MAAM,GACd,uBAAuB,CA6DzB;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iBAAiB,GACzB,mBAAmB,GAAG,SAAS,CAIjC;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,mBAAmB,GAC/B,IAAI,CAGN;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,SAAS,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB"}
1
+ {"version":3,"file":"eligibility-checker.d.ts","sourceRoot":"","sources":["../../../src/dispatch/fast-path/eligibility-checker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EAElB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,KAAK,mBAAmB,EAAgC,MAAM,kBAAkB,CAAC;AAM1F,UAAU,uBAAuB;IAC/B,WAAW,EAAE,mBAAmB,CAAC;IACjC,UAAU,EAAE,OAAO,CAAC;CACrB;AA0DD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,MAAM,GACd,uBAAuB,CAiEzB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iBAAiB,GACzB,mBAAmB,GAAG,SAAS,CAIjC;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,WAAW,EAAE,mBAAmB,GAC/B,IAAI,CAGN;AAED,oDAAoD;AACpD,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,SAAS,CAAC;CAC1B;AAED,oEAAoE;AACpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB"}