@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
@@ -0,0 +1,231 @@
1
+ import { Scope } from "../constants.js";
2
+ import { hasOnApplicationBootstrap, hasOnModuleInit } from "../lifecycle/index.js";
3
+ function isThenable(x) {
4
+ return x !== null && (typeof x === 'object' || typeof x === 'function') && typeof x.then === 'function';
5
+ }
6
+ /**
7
+ * Owns deferred-module state for one application: which module instances are
8
+ * still pending, which have been claimed by a container trigger, and how
9
+ * claimed groups get completed (construction + lifecycle-hook replay).
10
+ *
11
+ * Phases:
12
+ * - `'bootstrap'` (until the end of `callOnApplicationBootstrap`): completed
13
+ * groups are *absorbed* — their instances queue up for the application's
14
+ * normal hook phases, so a lazy module dragged in by an eager consumer gets
15
+ * today-identical semantics.
16
+ * - `'live'`: hooks replay at drain time (memoized, exactly once), then the
17
+ * instances are handed to the application for shutdown-hook symmetry.
18
+ *
19
+ * Sync/async duality: sync drains throw a descriptive error when a member
20
+ * factory or hook yields a thenable — lazy modules with async providers or
21
+ * hooks must be reached through an async seam (`materializeLazyModules()`,
22
+ * `resolveAsync`) or stay eager.
23
+ */ export class LazyModuleManager {
24
+ container;
25
+ pending = new Map();
26
+ claimed = [];
27
+ absorbed = [];
28
+ phase = 'bootstrap';
29
+ draining = false;
30
+ // In-flight async drain: concurrent drainAsync callers must await the SAME
31
+ // completion (the running loop picks their claims up), never resolve early
32
+ // with a group's hooks still pending.
33
+ drainPromise;
34
+ onMaterialized;
35
+ constructor(container){
36
+ this.container = container;
37
+ }
38
+ registerGroup(group) {
39
+ this.pending.set(group.moduleId, group);
40
+ }
41
+ /** Application sink for live-phase materializations (shutdown symmetry). */ setOnMaterialized(sink) {
42
+ this.onMaterialized = sink;
43
+ }
44
+ setPhaseLive() {
45
+ this.phase = 'live';
46
+ }
47
+ isPending(moduleId) {
48
+ return this.pending.has(moduleId);
49
+ }
50
+ /** Tokens whose owning modules are ALL still pending are deferred. */ hasPendingModules() {
51
+ return this.pending.size > 0;
52
+ }
53
+ claim(moduleId) {
54
+ const group = this.pending.get(moduleId);
55
+ if (!group) return;
56
+ this.pending.delete(moduleId);
57
+ this.claimed.push(group);
58
+ }
59
+ hasClaimed() {
60
+ return this.claimed.length > 0;
61
+ }
62
+ /**
63
+ * A drain loop is running. The container consults this before its
64
+ * post-resolution drain: `constructGroupAsync` resolves members through
65
+ * `resolveAsync`, whose end-of-cascade check would otherwise re-enter and
66
+ * AWAIT the very drain promise it is executing inside — a self-referential
67
+ * await that never settles (with ≥2 claimed groups). Claims made while
68
+ * draining are picked up by the running loop instead.
69
+ */ isDraining() {
70
+ return this.draining;
71
+ }
72
+ /** Instances constructed during the bootstrap phase, owed their hooks. */ takeAbsorbed() {
73
+ if (this.absorbed.length === 0) return [];
74
+ return this.absorbed.splice(0, this.absorbed.length);
75
+ }
76
+ drainSync() {
77
+ if (this.draining) return;
78
+ this.draining = true;
79
+ try {
80
+ // Construct-all-then-hook, batch by batch: a cascade's groups arrive in
81
+ // consumer-before-dependency claim order (the consumer's token resolves
82
+ // first; constructing it claims its deps), so hook phases run over the
83
+ // REVERSED batch — dependency-before-consumer, matching what the eager
84
+ // bootstrap would have produced. Hooks can claim further groups
85
+ // (discovery cascades) → outer loop.
86
+ while(this.claimed.length > 0){
87
+ const batch = [];
88
+ while(this.claimed.length > 0){
89
+ const group = this.claimed.shift();
90
+ batch.push({
91
+ group,
92
+ instances: this.constructGroupSync(group)
93
+ });
94
+ }
95
+ this.finishBatchSync(batch);
96
+ }
97
+ } finally{
98
+ this.draining = false;
99
+ }
100
+ }
101
+ drainAsync() {
102
+ if (this.draining) {
103
+ // External concurrent callers await the in-flight completion (its loop
104
+ // picks their claims up). Callers INSIDE the drain's own await chain
105
+ // never reach here — the container skips its post-resolution drain
106
+ // while isDraining() (self-await would deadlock).
107
+ return this.drainPromise ?? Promise.resolve();
108
+ }
109
+ this.draining = true;
110
+ const run = (async ()=>{
111
+ try {
112
+ while(this.claimed.length > 0){
113
+ const batch = [];
114
+ while(this.claimed.length > 0){
115
+ const group = this.claimed.shift();
116
+ batch.push({
117
+ group,
118
+ instances: await this.constructGroupAsync(group)
119
+ });
120
+ }
121
+ await this.finishBatchAsync(batch);
122
+ }
123
+ } finally{
124
+ this.draining = false;
125
+ this.drainPromise = undefined;
126
+ }
127
+ })();
128
+ this.drainPromise = run;
129
+ return run;
130
+ }
131
+ /**
132
+ * Materialize every still-pending group whose providers compute entrypoints
133
+ * (`collectEntrypoints` on a class prototype). Called by the application
134
+ * right before the entrypoint-registry snapshot so computed contributions
135
+ * are never silently absent. The cost — those modules are effectively
136
+ * eager — is the documented price of contributing computed entrypoints.
137
+ */ async materializeContributors() {
138
+ for (const group of [
139
+ ...this.pending.values()
140
+ ]){
141
+ if (group.hasEntrypointContributor) this.claim(group.moduleId);
142
+ }
143
+ await this.drainAsync();
144
+ }
145
+ /** Materialize everything still pending (warmup / tests / node runtimes). */ async materializeAll() {
146
+ for (const moduleId of [
147
+ ...this.pending.keys()
148
+ ])this.claim(moduleId);
149
+ await this.drainAsync();
150
+ }
151
+ groupTokensToConstruct(group) {
152
+ return group.tokens.filter((token)=>this.container.getProviderScope(token) !== Scope.REQUEST);
153
+ }
154
+ constructGroupSync(group) {
155
+ const out = new Set();
156
+ for (const token of this.groupTokensToConstruct(group)){
157
+ try {
158
+ out.add(this.container.resolve(token, group.moduleId));
159
+ } catch (error) {
160
+ // Only the sync-resolution-of-async-factory error is a seam problem;
161
+ // genuine provider failures must surface untouched.
162
+ if (error instanceof Error && error.message.includes('returned a Promise')) {
163
+ throw this.describeSyncFailure(group.moduleId, error);
164
+ }
165
+ throw error;
166
+ }
167
+ }
168
+ return [
169
+ ...out
170
+ ];
171
+ }
172
+ async constructGroupAsync(group) {
173
+ const out = new Set();
174
+ for (const token of this.groupTokensToConstruct(group)){
175
+ out.add(await this.container.resolveAsync(token, group.moduleId));
176
+ }
177
+ return [
178
+ ...out
179
+ ];
180
+ }
181
+ /** Reversed batch = dependency-before-consumer (see drainSync comment). */ orderBatch(batch) {
182
+ return [
183
+ ...batch
184
+ ].reverse();
185
+ }
186
+ finishBatchSync(batch) {
187
+ const ordered = this.orderBatch(batch);
188
+ if (this.phase === 'bootstrap') {
189
+ for (const { instances } of ordered)this.absorbed.push(...instances);
190
+ return;
191
+ }
192
+ for (const { group, instances } of ordered){
193
+ for (const instance of instances){
194
+ if (hasOnModuleInit(instance) && isThenable(instance.onModuleInit())) {
195
+ throw this.describeSyncFailure(group.moduleId);
196
+ }
197
+ }
198
+ }
199
+ for (const { group, instances } of ordered){
200
+ for (const instance of instances){
201
+ if (hasOnApplicationBootstrap(instance) && isThenable(instance.onApplicationBootstrap())) {
202
+ throw this.describeSyncFailure(group.moduleId);
203
+ }
204
+ }
205
+ }
206
+ this.onMaterialized?.(ordered.flatMap((b)=>b.instances));
207
+ }
208
+ async finishBatchAsync(batch) {
209
+ const ordered = this.orderBatch(batch);
210
+ if (this.phase === 'bootstrap') {
211
+ for (const { instances } of ordered)this.absorbed.push(...instances);
212
+ return;
213
+ }
214
+ for (const { instances } of ordered){
215
+ for (const instance of instances){
216
+ if (hasOnModuleInit(instance)) await instance.onModuleInit();
217
+ }
218
+ }
219
+ for (const { instances } of ordered){
220
+ for (const instance of instances){
221
+ if (hasOnApplicationBootstrap(instance)) await instance.onApplicationBootstrap();
222
+ }
223
+ }
224
+ this.onMaterialized?.(ordered.flatMap((b)=>b.instances));
225
+ }
226
+ describeSyncFailure(moduleId, cause) {
227
+ return new Error(`[vela] lazy module '${moduleId}' has async providers or lifecycle hooks and was ` + `triggered through a synchronous resolution path. Reach it through an async seam ` + `first (app.materializeLazyModules(), an async provider) or remove lazy: true.`, cause !== undefined ? {
228
+ cause
229
+ } : undefined);
230
+ }
231
+ }
@@ -0,0 +1,65 @@
1
+ import { InjectionToken, type InferTokens, type ProviderOptions, type Token, type Type } from '../container/types';
2
+ import type { ComponentType, DynamicModule } from '../registry/types';
3
+ import type { ModuleContributions } from './define-module';
4
+ export interface LazyProviderSpec<T, Inject extends readonly Token<unknown>[]> {
5
+ /** Token under which the memoized thunk `() => T` is provided. */
6
+ provide: Token<() => T>;
7
+ inject?: Inject;
8
+ useFactory: (...deps: InferTokens<Inject>) => T;
9
+ /** Memoize the first call's result (default true). */
10
+ memoize?: boolean;
11
+ }
12
+ /**
13
+ * Provide a zero-arg thunk `() => T` whose factory runs on FIRST CALL, not at
14
+ * provider construction — for values that don't exist yet when the module
15
+ * graph is built (Cloudflare bindings are only live at request time).
16
+ *
17
+ * The shared primitive replacing the hand-rolled
18
+ * `useFactory: (...deps) => () => build(...deps)` closure that auth and
19
+ * storage each copy-pasted.
20
+ *
21
+ * ```ts
22
+ * lazyProvider({
23
+ * provide: STORAGE_DRIVER_BUILDER,
24
+ * inject: [MODULE_OPTIONS_TOKEN],
25
+ * useFactory: (options) => options.driver(),
26
+ * })
27
+ * ```
28
+ */
29
+ export declare function lazyProvider<T, const Inject extends readonly Token<unknown>[] = readonly Token<unknown>[]>(spec: LazyProviderSpec<T, Inject>): ProviderOptions;
30
+ /**
31
+ * The one idiom for registering an app-wide component from a module's
32
+ * providers. Returns registrations to spread:
33
+ *
34
+ * ```ts
35
+ * providers: [MyService, ...provideGlobal('guard', AuthGuard)]
36
+ * ```
37
+ *
38
+ * Class components are registered as providers and wired via `useExisting`
39
+ * (so DI constructs them with their dependencies); instances via `useValue`.
40
+ * Inside `defineModule`, prefer the equivalent `global:` contribution slot.
41
+ */
42
+ export declare function provideGlobal(kind: ComponentType, component: Type | object): Array<Type | ProviderOptions>;
43
+ /**
44
+ * A first-class side-effect-only module: contributes providers/exports without
45
+ * being a configurable module — the supported form of the "empty marker
46
+ * module" trick (i18n's `registerMessages`). Content-derived `key` makes
47
+ * identical contributions dedup (HMR-idempotent) while distinct ones coexist.
48
+ *
49
+ * ```ts
50
+ * export function registerMessages(messages: Messages): DynamicModule {
51
+ * return sideEffectModule('I18nMessages', {
52
+ * providers: [{ provide: I18N_MESSAGES, useValue: messages }],
53
+ * exports: [I18N_MESSAGES],
54
+ * });
55
+ * }
56
+ * ```
57
+ */
58
+ export declare function sideEffectModule(name: string, contributions?: Omit<ModuleContributions, 'global'> & {
59
+ key?: string;
60
+ }): DynamicModule;
61
+ /**
62
+ * Blessed token-minting convention: always an `InjectionToken` (never a raw
63
+ * string), named for diagnostics. Namespace it `'<pkg>:<area>:<thing>'`.
64
+ */
65
+ export declare function moduleToken<T>(name: string): InjectionToken<T>;
@@ -0,0 +1,111 @@
1
+ import { InjectionToken } from "../container/types.js";
2
+ import { Module } from "./decorators.js";
3
+ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
4
+ import { stableHash } from "./stable-hash.js";
5
+ /**
6
+ * Provide a zero-arg thunk `() => T` whose factory runs on FIRST CALL, not at
7
+ * provider construction — for values that don't exist yet when the module
8
+ * graph is built (Cloudflare bindings are only live at request time).
9
+ *
10
+ * The shared primitive replacing the hand-rolled
11
+ * `useFactory: (...deps) => () => build(...deps)` closure that auth and
12
+ * storage each copy-pasted.
13
+ *
14
+ * ```ts
15
+ * lazyProvider({
16
+ * provide: STORAGE_DRIVER_BUILDER,
17
+ * inject: [MODULE_OPTIONS_TOKEN],
18
+ * useFactory: (options) => options.driver(),
19
+ * })
20
+ * ```
21
+ */ export function lazyProvider(spec) {
22
+ const memoize = spec.memoize ?? true;
23
+ return {
24
+ provide: spec.provide,
25
+ inject: [
26
+ ...spec.inject ?? []
27
+ ],
28
+ useFactory: (...deps)=>{
29
+ const build = ()=>spec.useFactory(...deps);
30
+ if (!memoize) return build;
31
+ let cached;
32
+ return ()=>(cached ??= {
33
+ value: build()
34
+ }).value;
35
+ }
36
+ };
37
+ }
38
+ const GLOBAL_COMPONENT_TOKENS = {
39
+ guard: APP_GUARD,
40
+ pipe: APP_PIPE,
41
+ interceptor: APP_INTERCEPTOR,
42
+ filter: APP_FILTER,
43
+ middleware: APP_MIDDLEWARE
44
+ };
45
+ /**
46
+ * The one idiom for registering an app-wide component from a module's
47
+ * providers. Returns registrations to spread:
48
+ *
49
+ * ```ts
50
+ * providers: [MyService, ...provideGlobal('guard', AuthGuard)]
51
+ * ```
52
+ *
53
+ * Class components are registered as providers and wired via `useExisting`
54
+ * (so DI constructs them with their dependencies); instances via `useValue`.
55
+ * Inside `defineModule`, prefer the equivalent `global:` contribution slot.
56
+ */ export function provideGlobal(kind, component) {
57
+ const token = GLOBAL_COMPONENT_TOKENS[kind];
58
+ if (typeof component === 'function') {
59
+ return [
60
+ component,
61
+ {
62
+ provide: token,
63
+ useExisting: component
64
+ }
65
+ ];
66
+ }
67
+ return [
68
+ {
69
+ provide: token,
70
+ useValue: component
71
+ }
72
+ ];
73
+ }
74
+ /**
75
+ * A first-class side-effect-only module: contributes providers/exports without
76
+ * being a configurable module — the supported form of the "empty marker
77
+ * module" trick (i18n's `registerMessages`). Content-derived `key` makes
78
+ * identical contributions dedup (HMR-idempotent) while distinct ones coexist.
79
+ *
80
+ * ```ts
81
+ * export function registerMessages(messages: Messages): DynamicModule {
82
+ * return sideEffectModule('I18nMessages', {
83
+ * providers: [{ provide: I18N_MESSAGES, useValue: messages }],
84
+ * exports: [I18N_MESSAGES],
85
+ * });
86
+ * }
87
+ * ```
88
+ */ export function sideEffectModule(name, contributions = {}) {
89
+ const { key, ...rest } = contributions;
90
+ // Computed-property-name idiom: mints a class whose .name is `name` without
91
+ // dynamic code evaluation (edge-safe; no `new Function`).
92
+ const moduleClass = {
93
+ [name]: class {
94
+ }
95
+ }[name];
96
+ Module({})(moduleClass);
97
+ return {
98
+ module: moduleClass,
99
+ key: key ?? stableHash(rest),
100
+ providers: rest.providers ?? [],
101
+ controllers: rest.controllers ?? [],
102
+ imports: rest.imports ?? [],
103
+ exports: rest.exports ?? []
104
+ };
105
+ }
106
+ /**
107
+ * Blessed token-minting convention: always an `InjectionToken` (never a raw
108
+ * string), named for diagnostics. Namespace it `'<pkg>:<area>:<thing>'`.
109
+ */ export function moduleToken(name) {
110
+ return new InjectionToken(name);
111
+ }
@@ -2,6 +2,12 @@ import type { Container } from "../container/container";
2
2
  import type { Token, Type } from "../container/types";
