@velajs/vela 1.8.8 → 1.10.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 (115) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
  3. package/dist/application.d.ts +10 -0
  4. package/dist/application.js +25 -0
  5. package/dist/cache/cache.module.d.ts +5 -7
  6. package/dist/cache/cache.module.js +44 -66
  7. package/dist/cache/cache.types.d.ts +26 -0
  8. package/dist/cache/cache.types.js +1 -1
  9. package/dist/cache/index.d.ts +2 -1
  10. package/dist/cache/index.js +1 -0
  11. package/dist/cache/tiered-cache.store.d.ts +19 -0
  12. package/dist/cache/tiered-cache.store.js +40 -0
  13. package/dist/config/config.module.d.ts +6 -5
  14. package/dist/config/config.module.js +46 -49
  15. package/dist/container/container.d.ts +33 -0
  16. package/dist/container/container.js +156 -7
  17. package/dist/container/disposable.d.ts +5 -0
  18. package/dist/container/disposable.js +33 -0
  19. package/dist/container/types.d.ts +8 -0
  20. package/dist/factory/bootstrap.js +10 -0
  21. package/dist/fetch/fetch.module.d.ts +5 -6
  22. package/dist/fetch/fetch.module.js +9 -42
  23. package/dist/http/ambient.d.ts +17 -0
  24. package/dist/http/ambient.js +44 -0
  25. package/dist/http/execution-context.js +4 -1
  26. package/dist/http/index.d.ts +1 -0
  27. package/dist/http/index.js +1 -0
  28. package/dist/http/route.manager.d.ts +19 -0
  29. package/dist/http/route.manager.js +84 -0
  30. package/dist/i18n/deep-merge.d.ts +2 -0
  31. package/dist/i18n/deep-merge.js +15 -0
  32. package/dist/i18n/i18n.middleware.d.ts +15 -0
  33. package/dist/i18n/i18n.middleware.js +94 -0
  34. package/dist/i18n/i18n.module.d.ts +13 -0
  35. package/dist/i18n/i18n.module.js +61 -0
  36. package/dist/i18n/i18n.options.d.ts +30 -0
  37. package/dist/i18n/i18n.options.js +16 -0
  38. package/dist/i18n/i18n.service.d.ts +20 -0
  39. package/dist/i18n/i18n.service.js +49 -0
  40. package/dist/i18n/i18n.tokens.d.ts +6 -0
  41. package/dist/i18n/i18n.tokens.js +3 -0
  42. package/dist/i18n/i18n.types.d.ts +6 -0
  43. package/dist/i18n/i18n.types.js +1 -0
  44. package/dist/i18n/index.d.ts +10 -0
  45. package/dist/i18n/index.js +8 -0
  46. package/dist/i18n/message-loader.service.d.ts +21 -0
  47. package/dist/i18n/message-loader.service.js +90 -0
  48. package/dist/i18n/message-registry.d.ts +16 -0
  49. package/dist/i18n/message-registry.js +49 -0
  50. package/dist/index.d.ts +7 -4
  51. package/dist/index.js +6 -2
  52. package/dist/module/configurable-module.builder.d.ts +44 -0
  53. package/dist/module/configurable-module.builder.js +177 -0
  54. package/dist/module/configurable-module.types.d.ts +93 -0
  55. package/dist/module/configurable-module.types.js +5 -0
  56. package/dist/module/index.d.ts +2 -0
  57. package/dist/module/index.js +1 -0
  58. package/dist/openapi/document.js +20 -2
  59. package/dist/openapi/types.d.ts +54 -0
  60. package/dist/pipeline/types.d.ts +21 -4
  61. package/dist/registry/metadata.registry.d.ts +16 -16
  62. package/dist/registry/metadata.registry.js +90 -32
  63. package/dist/seeder/index.d.ts +5 -0
  64. package/dist/seeder/index.js +4 -0
  65. package/dist/seeder/seeder.decorator.d.ts +7 -0
  66. package/dist/seeder/seeder.decorator.js +15 -0
  67. package/dist/seeder/seeder.module.d.ts +18 -0
  68. package/dist/seeder/seeder.module.js +40 -0
  69. package/dist/seeder/seeder.registry.d.ts +23 -0
  70. package/dist/seeder/seeder.registry.js +83 -0
  71. package/dist/seeder/seeder.tokens.d.ts +2 -0
  72. package/dist/seeder/seeder.tokens.js +1 -0
  73. package/dist/seeder/seeder.types.d.ts +20 -0
  74. package/dist/seeder/seeder.types.js +1 -0
  75. package/dist/storage/index.d.ts +4 -0
  76. package/dist/storage/index.js +2 -0
  77. package/dist/storage/path-template.d.ts +9 -0
  78. package/dist/storage/path-template.js +20 -0
  79. package/dist/storage/signed-url.d.ts +12 -0
  80. package/dist/storage/signed-url.js +55 -0
  81. package/dist/storage/storage.types.d.ts +41 -0
  82. package/dist/storage/storage.types.js +5 -0
  83. package/dist/throttler/throttler.module.d.ts +5 -8
  84. package/dist/throttler/throttler.module.js +27 -55
  85. package/dist/websocket/index.d.ts +12 -0
  86. package/dist/websocket/index.js +16 -0
  87. package/dist/websocket/websocket.decorators.d.ts +33 -0
  88. package/dist/websocket/websocket.decorators.js +58 -0
  89. package/dist/websocket/websocket.module.d.ts +18 -0
  90. package/dist/websocket/websocket.module.js +52 -0
  91. package/dist/websocket/websocket.tokens.d.ts +22 -0
  92. package/dist/websocket/websocket.tokens.js +20 -0
  93. package/dist/websocket/websocket.types.d.ts +86 -0
  94. package/dist/websocket/websocket.types.js +4 -0
  95. package/dist/websocket/ws-argument-resolver.d.ts +13 -0
  96. package/dist/websocket/ws-argument-resolver.js +40 -0
  97. package/dist/websocket/ws-dispatcher.d.ts +33 -0
  98. package/dist/websocket/ws-dispatcher.js +253 -0
  99. package/dist/websocket/ws-exception.d.ts +16 -0
  100. package/dist/websocket/ws-exception.js +32 -0
  101. package/dist/websocket/ws-execution-context.d.ts +9 -0
  102. package/dist/websocket/ws-execution-context.js +24 -0
  103. package/dist/websocket/ws-server.d.ts +31 -0
  104. package/dist/websocket/ws-server.js +74 -0
  105. package/dist/websocket/ws-sync.d.ts +47 -0
  106. package/dist/websocket/ws-sync.js +74 -0
  107. package/dist/websocket-node/index.d.ts +4 -0
  108. package/dist/websocket-node/index.js +5 -0
  109. package/dist/websocket-node/node-ws-client.d.ts +24 -0
  110. package/dist/websocket-node/node-ws-client.js +49 -0
  111. package/dist/websocket-node/redis-sync.d.ts +33 -0
  112. package/dist/websocket-node/redis-sync.js +56 -0
  113. package/dist/websocket-node/register-gateways.d.ts +20 -0
  114. package/dist/websocket-node/register-gateways.js +61 -0
  115. package/package.json +31 -1
