@velajs/vela 1.9.0 → 1.12.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 (161) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
  3. package/dist/application.d.ts +18 -0
  4. package/dist/application.js +45 -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 +58 -0
  16. package/dist/container/container.js +223 -18
  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/cors/cors.module.d.ts +5 -5
  21. package/dist/cors/cors.module.js +26 -25
  22. package/dist/discovery/discoverable.decorator.d.ts +38 -0
  23. package/dist/discovery/discoverable.decorator.js +42 -0
  24. package/dist/discovery/discovery.service.d.ts +94 -0
  25. package/dist/discovery/discovery.service.js +166 -0
  26. package/dist/discovery/index.d.ts +2 -0
  27. package/dist/discovery/index.js +2 -0
  28. package/dist/entrypoint/entrypoint.registry.d.ts +40 -0
  29. package/dist/entrypoint/entrypoint.registry.js +106 -0
  30. package/dist/entrypoint/entrypoint.types.d.ts +41 -0
  31. package/dist/entrypoint/entrypoint.types.js +3 -0
  32. package/dist/entrypoint/execution-context.d.ts +15 -0
  33. package/dist/entrypoint/execution-context.js +21 -0
  34. package/dist/entrypoint/execution-scope.d.ts +21 -0
  35. package/dist/entrypoint/execution-scope.js +28 -0
  36. package/dist/entrypoint/index.d.ts +4 -0
  37. package/dist/entrypoint/index.js +4 -0
  38. package/dist/event-emitter/event-emitter.subscriber.d.ts +3 -3
  39. package/dist/event-emitter/event-emitter.subscriber.js +12 -29
  40. package/dist/factory/adapter.d.ts +36 -0
  41. package/dist/factory/adapter.js +18 -0
  42. package/dist/factory/bootstrap.js +20 -2
  43. package/dist/factory.d.ts +11 -1
  44. package/dist/factory.js +23 -2
  45. package/dist/fetch/fetch.module.d.ts +5 -6
  46. package/dist/fetch/fetch.module.js +9 -42
  47. package/dist/http/ambient.d.ts +17 -0
  48. package/dist/http/ambient.js +44 -0
  49. package/dist/http/execution-context.js +4 -1
  50. package/dist/http/handler-executor.js +22 -19
  51. package/dist/http/index.d.ts +1 -0
  52. package/dist/http/index.js +1 -0
  53. package/dist/http/route-contributor.d.ts +68 -0
  54. package/dist/http/route-contributor.js +22 -0
  55. package/dist/http/route.manager.d.ts +19 -0
  56. package/dist/http/route.manager.js +112 -19
  57. package/dist/i18n/deep-merge.d.ts +2 -0
  58. package/dist/i18n/deep-merge.js +15 -0
  59. package/dist/i18n/i18n.middleware.d.ts +15 -0
  60. package/dist/i18n/i18n.middleware.js +94 -0
  61. package/dist/i18n/i18n.module.d.ts +13 -0
  62. package/dist/i18n/i18n.module.js +64 -0
  63. package/dist/i18n/i18n.options.d.ts +30 -0
  64. package/dist/i18n/i18n.options.js +16 -0
  65. package/dist/i18n/i18n.service.d.ts +20 -0
  66. package/dist/i18n/i18n.service.js +49 -0
  67. package/dist/i18n/i18n.tokens.d.ts +6 -0
  68. package/dist/i18n/i18n.tokens.js +3 -0
  69. package/dist/i18n/i18n.types.d.ts +6 -0
  70. package/dist/i18n/i18n.types.js +1 -0
  71. package/dist/i18n/index.d.ts +10 -0
  72. package/dist/i18n/index.js +8 -0
  73. package/dist/i18n/message-loader.service.d.ts +21 -0
  74. package/dist/i18n/message-loader.service.js +90 -0
  75. package/dist/i18n/message-registry.d.ts +16 -0
  76. package/dist/i18n/message-registry.js +49 -0
  77. package/dist/index.d.ts +17 -5
  78. package/dist/index.js +14 -3
  79. package/dist/internal.d.ts +0 -2
  80. package/dist/internal.js +0 -7
  81. package/dist/module/configurable-module.builder.d.ts +56 -0
  82. package/dist/module/configurable-module.builder.js +84 -0
  83. package/dist/module/configurable-module.types.d.ts +93 -0
  84. package/dist/module/configurable-module.types.js +5 -0
  85. package/dist/module/define-module.d.ts +105 -0
  86. package/dist/module/define-module.js +227 -0
  87. package/dist/module/index.d.ts +4 -0
  88. package/dist/module/index.js +3 -0
  89. package/dist/module/lazy-provider.d.ts +65 -0
  90. package/dist/module/lazy-provider.js +111 -0
  91. package/dist/openapi/document.js +16 -15
  92. package/dist/pipeline/component.manager.d.ts +15 -9
  93. package/dist/pipeline/component.manager.js +32 -46
  94. package/dist/pipeline/index.d.ts +2 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline-runner.d.ts +31 -0
  97. package/dist/pipeline/pipeline-runner.js +40 -0
  98. package/dist/pipeline/types.d.ts +21 -4
  99. package/dist/registry/metadata.registry.d.ts +24 -18
  100. package/dist/registry/metadata.registry.js +137 -49
  101. package/dist/schedule/schedule.module.d.ts +5 -0
  102. package/dist/schedule/schedule.module.js +18 -8
  103. package/dist/schedule/schedule.registry.d.ts +3 -3
  104. package/dist/schedule/schedule.registry.js +23 -44
  105. package/dist/schedule-node/schedule-node.module.d.ts +5 -0
  106. package/dist/schedule-node/schedule-node.module.js +20 -10
  107. package/dist/seeder/index.d.ts +5 -0
  108. package/dist/seeder/index.js +4 -0
  109. package/dist/seeder/seeder.decorator.d.ts +7 -0
  110. package/dist/seeder/seeder.decorator.js +15 -0
  111. package/dist/seeder/seeder.module.d.ts +16 -0
  112. package/dist/seeder/seeder.module.js +41 -0
  113. package/dist/seeder/seeder.registry.d.ts +23 -0
  114. package/dist/seeder/seeder.registry.js +83 -0
  115. package/dist/seeder/seeder.tokens.d.ts +2 -0
  116. package/dist/seeder/seeder.tokens.js +1 -0
  117. package/dist/seeder/seeder.types.d.ts +20 -0
  118. package/dist/seeder/seeder.types.js +1 -0
  119. package/dist/storage/index.d.ts +4 -0
  120. package/dist/storage/index.js +2 -0
  121. package/dist/storage/path-template.d.ts +9 -0
  122. package/dist/storage/path-template.js +20 -0
  123. package/dist/storage/signed-url.d.ts +12 -0
  124. package/dist/storage/signed-url.js +55 -0
  125. package/dist/storage/storage.types.d.ts +41 -0
  126. package/dist/storage/storage.types.js +5 -0
  127. package/dist/throttler/throttler.module.d.ts +5 -8
  128. package/dist/throttler/throttler.module.js +27 -55
  129. package/dist/websocket/index.d.ts +12 -0
  130. package/dist/websocket/index.js +16 -0
  131. package/dist/websocket/websocket.decorators.d.ts +33 -0
  132. package/dist/websocket/websocket.decorators.js +58 -0
  133. package/dist/websocket/websocket.module.d.ts +35 -0
  134. package/dist/websocket/websocket.module.js +63 -0
  135. package/dist/websocket/websocket.tokens.d.ts +23 -0
  136. package/dist/websocket/websocket.tokens.js +21 -0
  137. package/dist/websocket/websocket.types.d.ts +86 -0
  138. package/dist/websocket/websocket.types.js +4 -0
  139. package/dist/websocket/ws-argument-resolver.d.ts +13 -0
  140. package/dist/websocket/ws-argument-resolver.js +40 -0
  141. package/dist/websocket/ws-dispatcher.d.ts +45 -0
  142. package/dist/websocket/ws-dispatcher.js +259 -0
  143. package/dist/websocket/ws-exception.d.ts +16 -0
  144. package/dist/websocket/ws-exception.js +32 -0
  145. package/dist/websocket/ws-execution-context.d.ts +9 -0
  146. package/dist/websocket/ws-execution-context.js +24 -0
  147. package/dist/websocket/ws-server.d.ts +31 -0
  148. package/dist/websocket/ws-server.js +74 -0
  149. package/dist/websocket/ws-sync.d.ts +47 -0
  150. package/dist/websocket/ws-sync.js +74 -0
  151. package/dist/websocket-node/index.d.ts +4 -0
  152. package/dist/websocket-node/index.js +5 -0
  153. package/dist/websocket-node/node-ws-client.d.ts +24 -0
  154. package/dist/websocket-node/node-ws-client.js +49 -0
  155. package/dist/websocket-node/redis-sync.d.ts +33 -0
  156. package/dist/websocket-node/redis-sync.js +56 -0
  157. package/dist/websocket-node/register-gateways.d.ts +20 -0
  158. package/dist/websocket-node/register-gateways.js +61 -0
  159. package/package.json +31 -1
  160. package/dist/http/crud-bridge.d.ts +0 -90
  161. package/dist/http/crud-bridge.js +0 -25
