alloy-di 1.3.0 → 1.5.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/README.md +1 -1
- package/dist/lib/container.d.ts +98 -7
- package/dist/lib/container.js +210 -27
- package/dist/lib/decorators.d.ts +1 -1
- package/dist/lib/providers.d.ts +27 -3
- package/dist/lib/providers.js +23 -1
- package/dist/lib/scope.d.ts +38 -2
- package/dist/plugins/core/codegen.js +40 -4
- package/dist/plugins/core/container-loader.js +21 -7
- package/dist/plugins/core/decorators.js +12 -22
- package/dist/plugins/core/discovery-runtime.js +19 -4
- package/dist/plugins/core/discovery-store.js +20 -6
- package/dist/plugins/core/scanner.js +71 -7
- package/dist/plugins/core/scopes-validation.d.ts +19 -0
- package/dist/plugins/core/scopes-validation.js +225 -0
- package/dist/plugins/core/types.d.ts +9 -4
- package/dist/plugins/core/visualizer.d.ts +2 -2
- package/dist/plugins/core/visualizer.js +106 -12
- package/dist/plugins/rollup-plugin/barrel-exports.js +51 -0
- package/dist/plugins/rollup-plugin/dependency-resolution.js +42 -0
- package/dist/plugins/rollup-plugin/index.d.ts +2 -23
- package/dist/plugins/rollup-plugin/index.js +15 -128
- package/dist/plugins/rollup-plugin/manifest-deps.js +48 -0
- package/dist/plugins/rollup-plugin/package-exports.js +20 -0
- package/dist/plugins/rollup-plugin/types.d.ts +36 -0
- package/dist/plugins/vite-plugin/index.d.ts +9 -1
- package/dist/plugins/vite-plugin/index.js +14 -5
- package/dist/runtime.d.ts +4 -3
- package/dist/runtime.js +3 -2
- package/dist/scopes.d.ts +73 -0
- package/dist/scopes.js +167 -0
- package/dist/test.d.ts +37 -3
- package/dist/test.js +20 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# alloy-di
|
|
2
2
|
|
|
3
|
-
`alloy-di` is a
|
|
3
|
+
`alloy-di` is a build-time dependency injection toolkit for Vite. It scans your TypeScript during build, generates a static container, and ships a tiny runtime so you get dependency injection without reflection overhead.
|
|
4
4
|
|
|
5
5
|
## Highlights
|
|
6
6
|
|
package/dist/lib/container.d.ts
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
import { Constructor, Newable, Token } from "./types.js";
|
|
2
|
+
import { FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope } from "./scope.js";
|
|
2
3
|
import { ServiceIdentifier } from "./service-identifiers.js";
|
|
3
4
|
|
|
4
5
|
//#region src/lib/container.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The container-like surface a factory receives. Both the root `Container` and
|
|
8
|
+
* a `Scope` satisfy it, so a factory can resolve its own dependencies against
|
|
9
|
+
* whichever context owns its lifecycle (root for `singleton`, the scope for a
|
|
10
|
+
* custom-scoped factory).
|
|
11
|
+
*/
|
|
12
|
+
interface FactoryContext {
|
|
13
|
+
get<T>(target: Newable<T> | ServiceIdentifier<T>): Promise<T>;
|
|
14
|
+
getToken<T>(token: Token<T>): T;
|
|
15
|
+
}
|
|
16
|
+
/** A token-bound factory function executed at resolution time. */
|
|
17
|
+
type FactoryFn<T = unknown> = (context: FactoryContext) => T | Promise<T>;
|
|
5
18
|
/**
|
|
6
19
|
* Runtime dependency injection container used by generated modules and tests.
|
|
7
20
|
*
|
|
8
21
|
* It stores metadata discovered at build time, resolves constructor dependencies,
|
|
9
22
|
* performs singleton caching, and supports token-based value providers.
|
|
10
23
|
*/
|
|
11
|
-
declare class Container {
|
|
24
|
+
declare class Container implements ResolutionContext {
|
|
12
25
|
private readonly singletons;
|
|
13
26
|
private readonly pendingSingletons;
|
|
14
27
|
private readonly instanceOverrides;
|
|
15
28
|
private readonly metadataCache;
|
|
16
29
|
private readonly valueProviders;
|
|
30
|
+
private readonly factoryRegistry;
|
|
31
|
+
private readonly factoryValues;
|
|
32
|
+
private readonly factoryPending;
|
|
33
|
+
private factoryGeneration;
|
|
17
34
|
private readonly factoryWarningCache;
|
|
35
|
+
private readonly scopeHierarchy;
|
|
36
|
+
readonly scopeName: ServiceScope;
|
|
37
|
+
readonly parent: ResolutionContext | null;
|
|
38
|
+
getCached(target: Constructor): unknown;
|
|
39
|
+
setCached(target: Constructor, instance: unknown): void;
|
|
40
|
+
getPending(target: Constructor): Promise<unknown> | undefined;
|
|
41
|
+
setPending(target: Constructor, promise: Promise<unknown>): void;
|
|
42
|
+
deletePending(target: Constructor): void;
|
|
43
|
+
getProvider(tokenId: symbol): unknown;
|
|
44
|
+
hasProvider(tokenId: symbol): boolean;
|
|
45
|
+
getFactoryValue(tokenId: symbol): FactoryCacheEntry | undefined;
|
|
46
|
+
setFactoryValue(tokenId: symbol, generation: number, value: unknown): void;
|
|
47
|
+
getFactoryPending(tokenId: symbol): FactoryPendingEntry | undefined;
|
|
48
|
+
setFactoryPending(tokenId: symbol, generation: number, promise: Promise<unknown>): void;
|
|
49
|
+
deleteFactoryPending(tokenId: symbol, generation: number): void;
|
|
18
50
|
/**
|
|
19
51
|
* Resolve (and construct) the requested service.
|
|
20
52
|
*
|
|
@@ -23,6 +55,14 @@ declare class Container {
|
|
|
23
55
|
*/
|
|
24
56
|
get<T>(target: Newable<T>): Promise<T>;
|
|
25
57
|
get<T>(identifier: ServiceIdentifier<T>): Promise<T>;
|
|
58
|
+
/** @internal */
|
|
59
|
+
_registerScopeHierarchy(hierarchy: Record<string, string>): void;
|
|
60
|
+
/** @internal */
|
|
61
|
+
_validateScopeParent(childScope: ServiceScope, parentScope: ServiceScope): void;
|
|
62
|
+
/** @internal */
|
|
63
|
+
_resolveInContext<T>(target: Newable<T> | ServiceIdentifier<T>, context: ResolutionContext, options?: {
|
|
64
|
+
skipFactoryWarning?: boolean;
|
|
65
|
+
}): Promise<T>;
|
|
26
66
|
/**
|
|
27
67
|
* Provide a concrete instance override for a class constructor.
|
|
28
68
|
* Used by test utilities to inject mocks/stubs without altering global metadata.
|
|
@@ -45,6 +85,23 @@ declare class Container {
|
|
|
45
85
|
* @param value - The value that should be injected when the token is requested.
|
|
46
86
|
*/
|
|
47
87
|
provideValue<T>(token: Token<T>, value: T): void;
|
|
88
|
+
/**
|
|
89
|
+
* Register a factory function for an injection token. The factory runs at
|
|
90
|
+
* resolution time and receives the container so it can resolve its own
|
|
91
|
+
* dependencies.
|
|
92
|
+
*
|
|
93
|
+
* This is the imperative escape hatch backing the declarative `asFactory`
|
|
94
|
+
* provider; prefer `asFactory` + `applyProviders` for static registration.
|
|
95
|
+
*
|
|
96
|
+
* @param token - The token created via `createToken`.
|
|
97
|
+
* @param fn - Factory invoked with the resolving context; may be async.
|
|
98
|
+
* @param options.lifecycle - `singleton` (default, cached on the root),
|
|
99
|
+
* `transient` (re-run on every resolution), or a custom scope name (cached
|
|
100
|
+
* once per matching scope instance and disposed with it).
|
|
101
|
+
*/
|
|
102
|
+
provideFactory<T>(token: Token<T>, fn: FactoryFn<T>, options?: {
|
|
103
|
+
lifecycle?: ServiceScope;
|
|
104
|
+
}): void;
|
|
48
105
|
/**
|
|
49
106
|
* Retrieve a provided value for a token from this container.
|
|
50
107
|
* Throws if no provider is registered for the token.
|
|
@@ -62,10 +119,8 @@ declare class Container {
|
|
|
62
119
|
* @throws Error if a circular dependency is detected
|
|
63
120
|
*/
|
|
64
121
|
private resolve;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*/
|
|
68
|
-
private resolveSingleton;
|
|
122
|
+
private findContextForScope;
|
|
123
|
+
private resolveCached;
|
|
69
124
|
/**
|
|
70
125
|
* Instantiate a class by resolving and injecting all declared dependencies.
|
|
71
126
|
* Handles factory-lazy services by importing the real class before instantiation.
|
|
@@ -108,9 +163,45 @@ declare class Container {
|
|
|
108
163
|
*/
|
|
109
164
|
private runImporterWithRetry;
|
|
110
165
|
/**
|
|
111
|
-
* Resolve a token dependency via registered value providers.
|
|
166
|
+
* Resolve a token dependency via registered value providers or factories.
|
|
112
167
|
*/
|
|
113
168
|
private resolveTokenLike;
|
|
169
|
+
/**
|
|
170
|
+
* Execute a token-bound factory according to its lifecycle, caching its
|
|
171
|
+
* result on the context that owns that lifecycle:
|
|
172
|
+
*
|
|
173
|
+
* - `transient`: run `fn` on every call against the current context; nothing
|
|
174
|
+
* is cached.
|
|
175
|
+
* - `singleton`: cache on the root container.
|
|
176
|
+
* - custom scope: cache on the nearest ancestor context whose `scopeName`
|
|
177
|
+
* matches; if none is active, fall back to transient behaviour (mirrors how
|
|
178
|
+
* class resolution treats a custom scope with no matching context).
|
|
179
|
+
*/
|
|
180
|
+
private resolveFactory;
|
|
181
|
+
/**
|
|
182
|
+
* Resolve a factory whose result is cached on `targetCtx`, keyed by token id.
|
|
183
|
+
* Mirrors `resolveCached` for classes: return the cached value, else coalesce
|
|
184
|
+
* concurrent first-resolutions onto a single `pending` promise, else run the
|
|
185
|
+
* factory and cache. `pending` is cleared in `finally` so a rejected factory
|
|
186
|
+
* can be retried rather than caching the failure.
|
|
187
|
+
*/
|
|
188
|
+
private resolveFactoryCached;
|
|
189
|
+
/**
|
|
190
|
+
* Invoke a factory's function against the context that owns its lifecycle,
|
|
191
|
+
* with a best-effort synchronous re-entrancy guard.
|
|
192
|
+
*
|
|
193
|
+
* `executing` is set only for the synchronous duration of `fn(...)` — long
|
|
194
|
+
* enough to catch a factory that resolves its own token before returning (a
|
|
195
|
+
* synchronous self-cycle, or a synchronous mutual cycle between factories),
|
|
196
|
+
* but cleared the moment `fn` returns so that concurrent resolutions which
|
|
197
|
+
* legitimately coalesce on an in-flight result are never mistaken for a cycle.
|
|
198
|
+
*
|
|
199
|
+
* Known limitation: a factory that re-enters its own token *after* awaiting
|
|
200
|
+
* (an async cross-await self-reference) is indistinguishable from valid
|
|
201
|
+
* coalescing without async-context tracking, so that case is not caught here
|
|
202
|
+
* — it is documented in the factory-providers guide.
|
|
203
|
+
*/
|
|
204
|
+
private runFactory;
|
|
114
205
|
/**
|
|
115
206
|
* Format a readable representation of the resolution stack for error messages.
|
|
116
207
|
*/
|
|
@@ -121,4 +212,4 @@ declare class Container {
|
|
|
121
212
|
private getServiceMetadata;
|
|
122
213
|
}
|
|
123
214
|
//#endregion
|
|
124
|
-
export { Container };
|
|
215
|
+
export { Container, FactoryFn };
|
package/dist/lib/container.js
CHANGED
|
@@ -42,10 +42,77 @@ var Container = class {
|
|
|
42
42
|
instanceOverrides = /* @__PURE__ */ new Map();
|
|
43
43
|
metadataCache = /* @__PURE__ */ new Map();
|
|
44
44
|
valueProviders = /* @__PURE__ */ new Map();
|
|
45
|
+
factoryRegistry = /* @__PURE__ */ new Map();
|
|
46
|
+
factoryValues = /* @__PURE__ */ new Map();
|
|
47
|
+
factoryPending = /* @__PURE__ */ new Map();
|
|
48
|
+
factoryGeneration = 0;
|
|
45
49
|
factoryWarningCache = /* @__PURE__ */ new WeakSet();
|
|
50
|
+
scopeHierarchy = /* @__PURE__ */ new Map();
|
|
51
|
+
scopeName = ServiceScope.SINGLETON;
|
|
52
|
+
parent = null;
|
|
53
|
+
getCached(target) {
|
|
54
|
+
return this.singletons.get(target);
|
|
55
|
+
}
|
|
56
|
+
setCached(target, instance) {
|
|
57
|
+
this.singletons.set(target, instance);
|
|
58
|
+
}
|
|
59
|
+
getPending(target) {
|
|
60
|
+
return this.pendingSingletons.get(target);
|
|
61
|
+
}
|
|
62
|
+
setPending(target, promise) {
|
|
63
|
+
this.pendingSingletons.set(target, promise);
|
|
64
|
+
}
|
|
65
|
+
deletePending(target) {
|
|
66
|
+
this.pendingSingletons.delete(target);
|
|
67
|
+
}
|
|
68
|
+
getProvider(tokenId) {
|
|
69
|
+
return this.valueProviders.get(tokenId);
|
|
70
|
+
}
|
|
71
|
+
hasProvider(tokenId) {
|
|
72
|
+
return this.valueProviders.has(tokenId);
|
|
73
|
+
}
|
|
74
|
+
getFactoryValue(tokenId) {
|
|
75
|
+
return this.factoryValues.get(tokenId);
|
|
76
|
+
}
|
|
77
|
+
setFactoryValue(tokenId, generation, value) {
|
|
78
|
+
this.factoryValues.set(tokenId, {
|
|
79
|
+
generation,
|
|
80
|
+
value
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
getFactoryPending(tokenId) {
|
|
84
|
+
return this.factoryPending.get(tokenId);
|
|
85
|
+
}
|
|
86
|
+
setFactoryPending(tokenId, generation, promise) {
|
|
87
|
+
this.factoryPending.set(tokenId, {
|
|
88
|
+
generation,
|
|
89
|
+
promise
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
deleteFactoryPending(tokenId, generation) {
|
|
93
|
+
if (this.factoryPending.get(tokenId)?.generation === generation) this.factoryPending.delete(tokenId);
|
|
94
|
+
}
|
|
46
95
|
async get(targetOrIdentifier) {
|
|
47
96
|
if (typeof targetOrIdentifier === "symbol") return this.getByIdentifier(targetOrIdentifier);
|
|
48
|
-
return this.getByConstructor(targetOrIdentifier);
|
|
97
|
+
return this.getByConstructor(targetOrIdentifier, this);
|
|
98
|
+
}
|
|
99
|
+
/** @internal */
|
|
100
|
+
_registerScopeHierarchy(hierarchy) {
|
|
101
|
+
for (const [child, parent] of Object.entries(hierarchy)) this.scopeHierarchy.set(child, parent);
|
|
102
|
+
}
|
|
103
|
+
/** @internal */
|
|
104
|
+
_validateScopeParent(childScope, parentScope) {
|
|
105
|
+
const declaredParent = this.scopeHierarchy.get(childScope);
|
|
106
|
+
if (declaredParent && declaredParent !== parentScope) throw new Error(`[alloy] Invalid scope hierarchy construction: scope '${String(childScope)}' is declared with parent '${String(declaredParent)}', but was constructed with parent scope '${String(parentScope)}'.`);
|
|
107
|
+
}
|
|
108
|
+
/** @internal */
|
|
109
|
+
async _resolveInContext(target, context, options) {
|
|
110
|
+
if (typeof target === "symbol") {
|
|
111
|
+
const ctor = getConstructorByIdentifier(target);
|
|
112
|
+
if (!ctor) throw new Error(`No service registered for identifier ${target.description ?? target.toString()}`);
|
|
113
|
+
return this.getByConstructor(ctor, context, { skipFactoryWarning: true });
|
|
114
|
+
}
|
|
115
|
+
return this.getByConstructor(target, context, options);
|
|
49
116
|
}
|
|
50
117
|
/**
|
|
51
118
|
* Provide a concrete instance override for a class constructor.
|
|
@@ -69,7 +136,7 @@ var Container = class {
|
|
|
69
136
|
async getByIdentifier(identifier) {
|
|
70
137
|
const ctor = getConstructorByIdentifier(identifier);
|
|
71
138
|
if (!ctor) throw new Error(`No service registered for identifier ${identifier.description ?? identifier.toString()}`);
|
|
72
|
-
return this.getByConstructor(ctor, { skipFactoryWarning: true });
|
|
139
|
+
return this.getByConstructor(ctor, this, { skipFactoryWarning: true });
|
|
73
140
|
}
|
|
74
141
|
/**
|
|
75
142
|
* Register a concrete value for an injection token at runtime.
|
|
@@ -81,16 +148,43 @@ var Container = class {
|
|
|
81
148
|
this.valueProviders.set(token.id, value);
|
|
82
149
|
}
|
|
83
150
|
/**
|
|
151
|
+
* Register a factory function for an injection token. The factory runs at
|
|
152
|
+
* resolution time and receives the container so it can resolve its own
|
|
153
|
+
* dependencies.
|
|
154
|
+
*
|
|
155
|
+
* This is the imperative escape hatch backing the declarative `asFactory`
|
|
156
|
+
* provider; prefer `asFactory` + `applyProviders` for static registration.
|
|
157
|
+
*
|
|
158
|
+
* @param token - The token created via `createToken`.
|
|
159
|
+
* @param fn - Factory invoked with the resolving context; may be async.
|
|
160
|
+
* @param options.lifecycle - `singleton` (default, cached on the root),
|
|
161
|
+
* `transient` (re-run on every resolution), or a custom scope name (cached
|
|
162
|
+
* once per matching scope instance and disposed with it).
|
|
163
|
+
*/
|
|
164
|
+
provideFactory(token, fn, options) {
|
|
165
|
+
this.factoryRegistry.set(token.id, {
|
|
166
|
+
fn,
|
|
167
|
+
lifecycle: options?.lifecycle ?? ServiceScope.SINGLETON,
|
|
168
|
+
description: token.description,
|
|
169
|
+
generation: ++this.factoryGeneration,
|
|
170
|
+
executing: false
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
84
174
|
* Retrieve a provided value for a token from this container.
|
|
85
175
|
* Throws if no provider is registered for the token.
|
|
86
176
|
*/
|
|
87
177
|
getToken(token) {
|
|
88
|
-
if (!this.valueProviders.has(token.id))
|
|
178
|
+
if (!this.valueProviders.has(token.id)) {
|
|
179
|
+
const label = token.description ?? String(token.id);
|
|
180
|
+
if (this.factoryRegistry.has(token.id)) throw new Error(`Token ${label} is registered as a factory provider, which cannot be retrieved synchronously via getToken(). Declare it as a dependency so it resolves through the async resolution path instead.`);
|
|
181
|
+
throw new Error(`No provider registered for token ${label}`);
|
|
182
|
+
}
|
|
89
183
|
return this.valueProviders.get(token.id);
|
|
90
184
|
}
|
|
91
|
-
async getByConstructor(target, options) {
|
|
185
|
+
async getByConstructor(target, context, options) {
|
|
92
186
|
if (!options?.skipFactoryWarning) this.maybeWarnFactoryLazyConstructorUsage(target);
|
|
93
|
-
return this.resolve(target, []);
|
|
187
|
+
return this.resolve(target, [], context);
|
|
94
188
|
}
|
|
95
189
|
maybeWarnFactoryLazyConstructorUsage(target) {
|
|
96
190
|
if (!isDevEnvironment()) return;
|
|
@@ -107,7 +201,7 @@ var Container = class {
|
|
|
107
201
|
* @returns Promise resolving to the service instance
|
|
108
202
|
* @throws Error if a circular dependency is detected
|
|
109
203
|
*/
|
|
110
|
-
async resolve(target, resolutionStack) {
|
|
204
|
+
async resolve(target, resolutionStack, context) {
|
|
111
205
|
const overridden = this.instanceOverrides.get(target);
|
|
112
206
|
if (overridden) return overridden;
|
|
113
207
|
if (resolutionStack.includes(target)) throw new DependencyResolutionError(`Circular dependency detected: ${[...resolutionStack.map((t) => t.name), target.name].join(" -> ")}`, {
|
|
@@ -117,26 +211,40 @@ var Container = class {
|
|
|
117
211
|
});
|
|
118
212
|
const metadata = this.getServiceMetadata(target);
|
|
119
213
|
const nextStack = [...resolutionStack, target];
|
|
120
|
-
|
|
121
|
-
return this.
|
|
214
|
+
const targetCtx = this.findContextForScope(metadata.scope, context);
|
|
215
|
+
if (metadata.scope === ServiceScope.SINGLETON) return this.resolveCached(target, metadata, nextStack, targetCtx);
|
|
216
|
+
if (metadata.scope === ServiceScope.TRANSIENT) return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory, targetCtx);
|
|
217
|
+
if (targetCtx.scopeName === metadata.scope) return this.resolveCached(target, metadata, nextStack, targetCtx);
|
|
218
|
+
return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory, targetCtx);
|
|
122
219
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
220
|
+
findContextForScope(scope, startingContext) {
|
|
221
|
+
if (scope === ServiceScope.SINGLETON) {
|
|
222
|
+
let current = startingContext;
|
|
223
|
+
while (current.parent) current = current.parent;
|
|
224
|
+
return current;
|
|
225
|
+
}
|
|
226
|
+
if (scope === ServiceScope.TRANSIENT) return startingContext;
|
|
227
|
+
let current = startingContext;
|
|
228
|
+
while (current) {
|
|
229
|
+
if (current.scopeName === scope) return current;
|
|
230
|
+
current = current.parent;
|
|
231
|
+
}
|
|
232
|
+
return startingContext;
|
|
233
|
+
}
|
|
234
|
+
async resolveCached(target, metadata, resolutionStack, targetCtx) {
|
|
235
|
+
const cached = targetCtx.getCached(target);
|
|
128
236
|
if (cached) return cached;
|
|
129
|
-
const pending =
|
|
237
|
+
const pending = targetCtx.getPending(target);
|
|
130
238
|
if (pending) return await pending;
|
|
131
|
-
const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory).then((instance) => {
|
|
132
|
-
|
|
239
|
+
const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory, targetCtx).then((instance) => {
|
|
240
|
+
targetCtx.setCached(target, instance);
|
|
133
241
|
return instance;
|
|
134
242
|
});
|
|
135
|
-
|
|
243
|
+
targetCtx.setPending(target, creation);
|
|
136
244
|
try {
|
|
137
245
|
return await creation;
|
|
138
246
|
} finally {
|
|
139
|
-
|
|
247
|
+
targetCtx.deletePending(target);
|
|
140
248
|
}
|
|
141
249
|
}
|
|
142
250
|
/**
|
|
@@ -149,9 +257,9 @@ var Container = class {
|
|
|
149
257
|
* @param factory - Optional lazy factory to import the real class
|
|
150
258
|
* @returns Promise resolving to the instantiated service
|
|
151
259
|
*/
|
|
152
|
-
async createInstance(target, dependencies, resolutionStack, factory) {
|
|
260
|
+
async createInstance(target, dependencies, resolutionStack, factory, context) {
|
|
153
261
|
const ctor = factory ? await this.importWithRetry(factory, target, resolutionStack) : target;
|
|
154
|
-
return new ctor(...await Promise.all(dependencies.map((param) => this.resolveParam(param, ctor, resolutionStack))));
|
|
262
|
+
return new ctor(...await Promise.all(dependencies.map((param) => this.resolveParam(param, ctor, resolutionStack, context))));
|
|
155
263
|
}
|
|
156
264
|
/**
|
|
157
265
|
* Resolve a single dependency entry, handling lazies, tokens, and constructors.
|
|
@@ -163,7 +271,7 @@ var Container = class {
|
|
|
163
271
|
* @returns Promise resolving to the dependency instance
|
|
164
272
|
* @throws Error if dependency type is invalid
|
|
165
273
|
*/
|
|
166
|
-
async resolveParam(param, target, resolutionStack) {
|
|
274
|
+
async resolveParam(param, target, resolutionStack, context) {
|
|
167
275
|
const classification = classifyDependency(param);
|
|
168
276
|
if (!classification) {
|
|
169
277
|
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
@@ -176,10 +284,10 @@ var Container = class {
|
|
|
176
284
|
switch (classification.kind) {
|
|
177
285
|
case "lazy": {
|
|
178
286
|
const depClass = await this.importWithRetry(classification.lazy, target, resolutionStack);
|
|
179
|
-
return this.resolve(depClass, resolutionStack);
|
|
287
|
+
return this.resolve(depClass, resolutionStack, context);
|
|
180
288
|
}
|
|
181
|
-
case "token": return this.resolveTokenLike(classification.token, target, resolutionStack);
|
|
182
|
-
case "constructor": return this.resolve(classification.ctor, resolutionStack);
|
|
289
|
+
case "token": return this.resolveTokenLike(classification.token, target, resolutionStack, context);
|
|
290
|
+
case "constructor": return this.resolve(classification.ctor, resolutionStack, context);
|
|
183
291
|
}
|
|
184
292
|
return classification;
|
|
185
293
|
}
|
|
@@ -236,10 +344,16 @@ var Container = class {
|
|
|
236
344
|
}
|
|
237
345
|
}
|
|
238
346
|
/**
|
|
239
|
-
* Resolve a token dependency via registered value providers.
|
|
347
|
+
* Resolve a token dependency via registered value providers or factories.
|
|
240
348
|
*/
|
|
241
|
-
resolveTokenLike(tok, target, resolutionStack) {
|
|
242
|
-
|
|
349
|
+
resolveTokenLike(tok, target, resolutionStack, context) {
|
|
350
|
+
let current = context;
|
|
351
|
+
while (current) {
|
|
352
|
+
if (current.hasProvider(tok.id)) return current.getProvider(tok.id);
|
|
353
|
+
current = current.parent;
|
|
354
|
+
}
|
|
355
|
+
const factory = this.factoryRegistry.get(tok.id);
|
|
356
|
+
if (factory) return this.resolveFactory(tok.id, factory, context);
|
|
243
357
|
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
244
358
|
throw new DependencyResolutionError(`No provider registered for token ${tok.description ?? String(tok.id)} while resolving ${target.name}. Resolution stack: ${stackPath}`, {
|
|
245
359
|
target,
|
|
@@ -248,6 +362,75 @@ var Container = class {
|
|
|
248
362
|
});
|
|
249
363
|
}
|
|
250
364
|
/**
|
|
365
|
+
* Execute a token-bound factory according to its lifecycle, caching its
|
|
366
|
+
* result on the context that owns that lifecycle:
|
|
367
|
+
*
|
|
368
|
+
* - `transient`: run `fn` on every call against the current context; nothing
|
|
369
|
+
* is cached.
|
|
370
|
+
* - `singleton`: cache on the root container.
|
|
371
|
+
* - custom scope: cache on the nearest ancestor context whose `scopeName`
|
|
372
|
+
* matches; if none is active, fall back to transient behaviour (mirrors how
|
|
373
|
+
* class resolution treats a custom scope with no matching context).
|
|
374
|
+
*/
|
|
375
|
+
resolveFactory(tokenId, descriptor, context) {
|
|
376
|
+
if (descriptor.lifecycle === ServiceScope.TRANSIENT) return this.runFactory(descriptor, context);
|
|
377
|
+
const targetCtx = this.findContextForScope(descriptor.lifecycle, context);
|
|
378
|
+
if (descriptor.lifecycle !== ServiceScope.SINGLETON && targetCtx.scopeName !== descriptor.lifecycle) return this.runFactory(descriptor, context);
|
|
379
|
+
return this.resolveFactoryCached(tokenId, descriptor, targetCtx);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Resolve a factory whose result is cached on `targetCtx`, keyed by token id.
|
|
383
|
+
* Mirrors `resolveCached` for classes: return the cached value, else coalesce
|
|
384
|
+
* concurrent first-resolutions onto a single `pending` promise, else run the
|
|
385
|
+
* factory and cache. `pending` is cleared in `finally` so a rejected factory
|
|
386
|
+
* can be retried rather than caching the failure.
|
|
387
|
+
*/
|
|
388
|
+
resolveFactoryCached(tokenId, descriptor, targetCtx) {
|
|
389
|
+
const generation = descriptor.generation;
|
|
390
|
+
const cached = targetCtx.getFactoryValue(tokenId);
|
|
391
|
+
if (cached?.generation === generation) return cached.value;
|
|
392
|
+
const pending = targetCtx.getFactoryPending(tokenId);
|
|
393
|
+
if (pending?.generation === generation) return pending.promise;
|
|
394
|
+
const creation = (async () => {
|
|
395
|
+
try {
|
|
396
|
+
const value = await this.runFactory(descriptor, targetCtx);
|
|
397
|
+
targetCtx.setFactoryValue(tokenId, generation, value);
|
|
398
|
+
return value;
|
|
399
|
+
} finally {
|
|
400
|
+
targetCtx.deleteFactoryPending(tokenId, generation);
|
|
401
|
+
}
|
|
402
|
+
})();
|
|
403
|
+
targetCtx.setFactoryPending(tokenId, generation, creation);
|
|
404
|
+
return creation;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Invoke a factory's function against the context that owns its lifecycle,
|
|
408
|
+
* with a best-effort synchronous re-entrancy guard.
|
|
409
|
+
*
|
|
410
|
+
* `executing` is set only for the synchronous duration of `fn(...)` — long
|
|
411
|
+
* enough to catch a factory that resolves its own token before returning (a
|
|
412
|
+
* synchronous self-cycle, or a synchronous mutual cycle between factories),
|
|
413
|
+
* but cleared the moment `fn` returns so that concurrent resolutions which
|
|
414
|
+
* legitimately coalesce on an in-flight result are never mistaken for a cycle.
|
|
415
|
+
*
|
|
416
|
+
* Known limitation: a factory that re-enters its own token *after* awaiting
|
|
417
|
+
* (an async cross-await self-reference) is indistinguishable from valid
|
|
418
|
+
* coalescing without async-context tracking, so that case is not caught here
|
|
419
|
+
* — it is documented in the factory-providers guide.
|
|
420
|
+
*/
|
|
421
|
+
runFactory(descriptor, context) {
|
|
422
|
+
if (descriptor.executing) {
|
|
423
|
+
const label = descriptor.description ?? "<token>";
|
|
424
|
+
throw new Error(`Circular factory dependency detected: the factory for token ${label} resolved its own token while it was still being constructed. Break the cycle by restructuring the factory or resolving the dependency lazily.`);
|
|
425
|
+
}
|
|
426
|
+
descriptor.executing = true;
|
|
427
|
+
try {
|
|
428
|
+
return descriptor.fn(context);
|
|
429
|
+
} finally {
|
|
430
|
+
descriptor.executing = false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
251
434
|
* Format a readable representation of the resolution stack for error messages.
|
|
252
435
|
*/
|
|
253
436
|
formatStackPath(target, resolutionStack) {
|
package/dist/lib/decorators.d.ts
CHANGED
package/dist/lib/providers.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Newable, Token } from "./types.js";
|
|
2
|
-
import { Container } from "./container.js";
|
|
3
|
-
import { Lazy } from "./lazy.js";
|
|
4
2
|
import { ServiceScope } from "./scope.js";
|
|
3
|
+
import { Container, FactoryFn } from "./container.js";
|
|
4
|
+
import { Lazy } from "./lazy.js";
|
|
5
5
|
|
|
6
6
|
//#region src/lib/providers.d.ts
|
|
7
7
|
/**
|
|
@@ -56,11 +56,19 @@ interface LazyServiceProviderDescriptor<T = unknown> {
|
|
|
56
56
|
lifecycle: ProviderLifecycle;
|
|
57
57
|
deps?: DependenciesOption;
|
|
58
58
|
}
|
|
59
|
+
/** Descriptor for a token-bound factory provider with an explicit lifecycle. */
|
|
60
|
+
interface FactoryProviderDescriptor<T = unknown> {
|
|
61
|
+
kind: "factory";
|
|
62
|
+
token: Token<T>;
|
|
63
|
+
factory: FactoryFn<T>;
|
|
64
|
+
lifecycle: ProviderLifecycle;
|
|
65
|
+
}
|
|
59
66
|
/** Aggregate provider definitions for batch application. */
|
|
60
67
|
interface ProviderDefinitions {
|
|
61
68
|
values?: ValueProviderDescriptor[];
|
|
62
69
|
services?: ServiceProviderDescriptor[];
|
|
63
70
|
lazyServices?: LazyPlaceholder[];
|
|
71
|
+
factories?: FactoryProviderDescriptor[];
|
|
64
72
|
}
|
|
65
73
|
/**
|
|
66
74
|
* Identity helper for provider definition blocks.
|
|
@@ -76,6 +84,22 @@ declare function asClass<T extends Newable<unknown>>(useClass: T, options: {
|
|
|
76
84
|
lifecycle: ProviderLifecycle;
|
|
77
85
|
deps?: DependenciesOption;
|
|
78
86
|
}): ServiceProviderDescriptor<T>;
|
|
87
|
+
/**
|
|
88
|
+
* Create a factory provider: bind a token to a function executed at resolution
|
|
89
|
+
* time. Mirrors `asClass` — a token, a factory function, and a lifecycle.
|
|
90
|
+
*
|
|
91
|
+
* The factory receives the resolving context and may be async, so it can
|
|
92
|
+
* resolve its own dependencies. `singleton` caches the result on the root;
|
|
93
|
+
* `transient` re-runs on every resolution; a custom scope caches once per
|
|
94
|
+
* matching scope instance and disposes the result with it.
|
|
95
|
+
*
|
|
96
|
+
* @param token Token the produced value is bound to (its type checks `factory`'s return).
|
|
97
|
+
* @param factory Function invoked with the resolving context; may be async.
|
|
98
|
+
* @param options.lifecycle Lifecycle scope (singleton, transient, or a custom scope).
|
|
99
|
+
*/
|
|
100
|
+
declare function asFactory<T>(token: Token<T>, factory: FactoryFn<T>, options: {
|
|
101
|
+
lifecycle: ProviderLifecycle;
|
|
102
|
+
}): FactoryProviderDescriptor<T>;
|
|
79
103
|
/**
|
|
80
104
|
* Convenience lifecycle helpers for fluent provider creation.
|
|
81
105
|
*/
|
|
@@ -108,4 +132,4 @@ declare function asLazyClass<T, const TDeps extends DependenciesOption>(importer
|
|
|
108
132
|
*/
|
|
109
133
|
declare function applyProviders(container: Container, definitions: ProviderDefinitions | ProviderDefinitions[]): void;
|
|
110
134
|
//#endregion
|
|
111
|
-
export { ProviderDefinitions, applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle };
|
|
135
|
+
export { FactoryProviderDescriptor, ProviderDefinitions, applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle };
|
package/dist/lib/providers.js
CHANGED
|
@@ -34,6 +34,27 @@ function asClass(useClass, options) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
+
* Create a factory provider: bind a token to a function executed at resolution
|
|
38
|
+
* time. Mirrors `asClass` — a token, a factory function, and a lifecycle.
|
|
39
|
+
*
|
|
40
|
+
* The factory receives the resolving context and may be async, so it can
|
|
41
|
+
* resolve its own dependencies. `singleton` caches the result on the root;
|
|
42
|
+
* `transient` re-runs on every resolution; a custom scope caches once per
|
|
43
|
+
* matching scope instance and disposes the result with it.
|
|
44
|
+
*
|
|
45
|
+
* @param token Token the produced value is bound to (its type checks `factory`'s return).
|
|
46
|
+
* @param factory Function invoked with the resolving context; may be async.
|
|
47
|
+
* @param options.lifecycle Lifecycle scope (singleton, transient, or a custom scope).
|
|
48
|
+
*/
|
|
49
|
+
function asFactory(token, factory, options) {
|
|
50
|
+
return {
|
|
51
|
+
kind: "factory",
|
|
52
|
+
token,
|
|
53
|
+
factory,
|
|
54
|
+
lifecycle: options.lifecycle
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
37
58
|
* Convenience lifecycle helpers for fluent provider creation.
|
|
38
59
|
*/
|
|
39
60
|
const lifecycle = {
|
|
@@ -145,6 +166,7 @@ function applyProviders(container, definitions) {
|
|
|
145
166
|
detectProviderCycles(planEntries);
|
|
146
167
|
for (const definition of list) {
|
|
147
168
|
for (const valueProvider of definition.values ?? []) container.provideValue(valueProvider.token, valueProvider.value);
|
|
169
|
+
for (const factoryProvider of definition.factories ?? []) container.provideFactory(factoryProvider.token, factoryProvider.factory, { lifecycle: factoryProvider.lifecycle });
|
|
148
170
|
const registerService = (ctor, lifecycle, deps, factory) => {
|
|
149
171
|
dependenciesRegistry.set(ctor, {
|
|
150
172
|
scope: lifecycle,
|
|
@@ -161,4 +183,4 @@ function applyProviders(container, definitions) {
|
|
|
161
183
|
}
|
|
162
184
|
}
|
|
163
185
|
//#endregion
|
|
164
|
-
export { applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle };
|
|
186
|
+
export { applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle };
|
package/dist/lib/scope.d.ts
CHANGED
|
@@ -1,8 +1,44 @@
|
|
|
1
|
+
import { Constructor } from "./types.js";
|
|
2
|
+
|
|
1
3
|
//#region src/lib/scope.d.ts
|
|
2
4
|
declare const ServiceScope: {
|
|
3
5
|
readonly SINGLETON: "singleton";
|
|
4
6
|
readonly TRANSIENT: "transient";
|
|
5
7
|
};
|
|
6
|
-
|
|
8
|
+
interface AlloyScopes {
|
|
9
|
+
singleton: true;
|
|
10
|
+
transient: true;
|
|
11
|
+
}
|
|
12
|
+
type ServiceScope = keyof AlloyScopes;
|
|
13
|
+
/**
|
|
14
|
+
* A cached factory result tagged with the `generation` of the descriptor that
|
|
15
|
+
* produced it. A mismatch against the current descriptor's generation (after a
|
|
16
|
+
* factory is re-registered) makes the entry stale, so it is recomputed.
|
|
17
|
+
*/
|
|
18
|
+
interface FactoryCacheEntry {
|
|
19
|
+
generation: number;
|
|
20
|
+
value: unknown;
|
|
21
|
+
}
|
|
22
|
+
/** An in-flight factory resolution tagged with its descriptor generation. */
|
|
23
|
+
interface FactoryPendingEntry {
|
|
24
|
+
generation: number;
|
|
25
|
+
promise: Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
interface ResolutionContext {
|
|
28
|
+
readonly scopeName: ServiceScope;
|
|
29
|
+
getCached(target: Constructor): unknown;
|
|
30
|
+
setCached(target: Constructor, instance: unknown): void;
|
|
31
|
+
getPending(target: Constructor): Promise<unknown> | undefined;
|
|
32
|
+
setPending(target: Constructor, promise: Promise<unknown>): void;
|
|
33
|
+
deletePending(target: Constructor): void;
|
|
34
|
+
getProvider(tokenId: symbol): unknown;
|
|
35
|
+
hasProvider(tokenId: symbol): boolean;
|
|
36
|
+
getFactoryValue(tokenId: symbol): FactoryCacheEntry | undefined;
|
|
37
|
+
setFactoryValue(tokenId: symbol, generation: number, value: unknown): void;
|
|
38
|
+
getFactoryPending(tokenId: symbol): FactoryPendingEntry | undefined;
|
|
39
|
+
setFactoryPending(tokenId: symbol, generation: number, promise: Promise<unknown>): void;
|
|
40
|
+
deleteFactoryPending(tokenId: symbol, generation: number): void;
|
|
41
|
+
readonly parent: ResolutionContext | null;
|
|
42
|
+
}
|
|
7
43
|
//#endregion
|
|
8
|
-
export { ServiceScope };
|
|
44
|
+
export { AlloyScopes, FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope };
|