@@ -1,4 +1,5 @@
1
1
  import { Scope } from "../constants.js";
2
+ import { disposeInstance, isDisposable } from "./disposable.js";
2
3
  import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
3
4
  import { ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
4
5
  const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips type-only imports at ' + 'runtime and `design:paramtypes` emits `Object`/`undefined` for their ' + 'positions. Use a runtime `import { X }` for DI tokens.';
@@ -25,6 +26,13 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
25
26
  scopes = new Map();
26
27
  globals = new Set();
27
28
  diagnostics;
29
+ // The root container that owns shared state; a request child points back here
30
+ // so container-constructed SINGLETONs are tracked (and disposed) at the root,
31
+ // never by the ephemeral child that happened to first resolve them.
32
+ root = this;
33
+ // Container-constructed instances in creation order, for LIFO disposal.
34
+ // useValue providers are never tracked (they return before construction).
35
+ disposables = [];
28
36
  constructor(options = {}){
29
37
  this.diagnostics = options.diagnostics ?? 'log';
30
38
  }
@@ -285,7 +293,7 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
285
293
  if (!exporters) return undefined;
286
294
  for (const owner of exporters){
287
295
  const reg = this.providers.get(owner)?.get(token);
288
- if (reg) return reg.scope;
296
+ if (reg) return reg.effectiveScope ?? reg.scope;
289
297
  }
290
298
  return undefined;
291
299
  }
@@ -299,6 +307,95 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
299
307
  ];
300
308
  }
