@velajs/vela 1.10.0 → 1.13.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 (85) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/application.d.ts +25 -0
  3. package/dist/application.js +103 -2
  4. package/dist/container/container.d.ts +46 -1
  5. package/dist/container/container.js +156 -13
  6. package/dist/container/disposable.js +1 -1
  7. package/dist/container/types.d.ts +27 -0
  8. package/dist/cors/cors.module.d.ts +5 -5
  9. package/dist/cors/cors.module.js +26 -25
  10. package/dist/discovery/discoverable.decorator.d.ts +38 -0
  11. package/dist/discovery/discoverable.decorator.js +42 -0
  12. package/dist/discovery/discovery.service.d.ts +102 -0
  13. package/dist/discovery/discovery.service.js +177 -0
  14. package/dist/discovery/index.d.ts +2 -0
  15. package/dist/discovery/index.js +2 -0
  16. package/dist/entrypoint/entrypoint.registry.d.ts +42 -0
  17. package/dist/entrypoint/entrypoint.registry.js +112 -0
  18. package/dist/entrypoint/entrypoint.types.d.ts +41 -0
  19. package/dist/entrypoint/entrypoint.types.js +3 -0
  20. package/dist/entrypoint/execution-context.d.ts +15 -0
  21. package/dist/entrypoint/execution-context.js +21 -0
  22. package/dist/entrypoint/execution-scope.d.ts +21 -0
  23. package/dist/entrypoint/execution-scope.js +28 -0
  24. package/dist/entrypoint/index.d.ts +4 -0
  25. package/dist/entrypoint/index.js +4 -0
  26. package/dist/event-emitter/event-emitter.module.js +1 -0
  27. package/dist/event-emitter/event-emitter.subscriber.d.ts +3 -3
  28. package/dist/event-emitter/event-emitter.subscriber.js +12 -29
  29. package/dist/factory/adapter.d.ts +36 -0
  30. package/dist/factory/adapter.js +18 -0
  31. package/dist/factory/bootstrap.js +14 -2
  32. package/dist/factory.d.ts +11 -1
  33. package/dist/factory.js +23 -2
  34. package/dist/http/handler-executor.js +22 -19
  35. package/dist/http/route-contributor.d.ts +68 -0
  36. package/dist/http/route-contributor.js +22 -0
  37. package/dist/http/route.manager.js +33 -19
  38. package/dist/i18n/i18n.module.js +9 -1
  39. package/dist/index.d.ts +12 -3
  40. package/dist/index.js +9 -2
  41. package/dist/internal.d.ts +0 -2
  42. package/dist/internal.js +0 -7
  43. package/dist/module/configurable-module.builder.d.ts +12 -0
  44. package/dist/module/configurable-module.builder.js +21 -114
  45. package/dist/module/decorators.js +4 -2
  46. package/dist/module/define-module.d.ts +112 -0
  47. package/dist/module/define-module.js +232 -0
  48. package/dist/module/index.d.ts +3 -1
  49. package/dist/module/index.js +3 -1
  50. package/dist/module/lazy-modules.d.ts +82 -0
  51. package/dist/module/lazy-modules.js +231 -0
  52. package/dist/module/lazy-provider.d.ts +65 -0
  53. package/dist/module/lazy-provider.js +111 -0
  54. package/dist/module/module-loader.d.ts +21 -0
  55. package/dist/module/module-loader.js +99 -24
  56. package/dist/openapi/document.js +16 -15
  57. package/dist/pipeline/component.manager.d.ts +15 -9
  58. package/dist/pipeline/component.manager.js +32 -46
  59. package/dist/pipeline/index.d.ts +2 -0
  60. package/dist/pipeline/index.js +1 -0
  61. package/dist/pipeline/pipeline-runner.d.ts +31 -0
  62. package/dist/pipeline/pipeline-runner.js +40 -0
  63. package/dist/registry/metadata.registry.d.ts +9 -3
  64. package/dist/registry/metadata.registry.js +55 -25
  65. package/dist/registry/types.d.ts +10 -0
  66. package/dist/schedule/schedule.module.d.ts +5 -0
  67. package/dist/schedule/schedule.module.js +21 -8
  68. package/dist/schedule/schedule.registry.d.ts +3 -3
  69. package/dist/schedule/schedule.registry.js +23 -44
  70. package/dist/schedule-node/schedule-node.module.d.ts +5 -0
  71. package/dist/schedule-node/schedule-node.module.js +20 -10
  72. package/dist/seeder/seeder.module.d.ts +8 -10
  73. package/dist/seeder/seeder.module.js +19 -15
  74. package/dist/websocket/index.d.ts +1 -1
  75. package/dist/websocket/index.js +1 -1
  76. package/dist/websocket/websocket.module.d.ts +20 -3
  77. package/dist/websocket/websocket.module.js +50 -39
  78. package/dist/websocket/websocket.tokens.d.ts +2 -1
  79. package/dist/websocket/websocket.tokens.js +3 -2
  80. package/dist/websocket/ws-dispatcher.d.ts +21 -9
  81. package/dist/websocket/ws-dispatcher.js +44 -38
  82. package/dist/websocket-node/register-gateways.js +3 -3
  83. package/package.json +1 -1
  84. package/dist/http/crud-bridge.d.ts +0 -90
  85. package/dist/http/crud-bridge.js +0 -25