@@ -17,6 +17,8 @@ export declare class Container {
17
17
  private scopes;
18
18
  private globals;
19
19
  private diagnostics;
20
+ private root;
21
+ private disposables;
20
22
  constructor(options?: ContainerOptions);
21
23
  register<T>(provider: Type<T> | ProviderOptions<T>, declaringModuleId?: string): this;
22
24
  private registerClass;
@@ -50,8 +52,41 @@ export declare class Container {
50
52
  has(token: Token): boolean;
51
53
  /** True if the specified module's bucket has a registration for `token`. */
52
54
  hasInScope(token: Token, moduleId: string): boolean;
55
+ /** Module buckets that hold a registration for `token` (registration order). */
56
+ getOwnerModuleIds(token: Token): string[];
57
+ /**
58
+ * Force-replace a provider across module scopes — the supported form of the
59
+ * override loop test harnesses need (module-scoped resolution consults the
60
+ * module's own bucket first, so a root-only override would never win there).
61
+ *
62
+ * - `'all-existing'` (default): every non-root bucket that already holds the
63
+ * token, plus `__root__` (so framework-internal lookups see it too).
64
+ * - `'root'`: only the `__root__` bucket.
65
+ * - `string[]`: exactly these bucket ids.
66
+ */
67
+ replaceProvider<T>(provider: Type<T> | ProviderOptions<T>, options?: {
68
+ buckets?: 'all-existing' | 'root' | string[];
69
+ }): this;
53
70
  getProviderScope(token: Token): Scope | undefined;
54
71
  getTokens(): Token[];
72
+ /**
73
+ * Every statically-provided (useValue) instance across ALL module buckets.
74
+ * Unlike resolving a token (which yields only the first bucket's registration),
75
+ * this surfaces per-instance values — e.g. one BindingRef per multi-instance
76
+ * binding module — so framework adapters can initialize all of them.
77
+ */
78
+ getUseValues(): unknown[];
79
+ /**
80
+ * Compute request-scope bubbling for every registration (call once at
81
+ * bootstrap, after all providers are registered). A provider whose declared
82
+ * scope is not REQUEST but which (transitively) depends on a request-scoped
83
+ * provider is marked `effectiveScope = REQUEST`, so it is rebuilt per request
84
+ * instead of capturing the first request's instance — matching NestJS.
85
+ */
86
+ computeEffectiveScopes(): void;
87
+ /** The dependency tokens a registration would resolve when constructed. */
88
+ private dependencyTokensOf;
89
+ private tryFindRegistration;
55
90
  /**
56
91
  * Create a child container for request-scoped resolution.
57
92
  * The child shares the parent's providers but caches REQUEST-scoped
@@ -60,8 +95,31 @@ export declare class Container {
60
95
  createChild(): Container;
61
96
  createDetached(): Container;
62
97
  clear(): void;
98
+ /** Whether this container has any tracked disposables (cheap request-path check). */
99
+ hasDisposables(): boolean;
100
+ /**
101
+ * Dispose container-constructed instances (Symbol.asyncDispose /
102
+ * Symbol.dispose / .dispose()) in reverse creation order (LIFO). Errors are
103
+ * logged and skipped so one bad teardown never blocks the rest.
104
+ *
105
+ * A REQUEST child disposes only its own request-scoped instances; the root
106
+ * disposes shared SINGLETONs and clears their cached instance so a dev HMR
107
+ * generation cannot leak a stale graph. Shared provider maps are left intact
108
+ * for a request child (they belong to the root).
109
+ */
110
+ dispose(): Promise<void>;
63
111
  private resolveRegistration;
64
112
  private resolveClass;
113
+ /**
114
+ * Resolve one factory `inject` dependency. Since 1.11 the declaring module's
115
+ * scope is consulted FIRST (proper visibility semantics — a `forRootAsync`
116
+ * factory can inject providers reachable through its module's imports); on
117
+ * any failure the legacy no-requester lookup (root bucket, then first
118
+ * exporter) is kept as fallback, so every previously-resolving graph still
119
+ * resolves.
120
+ */
121
+ private resolveFactoryDependency;
122
+ private resolveFactoryDependencyAsync;
65
123
  private resolveFactory;
66
124
  resolveAsync<T>(token: Token<T>, requestingModuleId?: string): Promise<T>;
67
125
  private createLazyProxy;
@@ -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
  }
@@ -280,12 +288,45 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
280
288
  /** True if the specified module's bucket has a registration for `token`. */ hasInScope(token, moduleId) {
281
289
  return this.providers.get(moduleId)?.has(token) ?? false;
282
290
  }
291
+ /** Module buckets that hold a registration for `token` (registration order). */ getOwnerModuleIds(token) {
292
+ return [
293
+ ...this.exporterIndex.get(token) ?? []
294
+ ];
295
+ }
296
+ /**
297
+ * Force-replace a provider across module scopes — the supported form of the
298
+ * override loop test harnesses need (module-scoped resolution consults the
299
+ * module's own bucket first, so a root-only override would never win there).
300
+ *
301
+ * - `'all-existing'` (default): every non-root bucket that already holds the
302
+ * token, plus `__root__` (so framework-internal lookups see it too).
303
+ * - `'root'`: only the `__root__` bucket.
304
+ * - `string[]`: exactly these bucket ids.
305
+ */ replaceProvider(provider, options = {}) {
306
+ const buckets = options.buckets ?? 'all-existing';
307
+ if (buckets === 'root') {
308
+ return this.register(provider);
309
+ }
310
+ if (Array.isArray(buckets)) {
311
+ for (const moduleId of buckets)this.register(provider, moduleId);
312
+ return this;
313
+ }
314
+ const token = typeof provider === 'function' ? provider : provider.provide;
315
+ if (!token) {
316
+ throw new Error('replaceProvider requires a token (`provide`)');
317
+ }
318
+ for (const [moduleId, bucket] of this.providers){
319
+ if (moduleId === ROOT_MODULE_ID) continue;
320
+ if (bucket.has(token)) this.register(provider, moduleId);
321
+ }
322
+ return this.register(provider);
323
+ }
283
324
  getProviderScope(token) {
284
325
  const exporters = this.exporterIndex.get(token);
285
326
  if (!exporters) return undefined;
286
327
  for (const owner of exporters){
287
328
  const reg = this.providers.get(owner)?.get(token);
288
- if (reg) return reg.scope;
329
+ if (reg) return reg.effectiveScope ?? reg.scope;
289
330
  }
290
331
  return undefined;
291
332
  }
@@ -299,6 +340,95 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
299
340
  ];