301
309
  /**
310
+ * Every statically-provided (useValue) instance across ALL module buckets.
311
+ * Unlike resolving a token (which yields only the first bucket's registration),
312
+ * this surfaces per-instance values — e.g. one BindingRef per multi-instance
313
+ * binding module — so framework adapters can initialize all of them.
314
+ */ getUseValues() {
315
+ const out = [];
316
+ for (const bucket of this.providers.values()){
317
+ for (const reg of bucket.values()){
318
+ if (reg.useValue !== undefined) out.push(reg.useValue);
319
+ }
320
+ }
321
+ return out;
322
+ }
323
+ /**
324
+ * Compute request-scope bubbling for every registration (call once at
325
+ * bootstrap, after all providers are registered). A provider whose declared
326
+ * scope is not REQUEST but which (transitively) depends on a request-scoped
327
+ * provider is marked `effectiveScope = REQUEST`, so it is rebuilt per request
328
+ * instead of capturing the first request's instance — matching NestJS.
329
+ */ computeEffectiveScopes() {
330
+ // Reverse-propagation worklist: seed with every declared-REQUEST provider,
331
+ // then flood "request-ness" to consumers transitively. This is order- and
332
+ // cycle-independent (a node flips to REQUEST at most once), avoiding the
333
+ // finalize-during-in-progress-cycle hazard of a recursive DFS.
334
+ const regs = [];
335
+ for (const bucket of this.providers.values()){
336
+ for (const reg of bucket.values()){
337
+ reg.effectiveScope = reg.scope;
338
+ regs.push(reg);
339
+ }
340
+ }
341
+ // Build reverse edges: dependency registration -> registrations that consume it.
342
+ const consumers = new Map();
343
+ for (const reg of regs){
344
+ for (const depToken of this.dependencyTokensOf(reg)){
345
+ const depReg = this.tryFindRegistration(depToken, reg.declaringModuleId);
346
+ if (!depReg) continue;
347
+ const list = consumers.get(depReg);
348
+ if (list) list.push(reg);
349
+ else consumers.set(depReg, [
350
+ reg
351
+ ]);
352
+ }
353
+ }
354
+ const queue = regs.filter((r)=>r.effectiveScope === Scope.REQUEST);
355
+ while(queue.length > 0){
356
+ const dep = queue.pop();
357
+ for (const consumer of consumers.get(dep) ?? []){
358
+ if (consumer.effectiveScope !== Scope.REQUEST) {
359
+ consumer.effectiveScope = Scope.REQUEST;
360
+ queue.push(consumer);
361
+ }
362
+ }
363
+ }
364
+ }
365
+ /** The dependency tokens a registration would resolve when constructed. */ dependencyTokensOf(reg) {
366
+ if (reg.useExisting) return [
367
+ reg.useExisting
368
+ ];
369
+ if (reg.useFactory) {
370
+ return (reg.inject ?? []).map((t)=>t instanceof ForwardRef ? t.factory() : t);
371
+ }
372
+ const cls = reg.useClass ?? (typeof reg.provide === 'function' ? reg.provide : undefined);
373
+ if (!cls) return [];
374
+ const paramTypes = getConstructorDependencies(cls);
375
+ const injectMetadata = getInjectMetadata(cls);
376
+ const injectMap = new Map(injectMetadata.map((m)=>[
377
+ m.index,
378
+ m
379
+ ]));
380
+ const arity = Math.max(paramTypes.length, injectMetadata.reduce((max, m)=>Math.max(max, m.index + 1), 0));
381
+ const tokens = [];
382
+ for(let i = 0; i < arity; i++){
383
+ const raw = injectMap.get(i)?.token;
384
+ const token = raw instanceof ForwardRef ? raw.factory() : raw ?? paramTypes[i];
385
+ if (token && !isErasedTypeToken(token)) tokens.push(token);
386
+ }
387
+ return tokens;
388
+ }
389
+ tryFindRegistration(token, requestingModuleId) {
390
+ try {
391
+ return this.findRegistration(token, requestingModuleId);
392
+ } catch {
393
+ // Ambiguous (MultipleProvidersFoundError) or unreachable — treat as
394
+ // unknown for scope computation; real resolution will surface any error.
395
+ return undefined;
396
+ }
397
+ }
398
+ /**
302
399
  * Create a child container for request-scoped resolution.
303
400
  * The child shares the parent's providers but caches REQUEST-scoped
304
401
  * instances separately per child (per request).
@@ -312,6 +409,9 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
312
409
  child.exporterIndex = this.exporterIndex;
313
410
  child.scopes = this.scopes;
314
411
  child.globals = this.globals;
412
+ // SINGLETON disposal is owned by the root; the child keeps only its own
413
+ // REQUEST-scoped disposables (cleared when the request ends).
414
+ child.root = this.root;
315
415
  return child;
316
416
  }
317
417
  createDetached() {
@@ -343,6 +443,44 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
343
443
  this.requestInstances.clear();
344
444
  this.scopes.clear();
345
445
  this.globals.clear();
446
+ this.disposables = [];
447
+ }
448
+ /** Whether this container has any tracked disposables (cheap request-path check). */ hasDisposables() {
449
+ return this.disposables.length > 0;
450
+ }
451
+ /**
452
+ * Dispose container-constructed instances (Symbol.asyncDispose /
453
+ * Symbol.dispose / .dispose()) in reverse creation order (LIFO). Errors are
454
+ * logged and skipped so one bad teardown never blocks the rest.
455
+ *
456
+ * A REQUEST child disposes only its own request-scoped instances; the root
457
+ * disposes shared SINGLETONs and clears their cached instance so a dev HMR
458
+ * generation cannot leak a stale graph. Shared provider maps are left intact
459
+ * for a request child (they belong to the root).
460
+ */ async dispose() {
461
+ const pending = this.disposables;
462
+ this.disposables = [];
463
+ for(let i = pending.length - 1; i >= 0; i--){
464
+ try {
465
+ await disposeInstance(pending[i]);
466
+ } catch (error) {
467
+ if (this.diagnostics !== 'silent') {
468
+ console.error('Error disposing instance:', error);
469
+ }
470
+ }
471
+ }
472
+ this.requestInstances.clear();
473
+ if (this.root === this) {
474
+ // Drop cached singleton instances (except useValue, which the app owns)
475
+ // so a rebuilt graph starts clean.
476
+ for (const bucket of this.providers.values()){
477
+ for (const registration of bucket.values()){
478
+ if (registration.scope === Scope.SINGLETON && registration.useValue === undefined) {
479
+ registration.instance = undefined;
480
+ }
481
+ }
482
+ }
483
+ }
346
484
  }