@@ -33,9 +33,40 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
33
33
  // Container-constructed instances in creation order, for LIFO disposal.
34
34
  // useValue providers are never tracked (they return before construction).
35
35
  disposables = [];
36
+ // Lazy-module seam (root-owned; children reach it via this.root). A
37
+ // resolution of a deferred registration CLAIMS its module; claimed groups
38
+ // are completed (constructed + hooks replayed) only when the resolution
39
+ // stack has unwound and no resolveAsync cascade is in flight — running the
40
+ // replay mid-construction could force-resolve a class currently on the
41
+ // resolution stack (discovery cascades) and mint a spurious
42
+ // circular-dependency error.
43
+ lazyHook;
44
+ asyncDepth = 0;
36
45
  constructor(options = {}){
37
46
  this.diagnostics = options.diagnostics ?? 'log';
38
47
  }
48
+ /** Install the lazy-module seam (bootstrap-time; root container only). */ setLazyHook(hook) {
49
+ this.root.lazyHook = hook;
50
+ }
51
+ claimLazyModule(declaringModuleId) {
52
+ const hook = this.root.lazyHook;
53
+ if (hook?.isPending(declaringModuleId)) {
54
+ hook.claim(declaringModuleId);
55
+ }
56
+ }
57
+ maybeDrainSync() {
58
+ const root = this.root;
59
+ const hook = root.lazyHook;
60
+ if (!hook?.hasClaimed()) return;
61
+ // Never replay hooks while construction is in flight; an async cascade
62
+ // drains (with await) at its own end instead. A running drain picks
63
+ // pending claims up itself — re-entering it is at best a no-op and on
64
+ // the async path a self-deadlock.
65
+ if (this.resolutionStack.size > 0) return;
66
+ if (root.asyncDepth > 0) return;
67
+ if (hook.isDraining()) return;
68
+ hook.drainSync();
69
+ }
39
70
  register(provider, declaringModuleId) {
40
71
  const moduleId = declaringModuleId ?? ROOT_MODULE_ID;
41
72
  if (typeof provider === 'function') {
@@ -125,7 +156,9 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
125
156
  resolve(token, requestingModuleId) {
126
157
  const registration = this.findRegistration(token, requestingModuleId);
127
158
  if (registration) {
128
- return this.resolveRegistration(registration, requestingModuleId);
159
+ const instance = this.resolveRegistration(registration, requestingModuleId);
160
+ this.maybeDrainSync();
161
+ return instance;
129
162
  }
130
163
  // Visibility error fires first when the token exists in some bucket but
131
164
  // isn't reachable from the requester's scope — the user wants to know
@@ -155,7 +188,9 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
155
188
  }
156
189
  resolveAll(token, requestingModuleId) {
157
190
  const registrations = this.findAllRegistrations(token, requestingModuleId);
158
- return registrations.map((r)=>this.resolveRegistration(r, requestingModuleId));
191
+ const instances = registrations.map((r)=>this.resolveRegistration(r, requestingModuleId));
192
+ this.maybeDrainSync();
193
+ return instances;
159
194
  }
160
195
  /**
161
196
  * Find a single reachable registration for a token, applying the visibility
@@ -288,6 +323,65 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
288
323
  /** True if the specified module's bucket has a registration for `token`. */ hasInScope(token, moduleId) {
289
324
  return this.providers.get(moduleId)?.has(token) ?? false;
290
325
  }
326
+ /** Module buckets that hold a registration for `token` (registration order). */ getOwnerModuleIds(token) {
327
+ return [
328
+ ...this.exporterIndex.get(token) ?? []
329
+ ];
330
+ }
331
+ /**
332
+ * Force-replace a provider across module scopes — the supported form of the
333
+ * override loop test harnesses need (module-scoped resolution consults the
334
+ * module's own bucket first, so a root-only override would never win there).
335
+ *
336
+ * - `'all-existing'` (default): every non-root bucket that already holds the
337
+ * token, plus `__root__` (so framework-internal lookups see it too).
338
+ * - `'root'`: only the `__root__` bucket.
339
+ * - `string[]`: exactly these bucket ids.
340
+ */ replaceProvider(provider, options = {}) {
341
+ const buckets = options.buckets ?? 'all-existing';
342
+ if (buckets === 'root') {
343
+ return this.register(provider);
344
+ }
345
+ if (Array.isArray(buckets)) {
346
+ for (const moduleId of buckets)this.register(provider, moduleId);
347
+ return this;
348
+ }
349
+ const token = typeof provider === 'function' ? provider : provider.provide;
350
+ if (!token) {
351
+ throw new Error('replaceProvider requires a token (`provide`)');
352
+ }
353
+ for (const [moduleId, bucket] of this.providers){
354
+ if (moduleId === ROOT_MODULE_ID) continue;
355
+ if (bucket.has(token)) this.register(provider, moduleId);
356
+ }
357
+ return this.register(provider);
358
+ }
359
+ /**
360
+ * True when the token belongs exclusively to lazy modules that have not
361
+ * been materialized yet — resolving it would trigger materialization.
362
+ * Build-time probes (route-manager middleware priority, entrypoint
363
+ * snapshots) use this to defer instead of forcing the group.
364
+ */ isLazyPending(token) {
365
+ const hook = this.root.lazyHook;
366
+ if (!hook) return false;
367
+ const owners = this.exporterIndex.get(token);
368
+ if (!owners || owners.size === 0) return false;
369
+ for (const owner of owners){
370
+ if (!hook.isPending(owner)) return false;
371
+ }
372
+ return true;
373
+ }
374
+ /**
375
+ * True when any registration of the token holds a constructed instance.
376
+ * Diagnostic helper (cold-start tests): checks WITHOUT resolving, so it
377
+ * never triggers lazy materialization.
378
+ */ isInstantiated(token) {
379
+ for (const owner of this.exporterIndex.get(token) ?? []){
380
+ const reg = this.providers.get(owner)?.get(token);
381
+ if (reg && reg.instance !== undefined) return true;
382
+ }
383
+ return false;
384
+ }
291
385
  getProviderScope(token) {
292
386
  const exporters = this.exporterIndex.get(token);
293
387
  if (!exporters) return undefined;
@@ -434,6 +528,13 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
434
528
  child.exporterIndex = clonedIndex;
435
529
  child.scopes = this.scopes;
436
530
  child.globals = this.globals;
531
+ // Registration objects are shallow-shared, so a sandbox resolve of a lazy
532
+ // module's singleton caches onto the SHARED registration. Point the
533
+ // sandbox at the real root so that resolution claims the module and
534
+ // replays its hooks like any other trigger — otherwise ModuleRef.create
535
+ // would leave a hook-less instance poisoning the shared cache (and its
536
+ // disposables tracked on an ephemeral root).
537
+ child.root = this.root;
437
538
  return child;
438
539
  }
439
540
  clear() {
@@ -484,8 +585,12 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
484
585
  }
485
586
  resolveRegistration(registration, requestingModuleId) {
486
587
  if (registration.useValue !== undefined) {
588
+ // Deliberately BEFORE the lazy claim: reading a lazy module's useValue
589
+ // (options tokens) has no construction cost to defer and must not
590
+ // materialize the group.
487
591
  return registration.useValue;
488
592
  }
593
+ this.claimLazyModule(registration.declaringModuleId);
489
594
  if (registration.useExisting) {
490
595
  // Pass through the original requester to catch alias leaks
491
596
  return this.resolve(registration.useExisting, requestingModuleId);
@@ -516,10 +621,9 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
516
621
  try {
517
622
  let instance;
518
623
  if (registration.useFactory) {
519
- // Factories (forRootAsync, useFactory) commonly inject deps from the
520
- // importing module's scope. Vela has no `forRootAsync({ imports })`
521
- // surface to track that, so factory inject deps resolve from the
522
- // declaring module's POV — same escape-hatch shape as ModuleRef.
624
+ // Factory inject deps resolve from the declaring module's scope first
625
+ // (visibility-correct for forRootAsync), with the legacy no-requester
626
+ // lookup kept as fallback see resolveFactoryDependency.
523
627
  instance = this.resolveFactory(registration);
524
628
  } else if (registration.useClass) {
525
629
  instance = this.resolveClass(registration.useClass, registration.declaringModuleId);
@@ -578,16 +682,40 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
578
682
  });
579
683
  return new target(...dependencies);
580
684
  }
685
+ /**
686
+ * Resolve one factory `inject` dependency. Since 1.11 the declaring module's
687
+ * scope is consulted FIRST (proper visibility semantics — a `forRootAsync`
688
+ * factory can inject providers reachable through its module's imports); on
689
+ * any failure the legacy no-requester lookup (root bucket, then first
690
+ * exporter) is kept as fallback, so every previously-resolving graph still
691
+ * resolves.
692
+ */ resolveFactoryDependency(token, declaringModuleId) {
693
+ if (declaringModuleId !== ROOT_MODULE_ID && this.scopes.has(declaringModuleId)) {
694
+ try {
695
+ return this.resolve(token, declaringModuleId);
696
+ } catch {
697
+ // Fall through to the legacy escape hatch below.
698
+ }
699
+ }
700
+ return this.resolve(token);
701
+ }
702
+ async resolveFactoryDependencyAsync(token, declaringModuleId) {
703
+ if (declaringModuleId !== ROOT_MODULE_ID && this.scopes.has(declaringModuleId)) {
704
+ try {
705
+ return await this.resolveAsync(token, declaringModuleId);
706
+ } catch {
707
+ // Fall through to the legacy escape hatch below.
708
+ }
709
+ }
710
+ return this.resolveAsync(token);
711
+ }
581
712
  resolveFactory(registration) {
582
713
  if (!registration.useFactory) {
583
714
  throw new Error('Factory function is missing');
584
715
  }
585
- // Factory inject deps resolve without a requester — Vela has no
586
- // `forRootAsync({ imports })` surface to track which module the factory
587
- // resolves from, so it uses the same escape-hatch shape as ModuleRef.
588
716
  const dependencies = (registration.inject || []).map((token)=>{
589
717
  const resolved = token instanceof ForwardRef ? token.factory() : token;
590
- return this.resolve(resolved);
718
+ return this.resolveFactoryDependency(resolved, registration.declaringModuleId);
591
719
  });
592
720
  const result = registration.useFactory(...dependencies);
593
721
  if (result instanceof Promise) {
@@ -596,6 +724,20 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
596
724
  return result;
597
725
  }
598
726
  async resolveAsync(token, requestingModuleId) {
727
+ const root = this.root;
728
+ root.asyncDepth++;
729
+ let result;
730
+ try {
731
+ result = await this.resolveAsyncInner(token, requestingModuleId);
732
+ } finally{
733
+ root.asyncDepth--;
734
+ }
735
+ if (root.asyncDepth === 0 && root.lazyHook?.hasClaimed() && !root.lazyHook.isDraining()) {
736
+ await root.lazyHook.drainAsync();
737
+ }
738
+ return result;
739
+ }
740
+ async resolveAsyncInner(token, requestingModuleId) {
599
741
  const registration = this.findRegistration(token, requestingModuleId);
600
742
  if (!registration) {
601
743
  if (requestingModuleId !== undefined && this.scopes.has(requestingModuleId) && this.exporterIndex.has(token)) {
@@ -618,11 +760,12 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
618
760
  if (scope === Scope.SINGLETON && registration.instance !== undefined) {
619
761
  return registration.instance;
620
762
  }
621
- // Factory inject deps resolve without a requester (same escape hatch as
622
- // sync resolveFactory).
763
+ this.claimLazyModule(registration.declaringModuleId);
764
+ // Module scope first, legacy no-requester fallback — same policy as the
765
+ // sync resolveFactory path.
623
766
  const dependencies = await Promise.all((registration.inject || []).map((t)=>{
624
767
  const resolved = t instanceof ForwardRef ? t.factory() : t;
625
- return this.resolveAsync(resolved);
768
+ return this.resolveFactoryDependencyAsync(resolved, registration.declaringModuleId);
626
769
  }));
627
770
  const instance = await registration.useFactory(...dependencies);
628
771
  if (scope === Scope.SINGLETON) {
@@ -1,4 +1,4 @@
1
- // Edge-safe disposal contract (mirrors stratal's). Precedence:
1
+ // Edge-safe disposal contract. Precedence:
2
2
  // Symbol.asyncDispose > Symbol.dispose > .dispose()
3
3
  // Symbol.asyncDispose/Symbol.dispose are resolved at runtime (they are not in
4
4
  // the ES2022 lib the project targets), so nothing here depends on the
@@ -85,6 +85,33 @@ export interface ModuleScope {
85
85
  importedModules: Set<string>;
86
86
  exportedTokens: Set<Token>;
87
87
  isGlobal: boolean;
88
+ /** Module instance opted into deferred (first-use) materialization. */
89
+ lazy?: boolean;
90
+ }
91
+ /**
92
+ * The container's seam into lazy-module materialization (implemented by
93
+ * `LazyModuleManager`). The container only ever *claims* a pending module at
94
+ * a resolution trigger and *drains* completed claims when the resolution
95
+ * stack has fully unwound — construction and hook replay live behind this
96
+ * interface so the container stays module-system-agnostic.
97
+ */
98
+ export interface LazyResolutionHook {
99
+ /** Is this module instance still deferred (untriggered)? */
100
+ isPending(moduleId: string): boolean;
101
+ /** Mark a pending module as triggered; idempotent. */
102
+ claim(moduleId: string): void;
103
+ /** Any claimed-but-unmaterialized groups? (cheap fast-path check) */
104
+ hasClaimed(): boolean;
105
+ /**
106
+ * A drain loop is currently running. The container must NOT start (or
107
+ * await) another drain from inside it — the running loop picks pending
108
+ * claims up; awaiting would self-deadlock on the async path.
109
+ */
110
+ isDraining(): boolean;
111
+ /** Complete claimed groups synchronously; throws if async work surfaces. */
112
+ drainSync(): void;
113
+ /** Complete claimed groups, awaiting async construction and hooks. */
114
+ drainAsync(): Promise<void>;
88
115
  }
89
116
  export type Diagnostics = 'silent' | 'log' | 'throw';
90
117
  export interface ContainerOptions {
@@ -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
+ }
@@ -0,0 +1,102 @@
1
+ import { Scope } from '../constants';
2
+ import { Container } from '../container/container';
3
+ import type { Token, Type } from '../container/types';
4
+ import type { Constructor } from '../registry/types';
5
+ import type { DiscoverableDecorator } from './discoverable.decorator';
6
+ /** A container-registered class provider surfaced by discovery. */
7
+ export interface DiscoveredClass<T = unknown> {
8
+ token: Token;
9
+ metatype: Type<T>;
10
+ /** Module buckets holding this token (registration order; first = primary). */
11
+ moduleIds: string[];
12
+ scope: Scope;
13
+ /**
14
+ * Resolved instance. `undefined` only when the provider is request-scoped
15
+ * and `includeRequestScoped` was not set — request-scoped providers cannot
16
+ * be materialized at bootstrap without fabricating a phantom request.
17
+ */
18
+ instance: T | undefined;
19
+ }
20
+ export interface DiscoveredMethodMeta<M = unknown> {
21
+ class: DiscoveredClass;
22
+ methodName: string | symbol;
23
+ meta: M;
24
+ }
25
+ export interface DiscoveryFilter {
26
+ /** Restrict to providers declared by these module buckets. */
27
+ moduleId?: string | string[];
28
+ /**
29
+ * Resolve request-scoped providers too (constructs an instance outside any
30
+ * request — only for callers that know what they're doing). Default false:
31
+ * request-scoped hits are skipped with a diagnostics warning.
32
+ */
33
+ includeRequestScoped?: boolean;
34
+ /**
35
+ * Return providers of not-yet-materialized lazy modules as metadata-only
36
+ * entries (`instance: undefined`, mirroring the request-scoped convention)
37
+ * instead of resolving them — which would force the whole module group.
38
+ * Used by `EntrypointRegistry.build`; default false so every existing
39
+ * scanner keeps its transparent cascade-materialization semantics.
40
+ */
41
+ deferLazy?: boolean;
42
+ }
43
+ /**
44
+ * Decorator-driven provider discovery — the public replacement for the
45
+ * bootstrap scan every subsystem used to hand-roll (`container.getTokens()` +
46
+ * `MetadataRegistry.getCustomClassMeta` + `resolve` + diagnostics handling).
47
+ *
48
+ * Registered globally by `bootstrap()`, so any provider can inject it:
49
+ *
50
+ * ```ts
51
+ * @Injectable()
52
+ * class MyRegistry implements OnApplicationBootstrap {
53
+ * constructor(private readonly discovery: DiscoveryService) {}
54
+ * onApplicationBootstrap() {
55
+ * for (const { instance, meta } of this.discovery.providersWithMeta<MyMeta>(MY_KEY)) {
56
+ * this.register(instance, meta);
57
+ * }
58
+ * }
59
+ * }
60
+ * ```
61
+ *
62
+ * Discovery is kernel-level, not encapsulation-scoped: module visibility
63
+ * protects modules from *each other*, never from the framework's own
64
+ * dispatchers. Use `DiscoveryFilter.moduleId` for the rare narrowing case.
65
+ *
66
+ * The resolve-failure policy honors the container's diagnostics mode exactly
67
+ * like the legacy loops: `throw` rethrows, `log` warns and skips, `silent`
68
+ * skips — so a broken provider never silently changes discovery semantics.
69
+ */
70
+ export declare class DiscoveryService {
71
+ private readonly container;
72
+ constructor(container: Container);
73
+ /** Every class-token provider registered in the container. */
74
+ getProviders(filter?: DiscoveryFilter): DiscoveredClass[];
75
+ /**
76
+ * Providers whose class carries class-level metadata under `key` — both
77
+ * class decorators (single value) and method decorators that append
78
+ * class-level lists (`@Cron`, `@OnEvent`, `@SubscribeMessage`).
79
+ */
80
+ providersWithMeta<M>(key: string | DiscoverableDecorator<M>, filter?: DiscoveryFilter): Array<DiscoveredClass & {
81
+ meta: M;
82
+ }>;
83
+ /**
84
+ * Handler methods carrying metadata under `key`. Merges the two storage
85
+ * conventions:
86
+ * - class-level appended lists whose items carry `methodName`
87
+ * (`@Cron`/`@OnEvent`-style method decorators), and
88
+ * - true handler-level metadata (`@SetMetadata`/`createDiscoverableDecorator`
89
+ * applied to a method).
90
+ */
91
+ methodsWithMeta<M>(key: string | DiscoverableDecorator<M>, filter?: DiscoveryFilter): DiscoveredMethodMeta<M>[];
92
+ /**
93
+ * Index-first candidate walk with a legacy full-scan fallback: when the
94
+ * reverse index knows nothing about a key, an older copy of the registry
95
+ * module may have taken the writes (mixed dist/src or package versions in
96
+ * one process) — fall back to scanning container tokens the way the
97
+ * pre-DiscoveryService loops did.
98
+ */
99
+ private candidatesWithClassMeta;
100
+ private buildEntry;
101
+ }
102
+ export type { Constructor };