3
3
  import type { RouteManager } from "../http/route.manager";
4
4
  import type { MiddlewareRouteDefinition } from "./middleware";
5
+ /** What the loader records per lazy module instance (see LazyModuleManager). */
6
+ export interface LazyModuleGroupSpec {
7
+ moduleId: string;
8
+ tokens: Token[];
9
+ hasEntrypointContributor: boolean;
10
+ }
5
11
  export declare class ModuleLoader {
6
12
  private container;
7
13
  private router;
@@ -16,6 +22,8 @@ export declare class ModuleLoader {
16
22
  private appProviderTokens;
17
23
  private moduleIdByClassKey;
18
24
  private seenModuleIds;
25
+ private lazyModuleIds;
26
+ private lazyGroups;
19
27
  constructor(container: Container, router: RouteManager);
20
28
  load(rootModule: Type): void;
21
29
  private getModuleId;
@@ -25,6 +33,7 @@ export declare class ModuleLoader {
25
33
  private cacheExports;
26
34
  private processModule;
27
35
  private warnOnMixedDefaultAndKeyed;
36
+ /** Registers the provider and returns the token it was registered under. */
28
37
  private registerProvider;
29
38
  private isAppToken;
30
39
  private buildExportSet;
@@ -32,6 +41,18 @@ export declare class ModuleLoader {
32
41
  getRegisteredProviders(): Token[];
33
42
  getAppProviderTokens(token: Token): Token[];
34
43
  getConsumerMiddlewareDefinitions(): MiddlewareRouteDefinition[];
44
+ /**
45
+ * Every lazy module instance recorded during load. Handed to the
46
+ * LazyModuleManager at bootstrap; empty when no module opted in.
47
+ */
48
+ getLazyGroups(): LazyModuleGroupSpec[];
49
+ /**
50
+ * A token is deferred iff EVERY module bucket holding it belongs to a lazy
51
+ * module instance — a token also registered by a non-lazy module stays on
52
+ * the eager pass (and resolving it there claims the lazy sibling, which is
53
+ * the "any use = init" semantic, not a bug).
54
+ */
55
+ private isLazyOnlyToken;
35
56
  resolveAllInstances(): Promise<unknown[]>;
36
57
  private routeError;
37
58
  }
@@ -4,6 +4,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
4
4
  import { MetadataRegistry } from "../registry/metadata.registry.js";
5
5
  import { getOrCreateArray } from "../registry/util.js";
6
6
  import { getModuleMetadata, isModule } from "./decorators.js";
7
+ import { LazyModuleManager } from "./lazy-modules.js";
7
8
  import { MiddlewareBuilder } from "./middleware.js";
8
9
  const APP_TOKENS = new Set([
9
10
  APP_GUARD,
@@ -13,6 +14,16 @@ const APP_TOKENS = new Set([
13
14
  APP_MIDDLEWARE
14
15
  ]);
15
16
  const DEFAULT_KEY = "default";
17
+ /**
18
+ * Structural check for `ContributesEntrypoints` on a provider's class —
19
+ * deliberately string-coupled to the interface's method name so the loader
20
+ * does not import from `entrypoint/` (layering).
21
+ */ function declaresEntrypointContributor(provider) {
22
+ const cls = typeof provider === "function" ? provider : provider.useClass;
23
+ if (typeof cls !== "function") return false;
24
+ const proto = cls.prototype;
25
+ return typeof proto?.collectEntrypoints === "function";
26
+ }
16
27
  function isDynamicModule(value) {
17
28
  // TS 4.9+ narrows `'module' in value` so `value.module` is typed `unknown`
18
29
  // — no cast required for the typeof check below.
@@ -78,6 +89,10 @@ export class ModuleLoader {
78
89
  // (class, key) → composed moduleId (cached for stable identity within a load)
79
90
  moduleIdByClassKey = new Map();
80
91
  seenModuleIds = new Set();
92
+ // Lazy (deferred-init) module instances: moduleId → its own tokens in
93
+ // registration order. Consumed by LazyModuleManager via getLazyGroups().
94
+ lazyModuleIds = new Set();
95
+ lazyGroups = new Map();
81
96
  constructor(container, router){
82
97
  this.container = container;
83
98
  this.router = router;
@@ -87,6 +102,23 @@ export class ModuleLoader {
87
102
  for (const controller of this.collectedControllers){
88
103
  this.router.registerController(controller);
89
104
  }
105
+ // Arm the deferred-init seam HERE — at the end of load(), not in
106
+ // bootstrap() — so every consumer of the loader gets identical lazy
107
+ // semantics, including hand-rolled bootstrap paths that never call
108
+ // bootstrap() (@velajs/testing's TestingModuleBuilder.compile builds its
109
+ // own container). Still after all module-load-time resolutions
110
+ // (NestModule.configure), so load-time resolution never claims a group.
111
+ // Registered as a provider so VelaApplication can drive the phase
112
+ // transitions from any of those paths.
113
+ const lazyManager = new LazyModuleManager(this.container);
114
+ for (const group of this.getLazyGroups()){
115
+ lazyManager.registerGroup(group);
116
+ }
117
+ this.container.setLazyHook(lazyManager);
118
+ this.container.register({
119
+ provide: LazyModuleManager,
120
+ useValue: lazyManager
121
+ });
90
122
  }
91
123
  getModuleId(moduleClass, key) {
92
124
  let perClass = this.moduleIdByClassKey.get(moduleClass);
@@ -238,21 +270,38 @@ export class ModuleLoader {
238
270
  // include it in its own scope.
239
271
  localProviders.add(moduleClass);
240
272
  const isGlobal = metadata.isGlobal || isDynamicModule(moduleClassOrDynamic) && moduleClassOrDynamic.global === true;
273
+ const isLazy = metadata.lazy || isDynamicModule(moduleClassOrDynamic) && moduleClassOrDynamic.lazy === true;
241
274
  this.container.registerScope({
242
275
  moduleId,
243
276
  localProviders,
244
277
  importedModules: importedModuleIds,
245
278
  exportedTokens: new Set(allExports),
246
- isGlobal
279
+ isGlobal,
280
+ lazy: isLazy
247
281
  });
282
+ const lazyTokens = [];
283
+ let hasEntrypointContributor = false;
248
284
  for (const provider of allProviders){
249
- this.registerProvider(provider, moduleId);
285
+ const registered = this.registerProvider(provider, moduleId);
286
+ if (isLazy && registered !== undefined) {
287
+ lazyTokens.push(registered);
288
+ hasEntrypointContributor ||= declaresEntrypointContributor(provider);
289
+ }
250
290
  }
251
291
  for (const controller of allControllers){
252
292
  // Register the controller in its owning module's bucket so its
253
293
  // dependencies resolve from the module's POV (vs `__root__`'s).
254
294
  this.container.register(controller, moduleId);
255
295
  this.collectedControllers.add(controller);
296
+ if (isLazy) lazyTokens.push(controller);
297
+ }
298
+ if (isLazy) {
299
+ this.lazyModuleIds.add(moduleId);
300
+ this.lazyGroups.set(moduleId, {
301
+ moduleId,
302
+ tokens: lazyTokens,
303
+ hasEntrypointContributor
304
+ });
256
305
  }
257
306
  // Propagate module-level Use* decorators (@UseGuards, @UseInterceptors, etc.) to each controller
258
307
  for (const controller of allControllers){
@@ -293,35 +342,36 @@ export class ModuleLoader {
293
342
  console.warn(message);
294
343
  }
295
344
  }
296
- registerProvider(provider, moduleId) {
345
+ /** Registers the provider and returns the token it was registered under. */ registerProvider(provider, moduleId) {
297
346
  if (typeof provider === "function") {
298
347
  // Per-module bucket: the same class can be registered in multiple
299
348
  // modules' buckets simultaneously without collision.
300
349
  this.container.register(provider, moduleId);
301
350
  this.registeredProviders.push(provider);
302
- } else {
303
- const token = provider.provide;
304
- if (!token) {
305
- this.container.register(provider, moduleId);
306
- return;
307
- }
308
- if (this.isAppToken(token)) {
309
- // Multiple APP_* providers within the same module bucket need
310
- // distinct tokens so they don't overwrite each other in the bucket
311
- // Map. Across buckets, `container.resolveAll(APP_GUARD)` walks
312
- // every bucket — no need to mark synthetic tokens global.
313
- const syntheticToken = new InjectionToken(`${token.toString()}:${this.appProviderCounter++}`);
314
- this.container.register({
315
- ...provider,
316
- provide: syntheticToken
317
- }, moduleId);
318
- this.registeredProviders.push(syntheticToken);
319
- getOrCreateArray(this.appProviderTokens, token).push(syntheticToken);
320
- return;
321
- }
351
+ return provider;
352
+ }
353
+ const token = provider.provide;
354
+ if (!token) {
322
355
  this.container.register(provider, moduleId);
323
- this.registeredProviders.push(token);
356
+ return undefined;
324
357
  }
358
+ if (this.isAppToken(token)) {
359
+ // Multiple APP_* providers within the same module bucket need
360
+ // distinct tokens so they don't overwrite each other in the bucket
361
+ // Map. Across buckets, `container.resolveAll(APP_GUARD)` walks
362
+ // every bucket — no need to mark synthetic tokens global.
363
+ const syntheticToken = new InjectionToken(`${token.toString()}:${this.appProviderCounter++}`);
364
+ this.container.register({
365
+ ...provider,
366
+ provide: syntheticToken
367
+ }, moduleId);
368
+ this.registeredProviders.push(syntheticToken);
369
+ getOrCreateArray(this.appProviderTokens, token).push(syntheticToken);
370
+ return syntheticToken;
371
+ }
372
+ this.container.register(provider, moduleId);
373
+ this.registeredProviders.push(token);
374
+ return token;
325
375
  }
326
376
  isAppToken(token) {
327
377
  return APP_TOKENS.has(token);
@@ -364,6 +414,25 @@ export class ModuleLoader {
364
414
  ...this.consumerMiddlewareDefinitions
365
415
  ];
366
416
  }
417
+ /**
418
+ * Every lazy module instance recorded during load. Handed to the
419
+ * LazyModuleManager at bootstrap; empty when no module opted in.
420
+ */ getLazyGroups() {
421
+ return [
422
+ ...this.lazyGroups.values()
423
+ ];
424
+ }
425
+ /**
426
+ * A token is deferred iff EVERY module bucket holding it belongs to a lazy
427
+ * module instance — a token also registered by a non-lazy module stays on
428
+ * the eager pass (and resolving it there claims the lazy sibling, which is
429
+ * the "any use = init" semantic, not a bug).
430
+ */ isLazyOnlyToken(token) {
431
+ if (this.lazyModuleIds.size === 0) return false;
432
+ const owners = this.container.getOwnerModuleIds(token);
433
+ if (owners.length === 0) return false;
434
+ return owners.every((id)=>this.lazyModuleIds.has(id));
435
+ }
367
436
  async resolveAllInstances() {
368
437
  const instanceSet = new Set();
369
438
  for (const token of this.registeredProviders){
@@ -371,6 +440,9 @@ export class ModuleLoader {
371
440
  if (this.container.getProviderScope(token) === Scope.REQUEST) {
372
441
  continue;
373
442
  }
443
+ if (this.isLazyOnlyToken(token)) {
444
+ continue;
445
+ }
374
446
  const instance = await this.container.resolveAsync(token);
375
447
  instanceSet.add(instance);
376
448
  } catch (err) {
@@ -385,6 +457,9 @@ export class ModuleLoader {
385
457
  if (this.container.getProviderScope(controller) === Scope.REQUEST) {
386
458
  continue;
387
459
  }
460
+ if (this.isLazyOnlyToken(controller)) {
461
+ continue;
462
+ }
388
463
  const instance = await this.container.resolveAsync(controller);
389
464
  instanceSet.add(instance);
390
465
  } catch (err) {