347
485
  resolveRegistration(registration, requestingModuleId) {
348
486
  if (registration.useValue !== undefined) {
@@ -352,12 +490,16 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
352
490
  // Pass through the original requester to catch alias leaks
353
491
  return this.resolve(registration.useExisting, requestingModuleId);
354
492
  }
493
+ // Effective scope accounts for request-scope bubbling: a SINGLETON that
494
+ // (transitively) depends on a request-scoped provider is treated as REQUEST
495
+ // so it is rebuilt per request instead of capturing the first one.
496
+ const scope = registration.effectiveScope ?? registration.scope;
355
497
  // Singleton: return cached from registration (shared across all containers)
356
- if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
498
+ if (scope === Scope.SINGLETON && registration.instance !== undefined) {
357
499
  return registration.instance;
358
500
  }
359
501
  // Request: return cached from this child's requestInstances
360
- if (registration.scope === Scope.REQUEST) {
502
+ if (scope === Scope.REQUEST) {
361
503
  const cached = this.requestInstances.get(registration.provide);
362
504
  if (cached !== undefined) {
363
505
  return cached;
@@ -384,10 +526,14 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
384
526
  } else {
385
527
  throw new Error(`Invalid provider registration for: ${this.tokenToString(registration.provide)}`);
386
528
  }
387
- if (registration.scope === Scope.SINGLETON) {
529
+ if (scope === Scope.SINGLETON) {
388
530
  registration.instance = instance;
389
- } else if (registration.scope === Scope.REQUEST) {
531
+ // Track for disposal on the ROOT — a singleton outlives the request
532
+ // child that may have first constructed it.
533
+ if (isDisposable(instance)) this.root.disposables.push(instance);
534
+ } else if (scope === Scope.REQUEST) {
390
535
  this.requestInstances.set(registration.provide, instance);
536
+ if (isDisposable(instance)) this.disposables.push(instance);
391
537
  }
392
538
  return instance;
393
539
  } finally{
@@ -468,7 +614,8 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
468
614
  throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
469
615
  }
470
616
  if (registration.useFactory) {
471
- if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
617
+ const scope = registration.effectiveScope ?? registration.scope;
618
+ if (scope === Scope.SINGLETON && registration.instance !== undefined) {
472
619
  return registration.instance;
473
620
  }
474
621
  // Factory inject deps resolve without a requester (same escape hatch as
@@ -478,8 +625,10 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
478
625
  return this.resolveAsync(resolved);
479
626
  }));
480
627
  const instance = await registration.useFactory(...dependencies);
481
- if (registration.scope === Scope.SINGLETON) {
628
+ if (scope === Scope.SINGLETON) {
482
629
  registration.instance = instance;
630
+ // Track for disposal on the ROOT, matching the sync resolveRegistration path.
631
+ if (isDisposable(instance)) this.root.disposables.push(instance);
483
632
  }
484
633
  return instance;
485
634
  }
@@ -0,0 +1,5 @@
1
+ export interface Disposable {
2
+ dispose?(): void | Promise<void>;
3
+ }
4
+ export declare function isDisposable(value: unknown): boolean;
5
+ export declare function disposeInstance(value: unknown): Promise<void>;
@@ -0,0 +1,33 @@
1
+ // Edge-safe disposal contract (mirrors stratal's). Precedence:
2
+ // Symbol.asyncDispose > Symbol.dispose > .dispose()
3
+ // Symbol.asyncDispose/Symbol.dispose are resolved at runtime (they are not in
4
+ // the ES2022 lib the project targets), so nothing here depends on the
5
+ // esnext.disposable typings.
6
+ const ASYNC_DISPOSE = Symbol.asyncDispose;
7
+ const SYNC_DISPOSE = Symbol.dispose;
8
+ function asRecord(value) {
9
+ if (value === null) return undefined;
10
+ return typeof value === 'object' || typeof value === 'function' ? value : undefined;
11
+ }
12
+ export function isDisposable(value) {
13
+ const obj = asRecord(value);
14
+ if (!obj) return false;
15
+ if (ASYNC_DISPOSE && typeof obj[ASYNC_DISPOSE] === 'function') return true;
16
+ if (SYNC_DISPOSE && typeof obj[SYNC_DISPOSE] === 'function') return true;
17
+ return typeof obj.dispose === 'function';
18
+ }
19
+ export async function disposeInstance(value) {
20
+ const obj = asRecord(value);
21
+ if (!obj) return;
22
+ if (ASYNC_DISPOSE && typeof obj[ASYNC_DISPOSE] === 'function') {
23
+ await obj[ASYNC_DISPOSE].call(obj);
24
+ return;
25
+ }
26
+ if (SYNC_DISPOSE && typeof obj[SYNC_DISPOSE] === 'function') {
27
+ obj[SYNC_DISPOSE].call(obj);
28
+ return;
29
+ }
30
+ if (typeof obj.dispose === 'function') {
31
+ await obj.dispose.call(obj);
32
+ }
33
+ }
@@ -57,7 +57,15 @@ export interface ProviderOptions<T = unknown> {
57
57
  }
58
58
  export interface ProviderRegistration<T = unknown> {
59
59
  provide: Token<T>;
60
+ /** The declared scope (from `@Injectable`/`@Controller`/provider options). */
60
61
  scope: Scope;
62
+ /**
63
+ * Scope after request-scope bubbling: `REQUEST` if this provider (transitively)
64
+ * depends on a request-scoped provider, else the declared scope. Computed once
65
+ * at bootstrap (`Container.computeEffectiveScopes`); falls back to `scope`
66
+ * until then. Governs caching + eager instantiation.
67
+ */
68
+ effectiveScope?: Scope;
61
69
  /**
62
70
  * Module that owns this registration. Used by `resolveClass` to determine
63
71
  * the POV from which the class's dependencies resolve. Sandbox/bootstrap
@@ -54,6 +54,13 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
54
54
  });
55
55
  container.markGlobalToken(REQUEST_CONTEXT);
56
56
  const routeManager = new RouteManager(container, options);
57
+ // Resolvable so non-HTTP transports (the WebSocket dispatcher) can read the
58
+ // same global-tier components (APP_* + app.useGlobalX()).
59
+ container.register({
60
+ provide: RouteManager,
61
+ useValue: routeManager
62
+ });
63
+ container.markGlobalToken(RouteManager);
57
64
  ComponentManager.init(container);
58
65
  const loader = new ModuleLoader(container, routeManager);
59
66
  loader.load(rootModule);
@@ -68,6 +75,9 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
68
75
  };
69
76
  routeManager.useGlobalMiddleware(mw);
70
77
  }
78
+ // All providers are registered — compute request-scope bubbling so effective
79
+ // scopes are known before eager instantiation and request resolution.
80
+ container.computeEffectiveScopes();
71
81
  return {
72
82
  container,
73
83
  routeManager,
@@ -1,8 +1,7 @@
1
- import type { AsyncModuleOptions, DynamicModule } from '../module/types';
2
1
  import type { HttpModuleOptions } from './fetch.types';
3
- export declare class HttpModule {
4
- static forRoot(options?: HttpModuleOptions): DynamicModule;
5
- static forRootAsync(options: AsyncModuleOptions<HttpModuleOptions> & {
6
- key?: string;
7
- }): DynamicModule;
2
+ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<HttpModuleOptions, "forRoot", "create", {
3
+ isGlobal?: boolean;
4
+ }>;
5
+ export declare class HttpModule extends ConfigurableModuleClass {
8
6
  }
7
+ export {};
@@ -5,52 +5,18 @@ function _ts_decorate(decorators, target, key, desc) {
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
7
  import { Module } from "../module/decorators.js";
8
- import { stableHash } from "../module/stable-hash.js";
8
+ import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
9
9
  import { HttpService, HTTP_MODULE_OPTIONS } from "./fetch.service.js";
10
- export class HttpModule {
11
- static forRoot(options = {}) {
12
- return {
13
- module: HttpModule,
14
- key: stableHash(options),
15
- providers: [
16
- {
17
- provide: HTTP_MODULE_OPTIONS,
18
- useValue: options
19
- },
20
- HttpService
21
- ],
22
- exports: [
23
- HttpService,
24
- HTTP_MODULE_OPTIONS
25
- ]
26
- };
27
- }
28
- static forRootAsync(options) {
29
- return {
30
- module: HttpModule,
31
- key: options.key ?? stableHash({
32
- inject: options.inject,
33
- useFactory: options.useFactory
34
- }),
35
- imports: options.imports ?? [],
36
- providers: [
37
- {
38
- provide: HTTP_MODULE_OPTIONS,
39
- useFactory: options.useFactory,
40
- inject: options.inject ?? []
41
- },
42
- HttpService
43
- ],
44
- exports: [
45
- HttpService,
46
- HTTP_MODULE_OPTIONS
47
- ]
48
- };
49
- }
10
+ const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
11
+ moduleName: 'Http',
12
+ optionsInjectionToken: HTTP_MODULE_OPTIONS
13
+ }).build();
14
+ export class HttpModule extends ConfigurableModuleClass {
50
15
  }
51
16
  HttpModule = _ts_decorate([
52
17
  Module({
53
18
  providers: [
19
+ // Default for bare `imports: [HttpModule]`; forRoot(options) overrides via merge.
54
20
  {
55
21
  provide: HTTP_MODULE_OPTIONS,
56
22
  useValue: {}
@@ -58,7 +24,8 @@ HttpModule = _ts_decorate([
58
24
  HttpService
59
25
  ],
60
26
  exports: [
61
- HttpService
27
+ HttpService,
28
+ HTTP_MODULE_OPTIONS
62
29
  ]
63
30
  })
64
31
  ], HttpModule);
@@ -0,0 +1,17 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { Container } from '../container/container';
3
+ import { type RequestContext } from './request-context';
4
+ /**
5
+ * Returns Hono's `contextStorage()` middleware. Register it as the FIRST global
6
+ * middleware so `getCurrentContainer()` works everywhere downstream. When using
7
+ * `VelaFactory.create(module, { ambientContainer: true })` this is wired
8
+ * automatically — call this only for manual/raw Hono setups.
9
+ */
10
+ export declare function enableAmbientContainer(): MiddlewareHandler;
11
+ /**
12
+ * The current request's DI container. Throws with actionable guidance if
13
+ * ambient access isn't enabled or this runs outside a request.
14
+ */
15
+ export declare function getCurrentContainer(): Container;
16
+ /** The current request's {@link RequestContext} (via the ambient container). */
17
+ export declare function getCurrentRequestContext(): RequestContext;
@@ -0,0 +1,44 @@
1
+ import { contextStorage, getContext } from "hono/context-storage";
2
+ import { REQUEST_CONTEXT } from "./request-context.js";
3
+ // Opt-in ambient access to the per-request DI container / RequestContext,
4
+ // for deep code that lacks the Hono `Context` (services, standalone helpers).
5
+ //
6
+ // Portable across vela's target runtimes because the `node:async_hooks`
7
+ // dependency lives inside Hono's `contextStorage()` middleware — vela's own
8
+ // source never imports `node:*`. Uses only the workerd-safe ALS subset
9
+ // (`run()`/`getStore()`); no `enterWith`, no cross-thenable propagation.
10
+ //
11
+ // Default path is unchanged: request scope is still carried by the per-request
12
+ // child container (see RouteManager.getRequestContainer). This is a second
13
+ // READ path, active only when enabled.
14
+ /**
15
+ * Returns Hono's `contextStorage()` middleware. Register it as the FIRST global
16
+ * middleware so `getCurrentContainer()` works everywhere downstream. When using
17
+ * `VelaFactory.create(module, { ambientContainer: true })` this is wired
18
+ * automatically — call this only for manual/raw Hono setups.
19
+ */ export function enableAmbientContainer() {
20
+ return contextStorage();
21
+ }
22
+ function tryGetContext() {
23
+ try {
24
+ return getContext();
25
+ } catch {
26
+ // Thrown when called outside the contextStorage() middleware (i.e. ambient
27
+ // access not enabled, or outside a request).
28
+ return undefined;
29
+ }
30
+ }
31
+ /**
32
+ * The current request's DI container. Throws with actionable guidance if
33
+ * ambient access isn't enabled or this runs outside a request.
34
+ */ export function getCurrentContainer() {
35
+ const context = tryGetContext();
36
+ const container = context?.get('container');
37
+ if (!container) {
38
+ throw new Error('getCurrentContainer() is unavailable: enable ambient access via ' + "VelaFactory.create(module, { ambientContainer: true }) (or app.use(enableAmbientContainer())) " + 'and only call it within a request.');
39
+ }
40
+ return container;
41
+ }
42
+ /** The current request's {@link RequestContext} (via the ambient container). */ export function getCurrentRequestContext() {
43
+ return getCurrentContainer().resolve(REQUEST_CONTEXT);
44
+ }
@@ -10,7 +10,10 @@ export function buildExecutionContext(c, controller, handlerName) {
10
10
  switchToHttp: ()=>({
11
11
  getRequest: ()=>c.req.raw,
12
12
  getResponse: ()=>c
13
- })
13
+ }),
14
+ switchToWs: ()=>{
15
+ throw new Error('switchToWs() called on an HTTP ExecutionContext. This handler runs over HTTP, not a WebSocket gateway.');
16
+ }
14
17
  };
