@velajs/vela 1.1.0 → 1.3.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.
- package/CHANGELOG.md +40 -0
- package/dist/__tests__/workers/entry.d.ts +9 -0
- package/dist/container/container.d.ts +16 -5
- package/dist/container/container.js +143 -34
- package/dist/container/index.d.ts +2 -2
- package/dist/container/index.js +1 -1
- package/dist/container/types.d.ts +16 -0
- package/dist/container/types.js +14 -0
- package/dist/event-emitter/event-emitter.subscriber.js +6 -1
- package/dist/factory/bootstrap.d.ts +23 -0
- package/dist/factory/bootstrap.js +62 -0
- package/dist/factory.d.ts +2 -2
- package/dist/factory.js +2 -33
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.js +6 -0
- package/dist/module/module-loader.d.ts +4 -0
- package/dist/module/module-loader.js +80 -23
- package/dist/plugin/plugin.d.ts +31 -0
- package/dist/plugin/plugin.js +111 -0
- package/dist/schedule/schedule.registry.js +6 -1
- package/dist/schedule-node/index.d.ts +1 -0
- package/dist/schedule-node/index.js +1 -0
- package/dist/schedule-node/schedule.executor.d.ts +3 -1
- package/dist/schedule-node/schedule.executor.js +21 -5
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
Module boundaries are enforced. NestJS-shape: a service cannot resolve dependencies from a module it didn't import. Bootstrap is consolidated into a single primitive, discovery failures are diagnostically routed, and a generic plugin composer is included.
|
|
6
|
+
|
|
7
|
+
### New
|
|
8
|
+
|
|
9
|
+
- **Module visibility enforcement.** `VelaFactory.create(Mod)` checks every constructor injection: the token must be declared locally, exported by an imported module, marked `@Global`, or be an `InjectionToken` with a default factory. Throws `ModuleVisibilityError` with an actionable message on violation. Auto-registration of unknown class tokens is gone — declare every dependency in a module's `providers`. There is no opt-out flag; `ModuleRef.create()` is the sandbox escape hatch for transient instantiation.
|
|
10
|
+
|
|
11
|
+
- **`bootstrap(rootModule, options)`** — the wiring primitive shared by `VelaFactory.create`, `@velajs/testing`, and any non-HTTP consumer (CLI tools, custom runtimes). Returns `{ container, routeManager, loader }` without running lifecycle hooks or building the Hono app. `VelaFactory.create` is now a thin wrapper that calls `bootstrap()` then runs `OnModuleInit` / `OnApplicationBootstrap` and builds routes. Exported from `@velajs/vela` and `@velajs/vela/internal`.
|
|
12
|
+
|
|
13
|
+
- **Module visibility primitives.** `Container({ diagnostics })`, `ModuleScope`, `Container.registerScope`, `Container.markGlobalToken`, and `ModuleVisibilityError`. `useExisting` aliases honor visibility (alias targets the caller can't see are rejected).
|
|
14
|
+
|
|
15
|
+
- **Self-providing tokens stay visible.** `new InjectionToken('X', { factory: () => Y })` is implicitly globally visible — the token's factory IS its provider, so no explicit declaration is required.
|
|
16
|
+
|
|
17
|
+
- **Factory inject is a framework-level escape hatch.** `useFactory` provider deps (including `forRootAsync`, `registerAsync`) resolve without a module-visibility requester, so factory `inject: [...]` arrays can pull from the importing module's scope. A future `forRootAsync({ imports })` will tighten this; for now it's permissive by design.
|
|
18
|
+
|
|
19
|
+
- **Discovery diagnostics: `{ diagnostics: 'silent' | 'log' | 'throw' }`** (default `'log'`). Failed provider/controller resolution at `loader.resolveAllInstances`, schedule discovery, event-emitter discovery, and runtime job execution route through one dispatcher. `ModuleVisibilityError` always propagates regardless of mode.
|
|
20
|
+
|
|
21
|
+
- **Live Cloudflare Workers smoke tests.** `pnpm test:workers` runs vela inside workerd via `@cloudflare/vitest-pool-workers` (driven by a test-only `wrangler.toml`). Validates the edge-runtime contract end-to-end — boot, request lifecycle, per-request DI without `AsyncLocalStorage`, handler-chain order, OpenAPI mount — beyond what the static edge-audit can catch. Wired into CI alongside `pnpm test`.
|
|
22
|
+
|
|
23
|
+
- **Framework primitives are globally visible.** `Container`, `ModuleRef`, and `APP_GUARD`/`APP_PIPE`/`APP_INTERCEPTOR`/`APP_FILTER`/`APP_MIDDLEWARE` are marked global at boot — resolvable from any module without explicit imports, in both strict and non-strict mode. `ModuleRef.create()` continues to be the sandbox escape hatch (visibility check skipped for transient instantiation).
|
|
24
|
+
|
|
25
|
+
- **Plugin manifest + composer** — `definePlugin({ id, version, module, dependsOn?, metadata? })` and `composePlugins(plugins): DynamicModule` with topological sort, cycle detection, and missing-dep detection. Produces a global module that exposes a queryable `PluginRegistry` via `PLUGIN_REGISTRY_TOKEN` (`list`, `get(id)`, `dependents(id)`).
|
|
26
|
+
|
|
27
|
+
- **New types**: `ModuleScope`, `ContainerOptions`, `BootstrapOptions`, `BootstrapResult`, `Diagnostics`, `Plugin` — exported from both root and `/internal`.
|
|
28
|
+
|
|
29
|
+
### Internal cleanup
|
|
30
|
+
|
|
31
|
+
- **`Container` constructor accepts `ContainerOptions`** (`{ diagnostics? }`). Threads `requestingModuleId` through `resolve` / `resolveAsync` / `resolveAll`. Per-module scopes are tracked via `registerScope`; framework-internal globals via `markGlobalToken`. `providerOrigin: Map<Token, string>` records each provider's declaring module so constructor injections resolve from the *class's* module, not the caller's. Visibility enforcement runs whenever a `requestingModuleId` is supplied — there is no on/off switch.
|
|
32
|
+
|
|
33
|
+
- **`ModuleLoader` registers a `ModuleScope` per module** before recursing into imports — `localProviders` includes the module class itself (so `NestModule.configure()` resolution stays inside its own scope), controllers, and every provider token. Synthetic `APP_*` tokens are marked global at mint time so RouteManager's request-time resolutions (no requester) keep working.
|
|
34
|
+
|
|
35
|
+
- **`createChild()` shares container state by reference** (providers, scopes, globals, providerOrigin); `createDetached()` copies providers + providerOrigin and shares scopes + globals. Re-registering a token on a detached container without a moduleId clears any stale `providerOrigin` so sandbox-local registrations don't inherit a misleading owner.
|
|
36
|
+
|
|
37
|
+
- **`resolveAsync` factory branch now unwraps `ForwardRef` in `inject`** (mirroring the sync `resolveFactory`). Previously asymmetric — sync path worked, async path silently failed during `loader.resolveAllInstances` and was swallowed by the discovery `try/catch`.
|
|
38
|
+
|
|
39
|
+
- **Dropped unused `Container.parent` field** (audit #10). Was assigned in `createChild()` but never read.
|
|
40
|
+
|
|
41
|
+
- **Bootstrap consolidated into `src/factory/bootstrap.ts`** — `VelaFactory.create` no longer hand-rolls the APP_* / consumer-middleware / global-prefix wiring sequence. Net code reduction in `factory.ts`.
|
|
42
|
+
|
|
3
43
|
## 1.1.0 (2026-04-30)
|
|
4
44
|
|
|
5
45
|
Architectural remodel: one metadata model, one storage, public surface trimmed, internal primitives exposed via a stable subpath.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
fetch(request: Request, _env: unknown, _ctx: unknown): Promise<Response>;
|
|
3
|
+
};
|
|
4
|
+
export default _default;
|
|
5
|
+
declare global {
|
|
6
|
+
interface ExportedHandler<Env = unknown> {
|
|
7
|
+
fetch?(request: Request, env: Env, ctx: unknown): Response | Promise<Response>;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { Scope } from '../constants';
|
|
2
|
-
import type { ProviderOptions, Token, Type } from './types';
|
|
2
|
+
import type { ContainerOptions, Diagnostics, ModuleScope, ProviderOptions, Token, Type } from './types';
|
|
3
3
|
export declare class Container {
|
|
4
4
|
private providers;
|
|
5
5
|
private resolutionStack;
|
|
6
|
-
private parent;
|
|
7
6
|
private requestInstances;
|
|
8
|
-
|
|
7
|
+
private scopes;
|
|
8
|
+
private globals;
|
|
9
|
+
private providerOrigin;
|
|
10
|
+
private diagnostics;
|
|
11
|
+
constructor(options?: ContainerOptions);
|
|
12
|
+
register<T>(provider: Type<T> | ProviderOptions<T>, declaringModuleId?: string): this;
|
|
9
13
|
private registerClass;
|
|
10
14
|
private registerOptions;
|
|
11
|
-
|
|
15
|
+
private recordOrigin;
|
|
16
|
+
registerScope(scope: ModuleScope): void;
|
|
17
|
+
markGlobalToken(token: Token): void;
|
|
18
|
+
getDiagnostics(): Diagnostics;
|
|
19
|
+
resolve<T>(token: Token<T>, requestingModuleId?: string): T;
|
|
20
|
+
resolveAll<T>(token: Token<T>, requestingModuleId?: string): T[];
|
|
12
21
|
has(token: Token): boolean;
|
|
13
22
|
getProviderScope(token: Token): Scope | undefined;
|
|
14
23
|
getTokens(): Token[];
|
|
@@ -23,7 +32,9 @@ export declare class Container {
|
|
|
23
32
|
private resolveRegistration;
|
|
24
33
|
private resolveClass;
|
|
25
34
|
private resolveFactory;
|
|
26
|
-
resolveAsync<T>(token: Token<T
|
|
35
|
+
resolveAsync<T>(token: Token<T>, requestingModuleId?: string): Promise<T>;
|
|
27
36
|
private createLazyProxy;
|
|
37
|
+
private assertVisible;
|
|
38
|
+
private isExportedFromImports;
|
|
28
39
|
private tokenToString;
|
|
29
40
|
}
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { Scope } from "../constants.js";
|
|
2
2
|
import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
|
|
3
|
-
import { ForwardRef, InjectionToken } from "./types.js";
|
|
3
|
+
import { ForwardRef, InjectionToken, ModuleVisibilityError } from "./types.js";
|
|
4
4
|
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.';
|
|
5
5
|
export class Container {
|
|
6
6
|
providers = new Map();
|
|
7
7
|
resolutionStack = new Set();
|
|
8
|
-
parent = null;
|
|
9
8
|
requestInstances = new Map();
|
|
10
|
-
|
|
9
|
+
scopes = new Map();
|
|
10
|
+
globals = new Set();
|
|
11
|
+
providerOrigin = new Map();
|
|
12
|
+
diagnostics;
|
|
13
|
+
constructor(options = {}){
|
|
14
|
+
this.diagnostics = options.diagnostics ?? 'log';
|
|
15
|
+
}
|
|
16
|
+
register(provider, declaringModuleId) {
|
|
11
17
|
if (typeof provider === 'function') {
|
|
12
|
-
this.registerClass(provider);
|
|
18
|
+
this.registerClass(provider, declaringModuleId);
|
|
13
19
|
} else {
|
|
14
|
-
this.registerOptions(provider);
|
|
20
|
+
this.registerOptions(provider, declaringModuleId);
|
|
15
21
|
}
|
|
16
22
|
return this;
|
|
17
23
|
}
|
|
18
|
-
registerClass(target) {
|
|
24
|
+
registerClass(target, declaringModuleId) {
|
|
19
25
|
if (!isInjectable(target)) {
|
|
20
26
|
console.warn(`Warning: ${target.name} is not decorated with @Injectable(). ` + `It will be registered but dependency resolution may not work correctly.`);
|
|
21
27
|
}
|
|
@@ -25,8 +31,9 @@ export class Container {
|
|
|
25
31
|
scope,
|
|
26
32
|
useClass: target
|
|
27
33
|
});
|
|
34
|
+
this.recordOrigin(target, declaringModuleId);
|
|
28
35
|
}
|
|
29
|
-
registerOptions(options) {
|
|
36
|
+
registerOptions(options, declaringModuleId) {
|
|
30
37
|
const token = options.provide;
|
|
31
38
|
if (!token) {
|
|
32
39
|
throw new Error('Provider registration requires a token');
|
|
@@ -49,30 +56,72 @@ export class Container {
|
|
|
49
56
|
registration.useClass = token;
|
|
50
57
|
}
|
|
51
58
|
this.providers.set(token, registration);
|
|
59
|
+
this.recordOrigin(token, declaringModuleId);
|
|
60
|
+
// For aliases like `{ provide: Foo, useClass: Bar }`, also record Bar's
|
|
61
|
+
// origin so `resolveClass(Bar)` resolves Bar's deps from the alias's
|
|
62
|
+
// declaring module. Idempotent if Bar === Foo.
|
|
63
|
+
if (options.useClass !== undefined) {
|
|
64
|
+
this.recordOrigin(options.useClass, declaringModuleId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
recordOrigin(token, moduleId) {
|
|
68
|
+
if (moduleId !== undefined) {
|
|
69
|
+
this.providerOrigin.set(token, moduleId);
|
|
70
|
+
} else {
|
|
71
|
+
// Sandbox containers (createDetached) re-register tokens with no
|
|
72
|
+
// moduleId; clearing keeps the token "module-less" so it's only
|
|
73
|
+
// resolvable when the requester is also undefined.
|
|
74
|
+
this.providerOrigin.delete(token);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
registerScope(scope) {
|
|
78
|
+
this.scopes.set(scope.moduleId, scope);
|
|
79
|
+
if (scope.isGlobal) {
|
|
80
|
+
// Match NestJS / the loader's existing globalExports semantic: only
|
|
81
|
+
// exported tokens become globally visible. Non-exported providers of
|
|
82
|
+
// a @Global module still need explicit imports.
|
|
83
|
+
for (const token of scope.exportedTokens)this.globals.add(token);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
markGlobalToken(token) {
|
|
87
|
+
this.globals.add(token);
|
|
88
|
+
}
|
|
89
|
+
getDiagnostics() {
|
|
90
|
+
return this.diagnostics;
|
|
52
91
|
}
|
|
53
|
-
resolve(token) {
|
|
92
|
+
resolve(token, requestingModuleId) {
|
|
93
|
+
if (requestingModuleId !== undefined) {
|
|
94
|
+
this.assertVisible(requestingModuleId, token);
|
|
95
|
+
}
|
|
54
96
|
const registration = this.providers.get(token);
|
|
55
97
|
if (!registration) {
|
|
56
98
|
// `Object`/undefined at a token position is the fingerprint of a
|
|
57
|
-
// type-only import that TypeScript stripped — emit the hint
|
|
58
|
-
//
|
|
99
|
+
// type-only import that TypeScript stripped — emit the hint before
|
|
100
|
+
// attempting any other recovery.
|
|
59
101
|
if (token === Object || token == null) {
|
|
60
102
|
throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + IMPORT_TYPE_HINT);
|
|
61
103
|
}
|
|
62
104
|
if (typeof token === 'function') {
|
|
63
|
-
this.
|
|
64
|
-
return this.resolve(token);
|
|
105
|
+
throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + `Declare it in a module's providers.`);
|
|
65
106
|
}
|
|
107
|
+
// InjectionToken default factories are explicit user declarations
|
|
108
|
+
// attached to the token at construction — self-providing singletons.
|
|
66
109
|
if (token instanceof InjectionToken && token.options?.factory) {
|
|
67
110
|
this.register({
|
|
68
111
|
provide: token,
|
|
69
112
|
useFactory: token.options.factory
|
|
70
113
|
});
|
|
71
|
-
return this.resolve(token);
|
|
114
|
+
return this.resolve(token, requestingModuleId);
|
|
72
115
|
}
|
|
73
116
|
throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
|
|
74
117
|
}
|
|
75
|
-
return this.resolveRegistration(registration);
|
|
118
|
+
return this.resolveRegistration(registration, requestingModuleId);
|
|
119
|
+
}
|
|
120
|
+
resolveAll(token, requestingModuleId) {
|
|
121
|
+
if (!this.has(token)) return [];
|
|
122
|
+
return [
|
|
123
|
+
this.resolve(token, requestingModuleId)
|
|
124
|
+
];
|
|
76
125
|
}
|
|
77
126
|
has(token) {
|
|
78
127
|
return this.providers.has(token);
|
|
@@ -88,27 +137,44 @@ export class Container {
|
|
|
88
137
|
* The child shares the parent's providers but caches REQUEST-scoped
|
|
89
138
|
* instances separately per child (per request).
|
|
90
139
|
*/ createChild() {
|
|
91
|
-
const child = new Container(
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
const child = new Container({
|
|
141
|
+
diagnostics: this.diagnostics
|
|
142
|
+
});
|
|
143
|
+
// Share state by reference — request-scope children must see the same
|
|
144
|
+
// module graph as the root.
|
|
145
|
+
child.providers = this.providers;
|
|
146
|
+
child.scopes = this.scopes;
|
|
147
|
+
child.globals = this.globals;
|
|
148
|
+
child.providerOrigin = this.providerOrigin;
|
|
94
149
|
return child;
|
|
95
150
|
}
|
|
96
151
|
createDetached() {
|
|
97
|
-
const child = new Container(
|
|
98
|
-
|
|
152
|
+
const child = new Container({
|
|
153
|
+
diagnostics: this.diagnostics
|
|
154
|
+
});
|
|
155
|
+
// Copy mutable per-resolution state; share static module-graph metadata
|
|
156
|
+
// so sandbox resolutions can still see exported providers.
|
|
157
|
+
child.providers = new Map(this.providers);
|
|
158
|
+
child.providerOrigin = new Map(this.providerOrigin);
|
|
159
|
+
child.scopes = this.scopes;
|
|
160
|
+
child.globals = this.globals;
|
|
99
161
|
return child;
|
|
100
162
|
}
|
|
101
163
|
clear() {
|
|
102
164
|
this.providers.clear();
|
|
103
165
|
this.resolutionStack.clear();
|
|
104
166
|
this.requestInstances.clear();
|
|
167
|
+
this.scopes.clear();
|
|
168
|
+
this.globals.clear();
|
|
169
|
+
this.providerOrigin.clear();
|
|
105
170
|
}
|
|
106
|
-
resolveRegistration(registration) {
|
|
171
|
+
resolveRegistration(registration, requestingModuleId) {
|
|
107
172
|
if (registration.useValue !== undefined) {
|
|
108
173
|
return registration.useValue;
|
|
109
174
|
}
|
|
110
175
|
if (registration.useExisting) {
|
|
111
|
-
|
|
176
|
+
// Pass through the original requester to catch alias leaks
|
|
177
|
+
return this.resolve(registration.useExisting, requestingModuleId);
|
|
112
178
|
}
|
|
113
179
|
// Singleton: return cached from registration (shared across all containers)
|
|
114
180
|
if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
|
|
@@ -132,6 +198,10 @@ export class Container {
|
|
|
132
198
|
try {
|
|
133
199
|
let instance;
|
|
134
200
|
if (registration.useFactory) {
|
|
201
|
+
// Factories (forRootAsync, useFactory) commonly inject deps from the
|
|
202
|
+
// importing module's scope. Vela has no `forRootAsync({ imports })`
|
|
203
|
+
// surface to track that, so factory inject deps resolve without a
|
|
204
|
+
// requester — same escape-hatch shape as ModuleRef.
|
|
135
205
|
instance = this.resolveFactory(registration);
|
|
136
206
|
} else if (registration.useClass) {
|
|
137
207
|
instance = this.resolveClass(registration.useClass);
|
|
@@ -148,7 +218,9 @@ export class Container {
|
|
|
148
218
|
this.resolutionStack.delete(registration.provide);
|
|
149
219
|
}
|
|
150
220
|
}
|
|
151
|
-
resolveClass(target) {
|
|
221
|
+
resolveClass(target, callerModuleId) {
|
|
222
|
+
// The class's dependencies resolve from ITS module's POV, not the caller's.
|
|
223
|
+
const ownerModuleId = this.providerOrigin.get(target) ?? callerModuleId;
|
|
152
224
|
const paramTypes = getConstructorDependencies(target);
|
|
153
225
|
const injectMetadata = getInjectMetadata(target);
|
|
154
226
|
const injectMap = new Map(injectMetadata.map((m)=>[
|
|
@@ -169,19 +241,19 @@ export class Container {
|
|
|
169
241
|
}
|
|
170
242
|
// forwardRef with circular dep — break the cycle with a lazy Proxy
|
|
171
243
|
if (isForwardRef && this.resolutionStack.has(token)) {
|
|
172
|
-
return this.createLazyProxy(token);
|
|
244
|
+
return this.createLazyProxy(token, ownerModuleId);
|
|
173
245
|
}
|
|
174
|
-
return this.resolve(token);
|
|
246
|
+
return this.resolve(token, ownerModuleId);
|
|
175
247
|
});
|
|
176
248
|
return new target(...dependencies);
|
|
177
249
|
}
|
|
178
|
-
resolveFactory(registration) {
|
|
250
|
+
resolveFactory(registration, requestingModuleId) {
|
|
179
251
|
if (!registration.useFactory) {
|
|
180
252
|
throw new Error('Factory function is missing');
|
|
181
253
|
}
|
|
182
254
|
const dependencies = (registration.inject || []).map((token)=>{
|
|
183
255
|
const resolved = token instanceof ForwardRef ? token.factory() : token;
|
|
184
|
-
return this.resolve(resolved);
|
|
256
|
+
return this.resolve(resolved, requestingModuleId);
|
|
185
257
|
});
|
|
186
258
|
const result = registration.useFactory(...dependencies);
|
|
187
259
|
if (result instanceof Promise) {
|
|
@@ -189,12 +261,22 @@ export class Container {
|
|
|
189
261
|
}
|
|
190
262
|
return result;
|
|
191
263
|
}
|
|
192
|
-
async resolveAsync(token) {
|
|
264
|
+
async resolveAsync(token, requestingModuleId) {
|
|
265
|
+
if (requestingModuleId !== undefined) {
|
|
266
|
+
this.assertVisible(requestingModuleId, token);
|
|
267
|
+
}
|
|
193
268
|
const registration = this.providers.get(token);
|
|
194
269
|
if (!registration) {
|
|
195
270
|
if (typeof token === 'function') {
|
|
196
|
-
this.
|
|
197
|
-
|
|
271
|
+
throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + `Declare it in a module's providers.`);
|
|
272
|
+
}
|
|
273
|
+
if (token instanceof InjectionToken && token.options?.factory) {
|
|
274
|
+
// Self-providing token — register on demand from its declared factory.
|
|
275
|
+
this.register({
|
|
276
|
+
provide: token,
|
|
277
|
+
useFactory: token.options.factory
|
|
278
|
+
});
|
|
279
|
+
return this.resolveAsync(token, requestingModuleId);
|
|
198
280
|
}
|
|
199
281
|
throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
|
|
200
282
|
}
|
|
@@ -202,31 +284,58 @@ export class Container {
|
|
|
202
284
|
if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
|
|
203
285
|
return registration.instance;
|
|
204
286
|
}
|
|
205
|
-
|
|
287
|
+
// Factory inject deps resolve without a requester (escape hatch — see
|
|
288
|
+
// resolveRegistration's useFactory branch).
|
|
289
|
+
const dependencies = await Promise.all((registration.inject || []).map((t)=>{
|
|
290
|
+
const resolved = t instanceof ForwardRef ? t.factory() : t;
|
|
291
|
+
return this.resolveAsync(resolved);
|
|
292
|
+
}));
|
|
206
293
|
const instance = await registration.useFactory(...dependencies);
|
|
207
294
|
if (registration.scope === Scope.SINGLETON) {
|
|
208
295
|
registration.instance = instance;
|
|
209
296
|
}
|
|
210
297
|
return instance;
|
|
211
298
|
}
|
|
212
|
-
return this.resolve(token);
|
|
299
|
+
return this.resolve(token, requestingModuleId);
|
|
213
300
|
}
|
|
214
|
-
createLazyProxy(token) {
|
|
301
|
+
createLazyProxy(token, requestingModuleId) {
|
|
215
302
|
const container = this;
|
|
216
303
|
const target = Object.create(null);
|
|
217
304
|
return new Proxy(target, {
|
|
218
305
|
get (_target, prop) {
|
|
219
|
-
const instance = container.resolve(token);
|
|
306
|
+
const instance = container.resolve(token, requestingModuleId);
|
|
220
307
|
const value = instance[prop];
|
|
221
308
|
return typeof value === 'function' ? value.bind(instance) : value;
|
|
222
309
|
},
|
|
223
310
|
set (_target, prop, value) {
|
|
224
|
-
const instance = container.resolve(token);
|
|
311
|
+
const instance = container.resolve(token, requestingModuleId);
|
|
225
312
|
instance[prop] = value;
|
|
226
313
|
return true;
|
|
227
314
|
}
|
|
228
315
|
});
|
|
229
316
|
}
|
|
317
|
+
assertVisible(moduleId, token) {
|
|
318
|
+
if (this.globals.has(token)) return;
|
|
319
|
+
// InjectionTokens with a default factory are self-providing singletons —
|
|
320
|
+
// visible from any module without requiring explicit declaration.
|
|
321
|
+
if (token instanceof InjectionToken && token.options?.factory) return;
|
|
322
|
+
const scope = this.scopes.get(moduleId);
|
|
323
|
+
if (!scope) {
|
|
324
|
+
// No scope registered for this moduleId — typically synthetic /
|
|
325
|
+
// framework-internal callers. Allow rather than break primitives.
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (scope.localProviders.has(token)) return;
|
|
329
|
+
if (this.isExportedFromImports(scope, token)) return;
|
|
330
|
+
throw new ModuleVisibilityError(moduleId, token);
|
|
331
|
+
}
|
|
332
|
+
isExportedFromImports(scope, token) {
|
|
333
|
+
for (const importedId of scope.importedModules){
|
|
334
|
+
const imported = this.scopes.get(importedId);
|
|
335
|
+
if (imported?.exportedTokens.has(token)) return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
230
339
|
tokenToString(token) {
|
|
231
340
|
if (token instanceof InjectionToken) {
|
|
232
341
|
return token.toString();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Container } from './container';
|
|
2
2
|
export { Injectable, Inject, Optional, isInjectable, getScope } from './decorators';
|
|
3
|
-
export { InjectionToken, ForwardRef, forwardRef } from './types';
|
|
3
|
+
export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError } from './types';
|
|
4
4
|
export { ModuleRef } from './module-ref';
|
|
5
5
|
export { mixin } from './mixin';
|
|
6
|
-
export type { Type, Token, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, } from './types';
|
|
6
|
+
export type { Type, Token, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, ModuleScope, ContainerOptions, Diagnostics, } from './types';
|
package/dist/container/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Container } from "./container.js";
|
|
2
2
|
export { Injectable, Inject, Optional, isInjectable, getScope } from "./decorators.js";
|
|
3
|
-
export { InjectionToken, ForwardRef, forwardRef } from "./types.js";
|
|
3
|
+
export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError } from "./types.js";
|
|
4
4
|
export { ModuleRef } from "./module-ref.js";
|
|
5
5
|
export { mixin } from "./mixin.js";
|
|
@@ -43,3 +43,19 @@ export interface ProviderRegistration<T = unknown> {
|
|
|
43
43
|
inject?: Token[];
|
|
44
44
|
useExisting?: Token<T>;
|
|
45
45
|
}
|
|
46
|
+
export interface ModuleScope {
|
|
47
|
+
moduleId: string;
|
|
48
|
+
localProviders: Set<Token>;
|
|
49
|
+
importedModules: Set<string>;
|
|
50
|
+
exportedTokens: Set<Token>;
|
|
51
|
+
isGlobal: boolean;
|
|
52
|
+
}
|
|
53
|
+
export type Diagnostics = 'silent' | 'log' | 'throw';
|
|
54
|
+
export interface ContainerOptions {
|
|
55
|
+
diagnostics?: Diagnostics;
|
|
56
|
+
}
|
|
57
|
+
export declare class ModuleVisibilityError extends Error {
|
|
58
|
+
readonly moduleId: string;
|
|
59
|
+
readonly token: Token;
|
|
60
|
+
constructor(moduleId: string, token: Token);
|
|
61
|
+
}
|
package/dist/container/types.js
CHANGED
|
@@ -18,3 +18,17 @@ export class ForwardRef {
|
|
|
18
18
|
export function forwardRef(factory) {
|
|
19
19
|
return new ForwardRef(factory);
|
|
20
20
|
}
|
|
21
|
+
function describeToken(token) {
|
|
22
|
+
if (token instanceof InjectionToken) return token.toString();
|
|
23
|
+
if (typeof token === 'function') return token.name;
|
|
24
|
+
if (typeof token === 'symbol') return token.toString();
|
|
25
|
+
return String(token);
|
|
26
|
+
}
|
|
27
|
+
export class ModuleVisibilityError extends Error {
|
|
28
|
+
moduleId;
|
|
29
|
+
token;
|
|
30
|
+
constructor(moduleId, token){
|
|
31
|
+
super(`Module '${moduleId}' cannot resolve '${describeToken(token)}': ` + `not declared in providers, not imported from another module's exports, not @Global. ` + `Either add to imports/exports or mark as @Global.`), this.moduleId = moduleId, this.token = token;
|
|
32
|
+
this.name = 'ModuleVisibilityError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -34,7 +34,12 @@ export class EventEmitterSubscriber {
|
|
|
34
34
|
let instance;
|
|
35
35
|
try {
|
|
36
36
|
instance = this.container.resolve(token);
|
|
37
|
-
} catch
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const mode = this.container.getDiagnostics();
|
|
39
|
+
if (mode === 'throw') throw err;
|
|
40
|
+
if (mode === 'log') {
|
|
41
|
+
console.warn(`[vela] event subscriber discovery: cannot resolve ${token.name || String(token)}:`, err);
|
|
42
|
+
}
|
|
38
43
|
continue;
|
|
39
44
|
}
|
|
40
45
|
for (const { event, methodName } of metadata){
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Container } from '../container/container';
|
|
2
|
+
import type { Diagnostics, Type } from '../container/types';
|
|
3
|
+
import { RouteManager } from '../http/route.manager';
|
|
4
|
+
import type { RouteManagerOptions } from '../http/route.manager';
|
|
5
|
+
import { ModuleLoader } from '../module/module-loader';
|
|
6
|
+
export interface BootstrapOptions extends RouteManagerOptions {
|
|
7
|
+
diagnostics?: Diagnostics;
|
|
8
|
+
}
|
|
9
|
+
export interface BootstrapResult {
|
|
10
|
+
container: Container;
|
|
11
|
+
routeManager: RouteManager;
|
|
12
|
+
loader: ModuleLoader;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wire the DI graph for `rootModule` and prepare the route manager — without
|
|
16
|
+
* running lifecycle hooks or building the Hono app. The single primitive
|
|
17
|
+
* shared by `VelaFactory.create` (HTTP), `@velajs/testing` (test), and any
|
|
18
|
+
* non-HTTP consumer (CLI tools, custom runtimes).
|
|
19
|
+
*
|
|
20
|
+
* Framework-internal tokens (`Container`, `ModuleRef`, `APP_*`) are marked
|
|
21
|
+
* global so they are resolvable from any module.
|
|
22
|
+
*/
|
|
23
|
+
export declare function bootstrap(rootModule: Type, options?: BootstrapOptions): Promise<BootstrapResult>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Container } from "../container/container.js";
|
|
2
|
+
import { ModuleRef } from "../container/module-ref.js";
|
|
3
|
+
import { RouteManager } from "../http/route.manager.js";
|
|
4
|
+
import { ModuleLoader } from "../module/module-loader.js";
|
|
5
|
+
import { bindAppProviders } from "../pipeline/app-providers.js";
|
|
6
|
+
import { ComponentManager } from "../pipeline/component.manager.js";
|
|
7
|
+
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
|
|
8
|
+
/**
|
|
9
|
+
* Wire the DI graph for `rootModule` and prepare the route manager — without
|
|
10
|
+
* running lifecycle hooks or building the Hono app. The single primitive
|
|
11
|
+
* shared by `VelaFactory.create` (HTTP), `@velajs/testing` (test), and any
|
|
12
|
+
* non-HTTP consumer (CLI tools, custom runtimes).
|
|
13
|
+
*
|
|
14
|
+
* Framework-internal tokens (`Container`, `ModuleRef`, `APP_*`) are marked
|
|
15
|
+
* global so they are resolvable from any module.
|
|
16
|
+
*/ export async function bootstrap(rootModule, options = {}) {
|
|
17
|
+
const container = new Container({
|
|
18
|
+
diagnostics: options.diagnostics
|
|
19
|
+
});
|
|
20
|
+
container.register({
|
|
21
|
+
provide: Container,
|
|
22
|
+
useValue: container
|
|
23
|
+
});
|
|
24
|
+
container.markGlobalToken(Container);
|
|
25
|
+
container.register({
|
|
26
|
+
provide: ModuleRef,
|
|
27
|
+
useFactory: (c)=>new ModuleRef(c),
|
|
28
|
+
inject: [
|
|
29
|
+
Container
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
container.markGlobalToken(ModuleRef);
|
|
33
|
+
for (const t of [
|
|
34
|
+
APP_GUARD,
|
|
35
|
+
APP_PIPE,
|
|
36
|
+
APP_INTERCEPTOR,
|
|
37
|
+
APP_FILTER,
|
|
38
|
+
APP_MIDDLEWARE
|
|
39
|
+
]){
|
|
40
|
+
container.markGlobalToken(t);
|
|
41
|
+
}
|
|
42
|
+
const routeManager = new RouteManager(container, options);
|
|
43
|
+
ComponentManager.init(container);
|
|
44
|
+
const loader = new ModuleLoader(container, routeManager);
|
|
45
|
+
loader.load(rootModule);
|
|
46
|
+
bindAppProviders(routeManager, container, loader);
|
|
47
|
+
routeManager.registerConsumerMiddleware(loader.getConsumerMiddlewareDefinitions());
|
|
48
|
+
if (options.globalPrefix) {
|
|
49
|
+
routeManager.setGlobalPrefix(options.globalPrefix);
|
|
50
|
+
}
|
|
51
|
+
for (const handler of options.middleware ?? []){
|
|
52
|
+
const mw = {
|
|
53
|
+
use: handler
|
|
54
|
+
};
|
|
55
|
+
routeManager.useGlobalMiddleware(mw);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
container,
|
|
59
|
+
routeManager,
|
|
60
|
+
loader
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/factory.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VelaApplication } from './application';
|
|
2
2
|
import type { Type } from './container/types';
|
|
3
|
-
import type {
|
|
3
|
+
import type { BootstrapOptions } from './factory/bootstrap';
|
|
4
4
|
export declare const VelaFactory: {
|
|
5
|
-
create(rootModule: Type, options?:
|
|
5
|
+
create(rootModule: Type, options?: BootstrapOptions): Promise<VelaApplication>;
|
|
6
6
|
};
|
package/dist/factory.js
CHANGED
|
@@ -1,39 +1,8 @@
|
|
|
1
1
|
import { VelaApplication } from "./application.js";
|
|
2
|
-
import {
|
|
3
|
-
import { ModuleRef } from "./container/module-ref.js";
|
|
4
|
-
import { bindAppProviders } from "./pipeline/app-providers.js";
|
|
5
|
-
import { RouteManager } from "./http/route.manager.js";
|
|
6
|
-
import { ModuleLoader } from "./module/module-loader.js";
|
|
7
|
-
import { ComponentManager } from "./pipeline/component.manager.js";
|
|
2
|
+
import { bootstrap } from "./factory/bootstrap.js";
|
|
8
3
|
export const VelaFactory = {
|
|
9
4
|
async create (rootModule, options = {}) {
|
|
10
|
-
const container =
|
|
11
|
-
container.register({
|
|
12
|
-
provide: Container,
|
|
13
|
-
useValue: container
|
|
14
|
-
});
|
|
15
|
-
container.register({
|
|
16
|
-
provide: ModuleRef,
|
|
17
|
-
useFactory: (c)=>new ModuleRef(c),
|
|
18
|
-
inject: [
|
|
19
|
-
Container
|
|
20
|
-
]
|
|
21
|
-
});
|
|
22
|
-
const routeManager = new RouteManager(container, options);
|
|
23
|
-
ComponentManager.init(container);
|
|
24
|
-
const loader = new ModuleLoader(container, routeManager);
|
|
25
|
-
loader.load(rootModule);
|
|
26
|
-
bindAppProviders(routeManager, container, loader);
|
|
27
|
-
routeManager.registerConsumerMiddleware(loader.getConsumerMiddlewareDefinitions());
|
|
28
|
-
if (options.globalPrefix) {
|
|
29
|
-
routeManager.setGlobalPrefix(options.globalPrefix);
|
|
30
|
-
}
|
|
31
|
-
for (const handler of options.middleware ?? []){
|
|
32
|
-
const mw = {
|
|
33
|
-
use: handler
|
|
34
|
-
};
|
|
35
|
-
routeManager.useGlobalMiddleware(mw);
|
|
36
|
-
}
|
|
5
|
+
const { container, routeManager, loader } = await bootstrap(rootModule, options);
|
|
37
6
|
const app = new VelaApplication(container, routeManager);
|
|
38
7
|
const instances = await loader.resolveAllInstances();
|
|
39
8
|
app.setInstances(instances);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import './metadata';
|
|
|
2
2
|
export { defineMetadata, getMetadata } from './metadata';
|
|
3
3
|
export { VelaFactory } from './factory';
|
|
4
4
|
export { VelaApplication } from './application';
|
|
5
|
+
export { bootstrap } from './factory/bootstrap';
|
|
6
|
+
export type { BootstrapOptions, BootstrapResult } from './factory/bootstrap';
|
|
5
7
|
export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema, } from './openapi/index';
|
|
6
8
|
export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, } from './openapi/index';
|
|
7
|
-
export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from './container/index';
|
|
8
|
-
export type { Type, Token, InjectableOptions, ProviderOptions, } from './container/index';
|
|
9
|
+
export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, mixin, } from './container/index';
|
|
10
|
+
export type { Type, Token, InjectableOptions, ProviderOptions, ModuleScope, ContainerOptions, Diagnostics, } from './container/index';
|
|
9
11
|
export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
|
|
10
12
|
export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, } from './http/index';
|
|
11
13
|
export { Logger, LogLevel } from './services/index';
|
|
@@ -29,6 +31,8 @@ export type { ThrottlerModuleOptions, ThrottleConfig, ThrottlerStore, ThrottlerS
|
|
|
29
31
|
export { Global, Module, createModuleRef } from './module/index';
|
|
30
32
|
export type { ModuleOptions, DynamicModule, AsyncModuleOptions, ModuleImport } from './module/index';
|
|
31
33
|
export type { MiddlewareConsumer, NestModule, RouteInfo } from './http/index';
|
|
34
|
+
export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN, } from './plugin/plugin';
|
|
35
|
+
export type { Plugin } from './plugin/plugin';
|
|
32
36
|
export { UseMiddleware, UseGuards, UsePipes, UseInterceptors, UseFilters, Catch, SetMetadata, Reflector, APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE, } from './pipeline/index';
|
|
33
37
|
export type { HttpArgumentsHost, ExecutionContext, CanActivate, CallHandler, NestInterceptor, NestMiddleware, PipeTransform, ExceptionFilter, ArgumentMetadata, ReflectableDecorator, CreateDecoratorOptions, } from './pipeline/index';
|
|
34
38
|
export type { Constructor, MiddlewareType, GuardType, PipeType, InterceptorType, FilterType, } from './registry/index';
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,11 @@ export { defineMetadata, getMetadata } from "./metadata.js";
|
|
|
3
3
|
// Factory & Application
|
|
4
4
|
export { VelaFactory } from "./factory.js";
|
|
5
5
|
export { VelaApplication } from "./application.js";
|
|
6
|
+
export { bootstrap } from "./factory/bootstrap.js";
|
|
6
7
|
// OpenAPI
|
|
7
8
|
export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema } from "./openapi/index.js";
|
|
8
9
|
// DI Container
|
|
9
|
-
export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from "./container/index.js";
|
|
10
|
+
export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, mixin } from "./container/index.js";
|
|
10
11
|
// Constants
|
|
11
12
|
export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
|
|
12
13
|
// HTTP Decorators
|
|
@@ -31,6 +32,8 @@ export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthInd
|
|
|
31
32
|
export { ThrottlerModule, ThrottlerGuard, ThrottlerStorage, Throttle, SkipThrottle, THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA } from "./throttler/index.js";
|
|
32
33
|
// Module
|
|
33
34
|
export { Global, Module, createModuleRef } from "./module/index.js";
|
|
35
|
+
// Plugin manifest + composer
|
|
36
|
+
export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN } from "./plugin/plugin.js";
|
|
34
37
|
// Pipeline Decorators
|
|
35
38
|
export { UseMiddleware, UseGuards, UsePipes, UseInterceptors, UseFilters, Catch, SetMetadata, Reflector, APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE } from "./pipeline/index.js";
|
|
36
39
|
// Built-in Pipes
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { Container } from './container/container';
|
|
2
2
|
export { ModuleRef } from './container/module-ref';
|
|
3
|
+
export { ModuleVisibilityError } from './container/types';
|
|
4
|
+
export type { ModuleScope, ContainerOptions, Diagnostics, } from './container/types';
|
|
3
5
|
export { bindAppProviders } from './pipeline/app-providers';
|
|
4
6
|
export { RouteManager } from './http/route.manager';
|
|
5
7
|
export type { RouteManagerOptions } from './http/route.manager';
|
|
@@ -9,3 +11,7 @@ export { VelaApplication } from './application';
|
|
|
9
11
|
export { MetadataRegistry } from './registry/metadata.registry';
|
|
10
12
|
export { APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE, } from './pipeline/tokens';
|
|
11
13
|
export { getModuleMetadata, isModule } from './module/decorators';
|
|
14
|
+
export { bootstrap } from './factory/bootstrap';
|
|
15
|
+
export type { BootstrapOptions, BootstrapResult } from './factory/bootstrap';
|
|
16
|
+
export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN, } from './plugin/plugin';
|
|
17
|
+
export type { Plugin } from './plugin/plugin';
|
package/dist/internal.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// to the same degree as the public surface (./index).
|
|
4
4
|
export { Container } from "./container/container.js";
|
|
5
5
|
export { ModuleRef } from "./container/module-ref.js";
|
|
6
|
+
export { ModuleVisibilityError } from "./container/types.js";
|
|
6
7
|
export { bindAppProviders } from "./pipeline/app-providers.js";
|
|
7
8
|
export { RouteManager } from "./http/route.manager.js";
|
|
8
9
|
export { ModuleLoader } from "./module/module-loader.js";
|
|
@@ -11,3 +12,8 @@ export { VelaApplication } from "./application.js";
|
|
|
11
12
|
export { MetadataRegistry } from "./registry/metadata.registry.js";
|
|
12
13
|
export { APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE } from "./pipeline/tokens.js";
|
|
13
14
|
export { getModuleMetadata, isModule } from "./module/decorators.js";
|
|
15
|
+
// Bootstrap primitive — used by VelaFactory.create, @velajs/testing, and any
|
|
16
|
+
// non-HTTP consumer (CLI tools, custom runtimes).
|
|
17
|
+
export { bootstrap } from "./factory/bootstrap.js";
|
|
18
|
+
// Plugin manifest + composer
|
|
19
|
+
export { definePlugin, composePlugins, PluginRegistry, PluginRootModule, PLUGIN_REGISTRY_TOKEN } from "./plugin/plugin.js";
|
|
@@ -14,8 +14,11 @@ export declare class ModuleLoader {
|
|
|
14
14
|
private consumerMiddlewareDefinitions;
|
|
15
15
|
private appProviderCounter;
|
|
16
16
|
private appProviderTokens;
|
|
17
|
+
private moduleIdByClass;
|
|
18
|
+
private seenModuleIds;
|
|
17
19
|
constructor(container: Container, router: RouteManager);
|
|
18
20
|
load(rootModule: Type): void;
|
|
21
|
+
private getModuleId;
|
|
19
22
|
private processModule;
|
|
20
23
|
private registerProvider;
|
|
21
24
|
private isAppToken;
|
|
@@ -25,4 +28,5 @@ export declare class ModuleLoader {
|
|
|
25
28
|
getAppProviderTokens(token: Token): Token[];
|
|
26
29
|
getConsumerMiddlewareDefinitions(): MiddlewareRouteDefinition[];
|
|
27
30
|
resolveAllInstances(): Promise<unknown[]>;
|
|
31
|
+
private routeError;
|
|
28
32
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Scope } from "../constants.js";
|
|
2
|
-
import { ForwardRef, InjectionToken } from "../container/types.js";
|
|
2
|
+
import { ForwardRef, InjectionToken, ModuleVisibilityError } from "../container/types.js";
|
|
3
3
|
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
|
|
4
4
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
5
5
|
import { getOrCreateArray } from "../registry/util.js";
|
|
@@ -18,6 +18,9 @@ function isDynamicModule(value) {
|
|
|
18
18
|
function implementsNestModule(cls) {
|
|
19
19
|
return typeof cls.prototype?.configure === "function";
|
|
20
20
|
}
|
|
21
|
+
function tokenOfProvider(provider) {
|
|
22
|
+
return typeof provider === "function" ? provider : provider.provide;
|
|
23
|
+
}
|
|
21
24
|
export class ModuleLoader {
|
|
22
25
|
container;
|
|
23
26
|
router;
|
|
@@ -51,6 +54,8 @@ export class ModuleLoader {
|
|
|
51
54
|
[]
|
|
52
55
|
]
|
|
53
56
|
]);
|
|
57
|
+
moduleIdByClass = new Map();
|
|
58
|
+
seenModuleIds = new Set();
|
|
54
59
|
constructor(container, router){
|
|
55
60
|
this.container = container;
|
|
56
61
|
this.router = router;
|
|
@@ -61,6 +66,19 @@ export class ModuleLoader {
|
|
|
61
66
|
this.router.registerController(controller);
|
|
62
67
|
}
|
|
63
68
|
}
|
|
69
|
+
getModuleId(moduleClass) {
|
|
70
|
+
const cached = this.moduleIdByClass.get(moduleClass);
|
|
71
|
+
if (cached) return cached;
|
|
72
|
+
const baseName = moduleClass.name || "AnonModule";
|
|
73
|
+
let id = baseName;
|
|
74
|
+
let counter = 0;
|
|
75
|
+
while(this.seenModuleIds.has(id)){
|
|
76
|
+
id = `${baseName}#${++counter}`;
|
|
77
|
+
}
|
|
78
|
+
this.seenModuleIds.add(id);
|
|
79
|
+
this.moduleIdByClass.set(moduleClass, id);
|
|
80
|
+
return id;
|
|
81
|
+
}
|
|
64
82
|
processModule(moduleClassOrDynamic) {
|
|
65
83
|
// Handle dynamic modules ({ module, imports, controllers, providers })
|
|
66
84
|
let moduleClass;
|
|
@@ -103,8 +121,12 @@ export class ModuleLoader {
|
|
|
103
121
|
throw new Error(`Failed to get module metadata for ${moduleClass.name}`);
|
|
104
122
|
}
|
|
105
123
|
this.processingStack.add(moduleClass);
|
|
124
|
+
const moduleId = this.getModuleId(moduleClass);
|
|
106
125
|
try {
|
|
107
126
|
const importedProviders = new Set(this.globalExports);
|
|
127
|
+
// Determine importedModuleIds eagerly (before recursing) so child
|
|
128
|
+
// modules can be referenced in our scope's importedModules set.
|
|
129
|
+
const importedModuleIds = new Set();
|
|
108
130
|
for (const entry of [
|
|
109
131
|
...metadata.imports,
|
|
110
132
|
...extraImports
|
|
@@ -113,6 +135,7 @@ export class ModuleLoader {
|
|
|
113
135
|
const isForwardRef = entry instanceof ForwardRef;
|
|
114
136
|
const importedModule = isForwardRef ? entry.factory() : entry;
|
|
115
137
|
const importedModuleClass = isDynamicModule(importedModule) ? importedModule.module : importedModule;
|
|
138
|
+
importedModuleIds.add(this.getModuleId(importedModuleClass));
|
|
116
139
|
// If this forwardRef-wrapped import is currently being processed, skip it to
|
|
117
140
|
// break the circular chain. Non-forwardRef circular imports still throw.
|
|
118
141
|
if (isForwardRef && this.processingStack.has(importedModuleClass)) {
|
|
@@ -123,19 +146,42 @@ export class ModuleLoader {
|
|
|
123
146
|
importedProviders.add(token);
|
|
124
147
|
}
|
|
125
148
|
}
|
|
126
|
-
// Register metadata providers + dynamic module extra providers
|
|
127
149
|
const allProviders = [
|
|
128
150
|
...metadata.providers,
|
|
129
151
|
...extraProviders
|
|
130
152
|
];
|
|
131
|
-
for (const provider of allProviders){
|
|
132
|
-
this.registerProvider(provider);
|
|
133
|
-
}
|
|
134
|
-
// Collect metadata controllers + dynamic module extra controllers
|
|
135
153
|
const allControllers = [
|
|
136
154
|
...metadata.controllers,
|
|
137
155
|
...extraControllers
|
|
138
156
|
];
|
|
157
|
+
const allExports = [
|
|
158
|
+
...metadata.exports,
|
|
159
|
+
...extraExports
|
|
160
|
+
];
|
|
161
|
+
// Build the ModuleScope BEFORE registering providers so the visibility
|
|
162
|
+
// check sees the local-provider set as we register.
|
|
163
|
+
const localProviders = new Set();
|
|
164
|
+
for (const provider of allProviders){
|
|
165
|
+
const tk = tokenOfProvider(provider);
|
|
166
|
+
if (tk !== undefined) localProviders.add(tk);
|
|
167
|
+
}
|
|
168
|
+
for (const controller of allControllers){
|
|
169
|
+
localProviders.add(controller);
|
|
170
|
+
}
|
|
171
|
+
// The module class itself can be DI-resolved (NestModule.configure path);
|
|
172
|
+
// include it in its own scope.
|
|
173
|
+
localProviders.add(moduleClass);
|
|
174
|
+
const isGlobal = metadata.isGlobal || isDynamicModule(moduleClassOrDynamic) && moduleClassOrDynamic.global === true;
|
|
175
|
+
this.container.registerScope({
|
|
176
|
+
moduleId,
|
|
177
|
+
localProviders,
|
|
178
|
+
importedModules: importedModuleIds,
|
|
179
|
+
exportedTokens: new Set(allExports),
|
|
180
|
+
isGlobal
|
|
181
|
+
});
|
|
182
|
+
for (const provider of allProviders){
|
|
183
|
+
this.registerProvider(provider, moduleId);
|
|
184
|
+
}
|
|
139
185
|
for (const controller of allControllers){
|
|
140
186
|
this.collectedControllers.add(controller);
|
|
141
187
|
}
|
|
@@ -144,13 +190,8 @@ export class ModuleLoader {
|
|
|
144
190
|
MetadataRegistry.propagateControllerComponents(moduleClass, controller);
|
|
145
191
|
}
|
|
146
192
|
this.processedModules.add(moduleClass);
|
|
147
|
-
const allExports = [
|
|
148
|
-
...metadata.exports,
|
|
149
|
-
...extraExports
|
|
150
|
-
];
|
|
151
193
|
const exports = this.buildExportSet(allExports, allProviders, importedProviders);
|
|
152
194
|
this.moduleExportsCache.set(moduleClass, exports);
|
|
153
|
-
const isGlobal = metadata.isGlobal || isDynamicModule(moduleClassOrDynamic) && moduleClassOrDynamic.global === true;
|
|
154
195
|
if (isGlobal) {
|
|
155
196
|
for (const token of exports){
|
|
156
197
|
this.globalExports.add(token);
|
|
@@ -160,9 +201,9 @@ export class ModuleLoader {
|
|
|
160
201
|
// resolved through the container so it can have its own DI dependencies.
|
|
161
202
|
if (implementsNestModule(moduleClass)) {
|
|
162
203
|
if (!this.container.has(moduleClass)) {
|
|
163
|
-
this.container.register(moduleClass);
|
|
204
|
+
this.container.register(moduleClass, moduleId);
|
|
164
205
|
}
|
|
165
|
-
const instance = this.container.resolve(moduleClass);
|
|
206
|
+
const instance = this.container.resolve(moduleClass, moduleId);
|
|
166
207
|
const builder = new MiddlewareBuilder();
|
|
167
208
|
instance.configure(builder);
|
|
168
209
|
this.consumerMiddlewareDefinitions.push(...builder.getDefinitions());
|
|
@@ -172,16 +213,16 @@ export class ModuleLoader {
|
|
|
172
213
|
this.processingStack.delete(moduleClass);
|
|
173
214
|
}
|
|
174
215
|
}
|
|
175
|
-
registerProvider(provider) {
|
|
216
|
+
registerProvider(provider, moduleId) {
|
|
176
217
|
if (typeof provider === "function") {
|
|
177
218
|
if (!this.container.has(provider)) {
|
|
178
|
-
this.container.register(provider);
|
|
219
|
+
this.container.register(provider, moduleId);
|
|
179
220
|
this.registeredProviders.push(provider);
|
|
180
221
|
}
|
|
181
222
|
} else {
|
|
182
223
|
const token = provider.provide;
|
|
183
224
|
if (!token) {
|
|
184
|
-
this.container.register(provider);
|
|
225
|
+
this.container.register(provider, moduleId);
|
|
185
226
|
return;
|
|
186
227
|
}
|
|
187
228
|
if (this.isAppToken(token)) {
|
|
@@ -189,13 +230,16 @@ export class ModuleLoader {
|
|
|
189
230
|
this.container.register({
|
|
190
231
|
...provider,
|
|
191
232
|
provide: syntheticToken
|
|
192
|
-
});
|
|
233
|
+
}, moduleId);
|
|
234
|
+
// Synthetic APP_* tokens are resolved at request time by RouteManager
|
|
235
|
+
// with no requester; mark them global so the visibility check passes.
|
|
236
|
+
this.container.markGlobalToken(syntheticToken);
|
|
193
237
|
this.registeredProviders.push(syntheticToken);
|
|
194
238
|
getOrCreateArray(this.appProviderTokens, token).push(syntheticToken);
|
|
195
239
|
return;
|
|
196
240
|
}
|
|
197
241
|
if (!this.container.has(token)) {
|
|
198
|
-
this.container.register(provider);
|
|
242
|
+
this.container.register(provider, moduleId);
|
|
199
243
|
this.registeredProviders.push(token);
|
|
200
244
|
}
|
|
201
245
|
}
|
|
@@ -207,7 +251,7 @@ export class ModuleLoader {
|
|
|
207
251
|
const exportSet = new Set();
|
|
208
252
|
const providerTokens = new Set();
|
|
209
253
|
for (const p of providers){
|
|
210
|
-
const token =
|
|
254
|
+
const token = tokenOfProvider(p);
|
|
211
255
|
if (token !== undefined) providerTokens.add(token);
|
|
212
256
|
}
|
|
213
257
|
for (const exported of exports){
|
|
@@ -250,8 +294,11 @@ export class ModuleLoader {
|
|
|
250
294
|
}
|
|
251
295
|
const instance = await this.container.resolveAsync(token);
|
|
252
296
|
instanceSet.add(instance);
|
|
253
|
-
} catch
|
|
254
|
-
|
|
297
|
+
} catch (err) {
|
|
298
|
+
// Module visibility errors must always propagate — they indicate a
|
|
299
|
+
// genuine wiring bug, not something to swallow as a discovery skip.
|
|
300
|
+
if (err instanceof ModuleVisibilityError) throw err;
|
|
301
|
+
this.routeError(err, `resolve provider`);
|
|
255
302
|
}
|
|
256
303
|
}
|
|
257
304
|
for (const controller of this.collectedControllers){
|
|
@@ -261,12 +308,22 @@ export class ModuleLoader {
|
|
|
261
308
|
}
|
|
262
309
|
const instance = await this.container.resolveAsync(controller);
|
|
263
310
|
instanceSet.add(instance);
|
|
264
|
-
} catch
|
|
265
|
-
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (err instanceof ModuleVisibilityError) throw err;
|
|
313
|
+
this.routeError(err, `resolve controller ${controller.name}`);
|
|
266
314
|
}
|
|
267
315
|
}
|
|
268
316
|
return [
|
|
269
317
|
...instanceSet
|
|
270
318
|
];
|
|
271
319
|
}
|
|
320
|
+
routeError(err, context) {
|
|
321
|
+
const mode = this.container.getDiagnostics();
|
|
322
|
+
if (mode === "silent") return;
|
|
323
|
+
if (mode === "throw") {
|
|
324
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
325
|
+
}
|
|
326
|
+
// 'log'
|
|
327
|
+
console.warn(`[vela] ${context} failed:`, err);
|
|
328
|
+
}
|
|
272
329
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { InjectionToken } from '../container/types';
|
|
2
|
+
import type { Type } from '../container/types';
|
|
3
|
+
import type { DynamicModule } from '../registry/types';
|
|
4
|
+
export interface Plugin {
|
|
5
|
+
id: string;
|
|
6
|
+
version: string;
|
|
7
|
+
module: Type | DynamicModule;
|
|
8
|
+
dependsOn?: string[];
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Freeze and validate a plugin manifest. Returns the same object so it can
|
|
13
|
+
* be used in `composePlugins([definePlugin(...), ...])`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function definePlugin(plugin: Plugin): Plugin;
|
|
16
|
+
export declare class PluginRegistry {
|
|
17
|
+
private byId;
|
|
18
|
+
constructor(plugins: readonly Plugin[]);
|
|
19
|
+
list(): Plugin[];
|
|
20
|
+
get(id: string): Plugin | undefined;
|
|
21
|
+
dependents(id: string): Plugin[];
|
|
22
|
+
}
|
|
23
|
+
export declare const PLUGIN_REGISTRY_TOKEN: InjectionToken<PluginRegistry>;
|
|
24
|
+
export declare class PluginRootModule {
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compose a list of plugins into a single dynamic module that imports each
|
|
28
|
+
* plugin's module in dependency order, registers a `PluginRegistry`, and
|
|
29
|
+
* exposes it globally so any plugin can introspect peers.
|
|
30
|
+
*/
|
|
31
|
+
export declare function composePlugins(plugins: readonly Plugin[]): DynamicModule;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { InjectionToken } from "../container/types.js";
|
|
8
|
+
import { Module } from "../module/decorators.js";
|
|
9
|
+
/**
|
|
10
|
+
* Freeze and validate a plugin manifest. Returns the same object so it can
|
|
11
|
+
* be used in `composePlugins([definePlugin(...), ...])`.
|
|
12
|
+
*/ export function definePlugin(plugin) {
|
|
13
|
+
if (!plugin.id) throw new Error('Plugin requires an id');
|
|
14
|
+
if (!plugin.version) throw new Error('Plugin requires a version');
|
|
15
|
+
if (!plugin.module) throw new Error('Plugin requires a module');
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
...plugin
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export class PluginRegistry {
|
|
21
|
+
byId;
|
|
22
|
+
constructor(plugins){
|
|
23
|
+
this.byId = new Map();
|
|
24
|
+
for (const p of plugins){
|
|
25
|
+
if (this.byId.has(p.id)) {
|
|
26
|
+
throw new Error(`Duplicate plugin id: '${p.id}'`);
|
|
27
|
+
}
|
|
28
|
+
this.byId.set(p.id, p);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
list() {
|
|
32
|
+
return [
|
|
33
|
+
...this.byId.values()
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
get(id) {
|
|
37
|
+
return this.byId.get(id);
|
|
38
|
+
}
|
|
39
|
+
dependents(id) {
|
|
40
|
+
return this.list().filter((p)=>p.dependsOn?.includes(id) ?? false);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const PLUGIN_REGISTRY_TOKEN = new InjectionToken('PLUGIN_REGISTRY');
|
|
44
|
+
export class PluginRootModule {
|
|
45
|
+
}
|
|
46
|
+
PluginRootModule = _ts_decorate([
|
|
47
|
+
Module({})
|
|
48
|
+
], PluginRootModule);
|
|
49
|
+
/**
|
|
50
|
+
* Topologically sort plugins so each plugin's dependencies appear first.
|
|
51
|
+
* Throws on missing dependencies and cycles.
|
|
52
|
+
*/ function topologicalSort(plugins) {
|
|
53
|
+
const byId = new Map();
|
|
54
|
+
for (const p of plugins){
|
|
55
|
+
if (byId.has(p.id)) {
|
|
56
|
+
throw new Error(`Duplicate plugin id: '${p.id}'`);
|
|
57
|
+
}
|
|
58
|
+
byId.set(p.id, p);
|
|
59
|
+
}
|
|
60
|
+
const result = [];
|
|
61
|
+
const visiting = new Set();
|
|
62
|
+
const visited = new Set();
|
|
63
|
+
const visit = (p, path)=>{
|
|
64
|
+
if (visited.has(p.id)) return;
|
|
65
|
+
if (visiting.has(p.id)) {
|
|
66
|
+
const cycle = [
|
|
67
|
+
...path,
|
|
68
|
+
p.id
|
|
69
|
+
].join(' -> ');
|
|
70
|
+
throw new Error(`Plugin cycle detected: ${cycle}`);
|
|
71
|
+
}
|
|
72
|
+
visiting.add(p.id);
|
|
73
|
+
for (const depId of p.dependsOn ?? []){
|
|
74
|
+
const dep = byId.get(depId);
|
|
75
|
+
if (!dep) {
|
|
76
|
+
throw new Error(`Plugin '${p.id}' depends on missing plugin '${depId}'`);
|
|
77
|
+
}
|
|
78
|
+
visit(dep, [
|
|
79
|
+
...path,
|
|
80
|
+
p.id
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
visiting.delete(p.id);
|
|
84
|
+
visited.add(p.id);
|
|
85
|
+
result.push(p);
|
|
86
|
+
};
|
|
87
|
+
for (const p of plugins)visit(p, []);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compose a list of plugins into a single dynamic module that imports each
|
|
92
|
+
* plugin's module in dependency order, registers a `PluginRegistry`, and
|
|
93
|
+
* exposes it globally so any plugin can introspect peers.
|
|
94
|
+
*/ export function composePlugins(plugins) {
|
|
95
|
+
const sorted = topologicalSort(plugins);
|
|
96
|
+
const registry = new PluginRegistry(sorted);
|
|
97
|
+
return {
|
|
98
|
+
module: PluginRootModule,
|
|
99
|
+
imports: sorted.map((p)=>p.module),
|
|
100
|
+
providers: [
|
|
101
|
+
{
|
|
102
|
+
provide: PLUGIN_REGISTRY_TOKEN,
|
|
103
|
+
useValue: registry
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
exports: [
|
|
107
|
+
PLUGIN_REGISTRY_TOKEN
|
|
108
|
+
],
|
|
109
|
+
global: true
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -33,7 +33,12 @@ export class ScheduleRegistry {
|
|
|
33
33
|
let instance;
|
|
34
34
|
try {
|
|
35
35
|
instance = this.container.resolve(token);
|
|
36
|
-
} catch
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const mode = this.container.getDiagnostics();
|
|
38
|
+
if (mode === 'throw') throw err;
|
|
39
|
+
if (mode === 'log') {
|
|
40
|
+
console.warn(`[vela] schedule discovery: cannot resolve ${token.name || String(token)}:`, err);
|
|
41
|
+
}
|
|
37
42
|
continue;
|
|
38
43
|
}
|
|
39
44
|
if (cronMeta) {
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { Container } from '../container/container';
|
|
1
2
|
import type { OnApplicationBootstrap, OnModuleDestroy } from '../lifecycle/index';
|
|
2
3
|
import { ScheduleRegistry } from '../schedule/schedule.registry';
|
|
3
4
|
export declare class ScheduleExecutor implements OnApplicationBootstrap, OnModuleDestroy {
|
|
4
5
|
private registry;
|
|
6
|
+
private container;
|
|
5
7
|
private intervalTimers;
|
|
6
8
|
private cronTimers;
|
|
7
9
|
private cronMatcherCache;
|
|
8
10
|
private lastCronMinute;
|
|
9
11
|
private running;
|
|
10
|
-
constructor(registry: ScheduleRegistry);
|
|
12
|
+
constructor(registry: ScheduleRegistry, container: Container);
|
|
11
13
|
onApplicationBootstrap(): void;
|
|
12
14
|
private scheduleInterval;
|
|
13
15
|
private scheduleCron;
|
|
@@ -7,18 +7,26 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
7
7
|
function _ts_metadata(k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { Inject, Injectable } from "../container/index.js";
|
|
16
|
+
import { Container } from "../container/container.js";
|
|
11
17
|
import { parseCron } from "../schedule/cron-matcher.js";
|
|
12
18
|
import { ScheduleRegistry } from "../schedule/schedule.registry.js";
|
|
13
19
|
export class ScheduleExecutor {
|
|
14
20
|
registry;
|
|
21
|
+
container;
|
|
15
22
|
intervalTimers = [];
|
|
16
23
|
cronTimers = [];
|
|
17
24
|
cronMatcherCache = new Map();
|
|
18
25
|
lastCronMinute = new Map();
|
|
19
26
|
running = true;
|
|
20
|
-
constructor(registry){
|
|
27
|
+
constructor(registry, container){
|
|
21
28
|
this.registry = registry;
|
|
29
|
+
this.container = container;
|
|
22
30
|
}
|
|
23
31
|
onApplicationBootstrap() {
|
|
24
32
|
if (typeof setInterval !== 'function') {
|
|
@@ -61,8 +69,14 @@ export class ScheduleExecutor {
|
|
|
61
69
|
if (typeof method === 'function') {
|
|
62
70
|
await method.call(instance);
|
|
63
71
|
}
|
|
64
|
-
} catch
|
|
65
|
-
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Runtime job error — keep the scheduler running by default. Users
|
|
74
|
+
// can opt into rethrowing by setting diagnostics: 'throw'.
|
|
75
|
+
const mode = this.container.getDiagnostics();
|
|
76
|
+
if (mode === 'throw') throw err;
|
|
77
|
+
if (mode === 'log') {
|
|
78
|
+
console.warn(`[vela] scheduled job ${methodName} failed:`, err);
|
|
79
|
+
}
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
getMatcher(expression) {
|
|
@@ -84,8 +98,10 @@ export class ScheduleExecutor {
|
|
|
84
98
|
}
|
|
85
99
|
ScheduleExecutor = _ts_decorate([
|
|
86
100
|
Injectable(),
|
|
101
|
+
_ts_param(1, Inject(Container)),
|
|
87
102
|
_ts_metadata("design:type", Function),
|
|
88
103
|
_ts_metadata("design:paramtypes", [
|
|
89
|
-
typeof ScheduleRegistry === "undefined" ? Object : ScheduleRegistry
|
|
104
|
+
typeof ScheduleRegistry === "undefined" ? Object : ScheduleRegistry,
|
|
105
|
+
typeof Container === "undefined" ? Object : Container
|
|
90
106
|
])
|
|
91
107
|
], ScheduleExecutor);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/vela",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "NestJS-compatible framework for edge runtimes, powered by Hono",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -61,17 +61,21 @@
|
|
|
61
61
|
"hono": "^4"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
+
"@cloudflare/vitest-pool-workers": "^0.15.2",
|
|
64
65
|
"@swc/cli": "^0.8.0",
|
|
65
66
|
"@swc/core": "^1.15.11",
|
|
66
67
|
"@types/bun": "latest",
|
|
68
|
+
"@vitest/runner": "^4.1.5",
|
|
69
|
+
"@vitest/snapshot": "^4.1.5",
|
|
67
70
|
"typescript": "^5",
|
|
68
71
|
"unplugin-swc": "^1.5.9",
|
|
69
|
-
"vitest": "^4.
|
|
72
|
+
"vitest": "^4.1.5",
|
|
70
73
|
"zod": "^4.3.6"
|
|
71
74
|
},
|
|
72
75
|
"scripts": {
|
|
73
76
|
"build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
|
|
74
77
|
"test": "vitest run",
|
|
78
|
+
"test:workers": "vitest run --config vitest.config.workers.ts",
|
|
75
79
|
"typecheck": "tsc --noEmit"
|
|
76
80
|
}
|
|
77
81
|
}
|