300
341
  }
301
342
  /**
343
+ * Every statically-provided (useValue) instance across ALL module buckets.
344
+ * Unlike resolving a token (which yields only the first bucket's registration),
345
+ * this surfaces per-instance values — e.g. one BindingRef per multi-instance
346
+ * binding module — so framework adapters can initialize all of them.
347
+ */ getUseValues() {
348
+ const out = [];
349
+ for (const bucket of this.providers.values()){
350
+ for (const reg of bucket.values()){
351
+ if (reg.useValue !== undefined) out.push(reg.useValue);
352
+ }
353
+ }
354
+ return out;
355
+ }
356
+ /**
357
+ * Compute request-scope bubbling for every registration (call once at
358
+ * bootstrap, after all providers are registered). A provider whose declared
359
+ * scope is not REQUEST but which (transitively) depends on a request-scoped
360
+ * provider is marked `effectiveScope = REQUEST`, so it is rebuilt per request
361
+ * instead of capturing the first request's instance — matching NestJS.
362
+ */ computeEffectiveScopes() {
363
+ // Reverse-propagation worklist: seed with every declared-REQUEST provider,
364
+ // then flood "request-ness" to consumers transitively. This is order- and
365
+ // cycle-independent (a node flips to REQUEST at most once), avoiding the
366
+ // finalize-during-in-progress-cycle hazard of a recursive DFS.
367
+ const regs = [];
368
+ for (const bucket of this.providers.values()){
369
+ for (const reg of bucket.values()){
370
+ reg.effectiveScope = reg.scope;
371
+ regs.push(reg);
372
+ }
373
+ }
374
+ // Build reverse edges: dependency registration -> registrations that consume it.
375
+ const consumers = new Map();
376
+ for (const reg of regs){
377
+ for (const depToken of this.dependencyTokensOf(reg)){
378
+ const depReg = this.tryFindRegistration(depToken, reg.declaringModuleId);
379
+ if (!depReg) continue;
380
+ const list = consumers.get(depReg);
381
+ if (list) list.push(reg);
382
+ else consumers.set(depReg, [
383
+ reg
384
+ ]);
385
+ }
386
+ }
387
+ const queue = regs.filter((r)=>r.effectiveScope === Scope.REQUEST);
388
+ while(queue.length > 0){
389
+ const dep = queue.pop();
390
+ for (const consumer of consumers.get(dep) ?? []){
391
+ if (consumer.effectiveScope !== Scope.REQUEST) {
392
+ consumer.effectiveScope = Scope.REQUEST;
393
+ queue.push(consumer);
394
+ }
395
+ }
396
+ }
397
+ }
398
+ /** The dependency tokens a registration would resolve when constructed. */ dependencyTokensOf(reg) {
399
+ if (reg.useExisting) return [
400
+ reg.useExisting
401
+ ];
402
+ if (reg.useFactory) {
403
+ return (reg.inject ?? []).map((t)=>t instanceof ForwardRef ? t.factory() : t);
404
+ }
405
+ const cls = reg.useClass ?? (typeof reg.provide === 'function' ? reg.provide : undefined);
406
+ if (!cls) return [];
407
+ const paramTypes = getConstructorDependencies(cls);
408
+ const injectMetadata = getInjectMetadata(cls);
409
+ const injectMap = new Map(injectMetadata.map((m)=>[
410
+ m.index,
411
+ m
412
+ ]));
413
+ const arity = Math.max(paramTypes.length, injectMetadata.reduce((max, m)=>Math.max(max, m.index + 1), 0));
414
+ const tokens = [];
415
+ for(let i = 0; i < arity; i++){
416
+ const raw = injectMap.get(i)?.token;
417
+ const token = raw instanceof ForwardRef ? raw.factory() : raw ?? paramTypes[i];
418
+ if (token && !isErasedTypeToken(token)) tokens.push(token);
419
+ }
420
+ return tokens;
421
+ }
422
+ tryFindRegistration(token, requestingModuleId) {
423
+ try {
424
+ return this.findRegistration(token, requestingModuleId);
425
+ } catch {
426
+ // Ambiguous (MultipleProvidersFoundError) or unreachable — treat as
427
+ // unknown for scope computation; real resolution will surface any error.
428
+ return undefined;
429
+ }
430
+ }
431
+ /**
302
432
  * Create a child container for request-scoped resolution.
303
433
  * The child shares the parent's providers but caches REQUEST-scoped
304
434
  * instances separately per child (per request).
@@ -312,6 +442,9 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
312
442
  child.exporterIndex = this.exporterIndex;
313
443
  child.scopes = this.scopes;
314
444
  child.globals = this.globals;
445
+ // SINGLETON disposal is owned by the root; the child keeps only its own
446
+ // REQUEST-scoped disposables (cleared when the request ends).
447
+ child.root = this.root;
315
448
  return child;
316
449
  }
317
450
  createDetached() {
@@ -343,6 +476,44 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
343
476
  this.requestInstances.clear();
344
477
  this.scopes.clear();
345
478
  this.globals.clear();
479
+ this.disposables = [];
480
+ }
481
+ /** Whether this container has any tracked disposables (cheap request-path check). */ hasDisposables() {
482
+ return this.disposables.length > 0;
483
+ }
484
+ /**
485
+ * Dispose container-constructed instances (Symbol.asyncDispose /
486
+ * Symbol.dispose / .dispose()) in reverse creation order (LIFO). Errors are
487
+ * logged and skipped so one bad teardown never blocks the rest.
488
+ *
489
+ * A REQUEST child disposes only its own request-scoped instances; the root
490
+ * disposes shared SINGLETONs and clears their cached instance so a dev HMR
491
+ * generation cannot leak a stale graph. Shared provider maps are left intact
492
+ * for a request child (they belong to the root).
493
+ */ async dispose() {
494
+ const pending = this.disposables;
495
+ this.disposables = [];
496
+ for(let i = pending.length - 1; i >= 0; i--){
497
+ try {
498
+ await disposeInstance(pending[i]);
499
+ } catch (error) {
500
+ if (this.diagnostics !== 'silent') {
501
+ console.error('Error disposing instance:', error);
502
+ }
503
+ }
504
+ }
505
+ this.requestInstances.clear();
506
+ if (this.root === this) {
507
+ // Drop cached singleton instances (except useValue, which the app owns)
508
+ // so a rebuilt graph starts clean.
509
+ for (const bucket of this.providers.values()){
510
+ for (const registration of bucket.values()){
511
+ if (registration.scope === Scope.SINGLETON && registration.useValue === undefined) {
512
+ registration.instance = undefined;
513
+ }
514
+ }
515
+ }
516
+ }
346
517
  }