15
18
  }
16
19
  // Sentinel marker exposed via `ExecutionContext.getClass()` when the host is a
@@ -4,4 +4,5 @@ export { MiddlewareBuilder } from '../module/middleware';
4
4
  export type { MiddlewareConsumer, MiddlewareConfigProxy, RouteInfo, NestModule, MiddlewareRouteDefinition } from '../module/middleware';
5
5
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, isController, } from './decorators';
6
6
  export { createLazyParamDecorator } from './lazy-param.decorator';
7
+ export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext, } from './ambient';
7
8
  export type { RouteMetadata, ControllerMetadata, ControllerOptions, ParamMetadata, ControllerRegistration, } from './types';
@@ -2,3 +2,4 @@ export { RouteManager } from "./route.manager.js";
2
2
  export { MiddlewareBuilder } from "../module/middleware.js";
3
3
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, isController } from "./decorators.js";
4
4
  export { createLazyParamDecorator } from "./lazy-param.decorator.js";
5
+ export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext } from "./ambient.js";
@@ -9,6 +9,12 @@ export interface RouteManagerOptions {
9
9
  getClientIp?: (c: Context) => string | null;
10
10
  middleware?: MiddlewareHandler[];
11
11
  globalPrefix?: string;
12
+ /**
13
+ * Opt into ambient request-container access (`getCurrentContainer()` /
14
+ * `getCurrentRequestContext()`). Registers Hono's `contextStorage()` as the
15
+ * first middleware. Off by default — the explicit child container is unchanged.
16
+ */
17
+ ambientContainer?: boolean;
12
18
  }
13
19
  export declare class RouteManager {
14
20
  private container;
@@ -22,7 +28,20 @@ export declare class RouteManager {
22
28
  private globalPrefix;
23
29
  private consumerMiddlewareDefinitions;
24
30
  private readonly handlerExecutor;
31
+ private readonly ambientContainer;
25
32
  constructor(container: Container, options?: RouteManagerOptions);
33
+ /**
34
+ * The global-tier components (`APP_*` provider tokens + imperative
35
+ * `app.useGlobalX()` instances). Exposed so non-HTTP transports (the
36
+ * WebSocket dispatcher) can apply the same app-wide guards/pipes/interceptors/
37
+ * filters as HTTP routes, read live so late registrations propagate.
38
+ */
39
+ getGlobalComponents(): {
40
+ guards: Array<GuardType | Token<CanActivate>>;
41
+ pipes: Array<PipeType | Token<PipeTransform>>;
42
+ interceptors: Array<InterceptorType | Token<NestInterceptor>>;
43
+ filters: Array<FilterType | Token<ExceptionFilter>>;
44
+ };
26
45
  registerConsumerMiddleware(definitions: MiddlewareRouteDefinition[]): this;
27
46
  setGlobalPrefix(prefix: string): this;
28
47
  useGlobalMiddleware(...middleware: MiddlewareType[]): this;
@@ -1,4 +1,5 @@
1
1
  import { Hono } from "hono";
2
+ import { contextStorage } from "hono/context-storage";
2
3
  import { HttpMethod } from "../constants.js";
3
4
  import { HttpException } from "../errors/http-exception.js";
4
5
  import { getMetadata } from "../metadata.js";
@@ -14,6 +15,39 @@ import { ComponentManager } from "../pipeline/component.manager.js";
14
15
  import { shouldFilterCatch } from "../pipeline/decorators.js";
15
16
  import { MetadataRegistry } from "../registry/metadata.registry.js";
16
17
  const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
18
+ // Mirror a response body stream, invoking `onDone` exactly once when it is fully
19
+ // read, errors, or is cancelled. Lets request-scoped resources be disposed only
20
+ // after the body has drained — never mid-stream.
21
+ function disposeStreamWhenDone(body, onDone) {
22
+ const reader = body.getReader();
23
+ let finished = false;
24
+ const finish = ()=>{
25
+ if (!finished) {
26
+ finished = true;
27
+ onDone();
28
+ }
29
+ };
30
+ return new ReadableStream({
31
+ async pull (controller) {
32
+ try {
33
+ const { done, value } = await reader.read();
34
+ if (done) {
35
+ controller.close();
36
+ finish();
37
+ return;
38
+ }
39
+ controller.enqueue(value);
40
+ } catch (error) {
41
+ controller.error(error);
42
+ finish();
43
+ }
44
+ },
45
+ cancel (reason) {
46
+ finish();
47
+ return reader.cancel(reason);
48
+ }
49
+ });
50
+ }
17
51
  export class RouteManager {
18
52
  container;
19
53
  static METHOD_REGISTRAR = new Map([
@@ -59,8 +93,10 @@ export class RouteManager {
59
93
  globalPrefix = '';
60
94
  consumerMiddlewareDefinitions = [];
61
95
  handlerExecutor;
96
+ ambientContainer;
62
97
  constructor(container, options = {}){
63
98
  this.container = container;
99
+ this.ambientContainer = options.ambientContainer ?? false;
64
100
  const argumentResolver = new ArgumentResolver(options.getClientIp ?? defaultGetClientIp);
65
101
  this.handlerExecutor = new HandlerExecutor(argumentResolver, ()=>({
66
102
  guards: this.globalGuards,
@@ -69,6 +105,19 @@ export class RouteManager {
69
105
  filters: this.globalFilters
70
106
  }), (c)=>this.getRequestContainer(c));
71
107
  }
108
+ /**
109
+ * The global-tier components (`APP_*` provider tokens + imperative
110
+ * `app.useGlobalX()` instances). Exposed so non-HTTP transports (the
111
+ * WebSocket dispatcher) can apply the same app-wide guards/pipes/interceptors/
112
+ * filters as HTTP routes, read live so late registrations propagate.
113
+ */ getGlobalComponents() {
114
+ return {
115
+ guards: this.globalGuards,
116
+ pipes: this.globalPipes,
117
+ interceptors: this.globalInterceptors,
118
+ filters: this.globalFilters
119
+ };
120
+ }
72
121
  registerConsumerMiddleware(definitions) {
73
122
  this.consumerMiddlewareDefinitions.push(...definitions);
74
123
  return this;
@@ -221,6 +270,41 @@ export class RouteManager {
221
270
  }
222
271
  async build() {
223
272
  const app = new Hono();
273
+ // Outermost: dispose the per-request child container once the request is
274
+ // fully done. Fast-paths out when the child has no request-scoped
275
+ // disposables (the common case → zero overhead / no behavior change).
276
+ // Streaming-safe: when a response body is present, disposal is deferred
277
+ // until the body is fully read (or errors/cancels), never mid-stream.
278
+ app.use('*', async (c, next)=>{
279
+ let threw = false;
280
+ try {
281
+ await next();
282
+ } catch (error) {
283
+ threw = true;
284
+ throw error;
285
+ } finally{
286
+ const child = c.get('container');
287
+ if (child?.hasDisposables()) {
288
+ const body = threw ? null : c.res?.body ?? null;
289
+ if (body) {
290
+ // Defer disposal to when the runtime finishes reading the body.
291
+ c.res = new Response(disposeStreamWhenDone(body, ()=>void child.dispose()), {
292
+ status: c.res.status,
293
+ statusText: c.res.statusText,
294
+ headers: c.res.headers
295
+ });
296
+ } else {
297
+ // No body (or error path) — nothing streaming, dispose now.
298
+ await child.dispose();
299
+ }
300
+ }
301
+ }
302
+ });
303
+ // Opt-in ambient container: contextStorage() must be an early middleware so
304
+ // getContext() is available to everything downstream.
305
+ if (this.ambientContainer) {
306
+ app.use('*', contextStorage());
307
+ }
224
308
  // Sort global middleware by priority (lower runs first) with insertion
225
309
  // index as tiebreaker so equal priorities preserve registration order.
226
310
  const sortedGlobal = this.globalMiddleware.map((entry, index)=>({
@@ -0,0 +1,2 @@
1
+ /** Deep-merge two message trees; source overrides target at the leaf level. */
2
+ export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,15 @@
1
+ /** Deep-merge two message trees; source overrides target at the leaf level. */ export function deepMerge(target, source) {
2
+ const result = {
3
+ ...target
4
+ };
5
+ for (const key of Object.keys(source)){
6
+ const targetValue = target[key];
7
+ const sourceValue = source[key];
8
+ if (typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue) && typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)) {
9
+ result[key] = deepMerge(targetValue, sourceValue);
10
+ } else {
11
+ result[key] = sourceValue;
12
+ }
13
+ }
14
+ return result;
15
+ }