347
518
  resolveRegistration(registration, requestingModuleId) {
348
519
  if (registration.useValue !== undefined) {
@@ -352,12 +523,16 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
352
523
  // Pass through the original requester to catch alias leaks
353
524
  return this.resolve(registration.useExisting, requestingModuleId);
354
525
  }
526
+ // Effective scope accounts for request-scope bubbling: a SINGLETON that
527
+ // (transitively) depends on a request-scoped provider is treated as REQUEST
528
+ // so it is rebuilt per request instead of capturing the first one.
529
+ const scope = registration.effectiveScope ?? registration.scope;
355
530
  // Singleton: return cached from registration (shared across all containers)
356
- if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
531
+ if (scope === Scope.SINGLETON && registration.instance !== undefined) {
357
532
  return registration.instance;
358
533
  }
359
534
  // Request: return cached from this child's requestInstances
360
- if (registration.scope === Scope.REQUEST) {
535
+ if (scope === Scope.REQUEST) {
361
536
  const cached = this.requestInstances.get(registration.provide);
362
537
  if (cached !== undefined) {
363
538
  return cached;
@@ -374,20 +549,23 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
374
549
  try {
375
550
  let instance;
376
551
  if (registration.useFactory) {
377
- // Factories (forRootAsync, useFactory) commonly inject deps from the
378
- // importing module's scope. Vela has no `forRootAsync({ imports })`
379
- // surface to track that, so factory inject deps resolve from the
380
- // declaring module's POV — same escape-hatch shape as ModuleRef.
552
+ // Factory inject deps resolve from the declaring module's scope first
553
+ // (visibility-correct for forRootAsync), with the legacy no-requester
554
+ // lookup kept as fallback see resolveFactoryDependency.
381
555
  instance = this.resolveFactory(registration);
382
556
  } else if (registration.useClass) {
383
557
  instance = this.resolveClass(registration.useClass, registration.declaringModuleId);
384
558
  } else {
385
559
  throw new Error(`Invalid provider registration for: ${this.tokenToString(registration.provide)}`);
386
560
  }
387
- if (registration.scope === Scope.SINGLETON) {
561
+ if (scope === Scope.SINGLETON) {
388
562
  registration.instance = instance;
389
- } else if (registration.scope === Scope.REQUEST) {
563
+ // Track for disposal on the ROOT — a singleton outlives the request
564
+ // child that may have first constructed it.
565
+ if (isDisposable(instance)) this.root.disposables.push(instance);
566
+ } else if (scope === Scope.REQUEST) {
390
567
  this.requestInstances.set(registration.provide, instance);
568
+ if (isDisposable(instance)) this.disposables.push(instance);
391
569
  }
392
570
  return instance;
393
571
  } finally{
@@ -432,16 +610,40 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
432
610
  });
433
611
  return new target(...dependencies);
434
612
  }
613
+ /**
614
+ * Resolve one factory `inject` dependency. Since 1.11 the declaring module's
615
+ * scope is consulted FIRST (proper visibility semantics — a `forRootAsync`
616
+ * factory can inject providers reachable through its module's imports); on
617
+ * any failure the legacy no-requester lookup (root bucket, then first
618
+ * exporter) is kept as fallback, so every previously-resolving graph still
619
+ * resolves.
620
+ */ resolveFactoryDependency(token, declaringModuleId) {
621
+ if (declaringModuleId !== ROOT_MODULE_ID && this.scopes.has(declaringModuleId)) {
622
+ try {
623
+ return this.resolve(token, declaringModuleId);
624
+ } catch {
625
+ // Fall through to the legacy escape hatch below.
626
+ }
627
+ }
628
+ return this.resolve(token);
629
+ }
630
+ async resolveFactoryDependencyAsync(token, declaringModuleId) {
631
+ if (declaringModuleId !== ROOT_MODULE_ID && this.scopes.has(declaringModuleId)) {
632
+ try {
633
+ return await this.resolveAsync(token, declaringModuleId);
634
+ } catch {
635
+ // Fall through to the legacy escape hatch below.
636
+ }
637
+ }
638
+ return this.resolveAsync(token);
639
+ }
435
640
  resolveFactory(registration) {
436
641
  if (!registration.useFactory) {
437
642
  throw new Error('Factory function is missing');
438
643
  }
439
- // Factory inject deps resolve without a requester — Vela has no
440
- // `forRootAsync({ imports })` surface to track which module the factory
441
- // resolves from, so it uses the same escape-hatch shape as ModuleRef.
442
644
  const dependencies = (registration.inject || []).map((token)=>{
443
645
  const resolved = token instanceof ForwardRef ? token.factory() : token;
444
- return this.resolve(resolved);
646
+ return this.resolveFactoryDependency(resolved, registration.declaringModuleId);
445
647
  });
446
648
  const result = registration.useFactory(...dependencies);
447
649
  if (result instanceof Promise) {
@@ -468,18 +670,21 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
468
670
  throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
469
671
  }
470
672
  if (registration.useFactory) {
471
- if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
673
+ const scope = registration.effectiveScope ?? registration.scope;
674
+ if (scope === Scope.SINGLETON && registration.instance !== undefined) {
472
675
  return registration.instance;
473
676
  }
474
- // Factory inject deps resolve without a requester (same escape hatch as
475
- // sync resolveFactory).
677
+ // Module scope first, legacy no-requester fallback same policy as the
678
+ // sync resolveFactory path.
476
679
  const dependencies = await Promise.all((registration.inject || []).map((t)=>{
477
680
  const resolved = t instanceof ForwardRef ? t.factory() : t;
478
- return this.resolveAsync(resolved);
681
+ return this.resolveFactoryDependencyAsync(resolved, registration.declaringModuleId);
479
682
  }));
480
683
  const instance = await registration.useFactory(...dependencies);
481
- if (registration.scope === Scope.SINGLETON) {
684
+ if (scope === Scope.SINGLETON) {
482
685
  registration.instance = instance;
686
+ // Track for disposal on the ROOT, matching the sync resolveRegistration path.
687
+ if (isDisposable(instance)) this.root.disposables.push(instance);
483
688
  }
484
689
  return instance;
485
690
  }
@@ -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. 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
@@ -1,7 +1,7 @@
1
- import type { DynamicModule } from '../module/types';
2
1
  import type { CorsOptions } from './cors.types';
3
- export declare class CorsModule {
4
- static forRoot(options?: CorsOptions & {
5
- key?: string;
6
- }): DynamicModule;
2
+ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<CorsOptions, "forRoot", "create", {
3
+ isGlobal?: boolean;
4
+ }>;
5
+ export declare class CorsModule extends ConfigurableModuleClass {
7
6
  }
7
+ export {};
@@ -1,36 +1,37 @@
1
1
  import { cors } from "hono/cors";
2
- import { stableHash } from "../module/stable-hash.js";
2
+ import { defineModule } from "../module/define-module.js";
3
3
  import { APP_MIDDLEWARE } from "../pipeline/tokens.js";
4
4
  import { CORS_OPTIONS } from "./cors.tokens.js";
5
- export class CorsModule {
6
- static forRoot(options = {}) {
7
- const corsMiddleware = cors({
8
- origin: options.origin ?? '*',
9
- allowMethods: options.allowMethods,
10
- allowHeaders: options.allowHeaders,
11
- exposeHeaders: options.exposeHeaders,
12
- credentials: options.credentials,
13
- maxAge: options.maxAge
14
- });
15
- const middleware = {
16
- use: (c, next)=>corsMiddleware(c, next)
17
- };
18
- return {
19
- module: CorsModule,
20
- key: options.key ?? stableHash(options),
5
+ function buildCorsMiddleware(options) {
6
+ const corsMiddleware = cors({
7
+ origin: options.origin ?? '*',
8
+ allowMethods: options.allowMethods,
9
+ allowHeaders: options.allowHeaders,
10
+ exposeHeaders: options.exposeHeaders,
11
+ credentials: options.credentials,
12
+ maxAge: options.maxAge
13
+ });
14
+ return {
15
+ use: (c, next)=>corsMiddleware(c, next)
16
+ };
17
+ }
18
+ const { ConfigurableModuleClass } = defineModule({
19
+ name: 'Cors',
20
+ optionsToken: CORS_OPTIONS,
21
+ setup: ({ OPTIONS })=>({
21
22
  providers: [
22
- {
23
- provide: CORS_OPTIONS,
24
- useValue: options
25
- },
26
23
  {
27
24
  provide: APP_MIDDLEWARE,
28
- useValue: middleware
25
+ useFactory: (options)=>buildCorsMiddleware(options),
26
+ inject: [
27
+ OPTIONS
28
+ ]
29
29
  }
30
30
  ],
31
31
  exports: [
32
- CORS_OPTIONS
32
+ OPTIONS
33
33
  ]
34
- };
35
- }
34
+ })
35
+ });
36
+ export class CorsModule extends ConfigurableModuleClass {
36
37
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A decorator whose annotated classes/methods are findable through
3
+ * `DiscoveryService` by the decorator itself (no string key at the call site).
4
+ */
5
+ export interface DiscoverableDecorator<T> {
6
+ (value: T): ClassDecorator & MethodDecorator;
7
+ /** The metadata key backing this decorator — stable across HMR re-evals. */
8
+ KEY: string;
9
+ }
10
+ export interface CreateDiscoverableDecoratorOptions {
11
+ /**
12
+ * Append to a class-level list instead of overwriting a single slot —
13
+ * for stackable method decorators (`@Cron`-style: each application pushes
14
+ * `{ methodName, ...value }` onto the class's list, which
15
+ * `DiscoveryService.methodsWithMeta` flattens back to per-method entries).
16
+ */
17
+ append?: boolean;
18
+ }
19
+ /**
20
+ * Mint a discoverable decorator for a module's extension surface:
21
+ *
22
+ * ```ts
23
+ * export interface QueueConsumerMeta { queue: string }
24
+ * export const QueueConsumer = createDiscoverableDecorator<QueueConsumerMeta>('vela:queue:consumer');
25
+ *
26
+ * @QueueConsumer({ queue: 'emails' })
27
+ * @Injectable()
28
+ * class EmailConsumer { ... }
29
+ *
30
+ * // At bootstrap, anywhere:
31
+ * discovery.providersWithMeta(QueueConsumer) // typed { meta: QueueConsumerMeta }
32
+ * ```
33
+ *
34
+ * Unlike `Reflector.createDecorator`, the key is required and caller-chosen:
35
+ * a random key would mint a fresh identity on every HMR re-eval and orphan
36
+ * previously-decorated classes. Namespace it (`'<pkg>:<area>:<thing>'`).
37
+ */
38
+ export declare function createDiscoverableDecorator<T>(key: string, options?: CreateDiscoverableDecoratorOptions): DiscoverableDecorator<T>;
@@ -0,0 +1,42 @@
1
+ import { MetadataRegistry } from "../registry/metadata.registry.js";
2
+ /**
3
+ * Mint a discoverable decorator for a module's extension surface:
4
+ *
5
+ * ```ts
6
+ * export interface QueueConsumerMeta { queue: string }
7
+ * export const QueueConsumer = createDiscoverableDecorator<QueueConsumerMeta>('vela:queue:consumer');
8
+ *
9
+ * @QueueConsumer({ queue: 'emails' })
10
+ * @Injectable()
11
+ * class EmailConsumer { ... }
12
+ *
13
+ * // At bootstrap, anywhere:
14
+ * discovery.providersWithMeta(QueueConsumer) // typed { meta: QueueConsumerMeta }
15
+ * ```
16
+ *
17
+ * Unlike `Reflector.createDecorator`, the key is required and caller-chosen:
18
+ * a random key would mint a fresh identity on every HMR re-eval and orphan
19
+ * previously-decorated classes. Namespace it (`'<pkg>:<area>:<thing>'`).
20
+ */ export function createDiscoverableDecorator(key, options = {}) {
21
+ const decorator = (value)=>{
22
+ return (target, propertyKey)=>{
23
+ const ctor = propertyKey !== undefined ? target.constructor : target;
24
+ if (options.append) {
25
+ if (propertyKey !== undefined) {
26
+ MetadataRegistry.appendCustomClassMeta(ctor, key, {
27
+ methodName: propertyKey,
28
+ ...value
29
+ });
30
+ } else {
31
+ MetadataRegistry.appendCustomClassMeta(ctor, key, value);
32
+ }
33
+ } else if (propertyKey !== undefined) {
34
+ MetadataRegistry.setCustomHandlerMeta(ctor, propertyKey, key, value);
35
+ } else {
36
+ MetadataRegistry.setCustomClassMeta(ctor, key, value);
37
+ }
38
+ };
39
+ };
40
+ decorator.KEY = key;
41
+ return decorator;
42
+ }