@fnioc/di 1.0.0 → 2.0.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 +36 -27
- package/dist/index.d.ts +652 -8
- package/dist/index.js +521 -23
- package/package.json +6 -9
- package/dist/builder.d.ts +0 -81
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -85
- package/dist/builder.js.map +0 -1
- package/dist/errors.d.ts +0 -73
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -139
- package/dist/errors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/scope.d.ts +0 -180
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -466
- package/dist/scope.js.map +0 -1
- package/dist/types.d.ts +0 -57
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -4
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,23 +1,521 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
// ../core/src/store.ts
|
|
2
|
+
var hole = null;
|
|
3
|
+
var GLOBAL_KEY = Symbol.for("fnioc:deps");
|
|
4
|
+
var globals = globalThis;
|
|
5
|
+
var store = globals[GLOBAL_KEY] ??= new Map;
|
|
6
|
+
// ../core/src/defineDeps.ts
|
|
7
|
+
function isFactoryRef(slot) {
|
|
8
|
+
return typeof slot === "object" && slot !== null && typeof slot.factory === "string";
|
|
9
|
+
}
|
|
10
|
+
function isScopeRef(slot) {
|
|
11
|
+
return typeof slot === "object" && slot !== null && slot.scope === true;
|
|
12
|
+
}
|
|
13
|
+
function slotsEqual(a, b) {
|
|
14
|
+
const aIsRef = isFactoryRef(a);
|
|
15
|
+
const bIsRef = isFactoryRef(b);
|
|
16
|
+
if (aIsRef || bIsRef) {
|
|
17
|
+
return aIsRef && bIsRef && a.factory === b.factory;
|
|
18
|
+
}
|
|
19
|
+
const aIsScope = isScopeRef(a);
|
|
20
|
+
const bIsScope = isScopeRef(b);
|
|
21
|
+
if (aIsScope || bIsScope) {
|
|
22
|
+
return aIsScope && bIsScope;
|
|
23
|
+
}
|
|
24
|
+
return a === b;
|
|
25
|
+
}
|
|
26
|
+
function signaturesEqual(a, b) {
|
|
27
|
+
if (a.length !== b.length)
|
|
28
|
+
return false;
|
|
29
|
+
for (let i = 0;i < a.length; i++) {
|
|
30
|
+
if (!slotsEqual(a[i], b[i]))
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
function defineDeps(target, signatures) {
|
|
36
|
+
const existing = store.get(target);
|
|
37
|
+
if (existing !== undefined) {
|
|
38
|
+
const merged = [...existing.signatures];
|
|
39
|
+
for (const sig of signatures) {
|
|
40
|
+
if (!merged.some((s) => signaturesEqual(s, sig))) {
|
|
41
|
+
merged.push(sig);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
store.set(target, { signatures: merged });
|
|
45
|
+
} else {
|
|
46
|
+
store.set(target, { signatures });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getDeps(target) {
|
|
50
|
+
return store.get(target);
|
|
51
|
+
}
|
|
52
|
+
// ../core/src/signature.ts
|
|
53
|
+
function signature(...tokens) {
|
|
54
|
+
return (value, _context) => {
|
|
55
|
+
defineDeps(value, [tokens.slice()]);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ../core/src/forCtor.ts
|
|
59
|
+
function forCtor(ctor) {
|
|
60
|
+
const builder = {
|
|
61
|
+
signature(...tokens) {
|
|
62
|
+
defineDeps(ctor, [tokens.slice()]);
|
|
63
|
+
return builder;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return builder;
|
|
67
|
+
}
|
|
68
|
+
// src/errors.ts
|
|
69
|
+
class DiError extends Error {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = new.target.name;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class UnregisteredTokenError extends DiError {
|
|
77
|
+
token;
|
|
78
|
+
constructor(token) {
|
|
79
|
+
super(`No registration found for token "${token}". Register it with ` + `services.add(...) before resolving.`);
|
|
80
|
+
this.token = token;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class MissingScopeError extends DiError {
|
|
85
|
+
token;
|
|
86
|
+
scope;
|
|
87
|
+
availableScopes;
|
|
88
|
+
constructor(token, scope, availableScopes) {
|
|
89
|
+
super(`Cannot resolve "${token}": its lifetime is tagged "${scope}", but no ` + `ancestor scope with that name exists in the resolving chain ` + `(available: ${availableScopes.length > 0 ? availableScopes.map((s) => `"${s}"`).join(" → ") : "none"}). ` + `This usually means a longer-lived service depends on a ` + `shorter-lived one (e.g. a "singleton" needing a "request"). Never ` + `auto-created — fix the registration's lifetime or create the scope.`);
|
|
90
|
+
this.token = token;
|
|
91
|
+
this.scope = scope;
|
|
92
|
+
this.availableScopes = availableScopes;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class MissingMetadataError extends DiError {
|
|
97
|
+
token;
|
|
98
|
+
ctorName;
|
|
99
|
+
constructor(token, ctorName) {
|
|
100
|
+
super(`No dep metadata found for ${ctorName} (resolving "${token}"). The ` + `constructor has parameters but no @signature, forCtor, or ` + `transformer-generated defineDeps call was found. Use ` + `forCtor(...).signature(...) or register it with useFactory to wire ` + `it manually.`);
|
|
101
|
+
this.token = token;
|
|
102
|
+
this.ctorName = ctorName;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class NoSatisfiableSignatureError extends DiError {
|
|
107
|
+
token;
|
|
108
|
+
ctorName;
|
|
109
|
+
unsatisfiable;
|
|
110
|
+
constructor(token, ctorName, unsatisfiable) {
|
|
111
|
+
super(`No satisfiable constructor signature for ${ctorName} (resolving ` + `"${token}"). Every candidate signature names a dependency that is ` + `not registered in the owning scope` + (unsatisfiable.length > 0 ? `; unsatisfiable tokens: ${unsatisfiable.map((t) => `"${t}"`).join(", ")}` : "") + `. Register the missing dependencies, or provide a useFactory ` + `override.`);
|
|
112
|
+
this.token = token;
|
|
113
|
+
this.ctorName = ctorName;
|
|
114
|
+
this.unsatisfiable = unsatisfiable;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class CircularDependencyError extends DiError {
|
|
119
|
+
path;
|
|
120
|
+
constructor(path) {
|
|
121
|
+
super(`Circular dependency detected:
|
|
122
|
+
${path.join(" → ")}`);
|
|
123
|
+
this.path = path;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class FactoryTargetError extends DiError {
|
|
128
|
+
factoryToken;
|
|
129
|
+
reason;
|
|
130
|
+
constructor(factoryToken, reason) {
|
|
131
|
+
super(reason === "unregistered" ? `Cannot inject a factory for "${factoryToken}": no registration ` + `found for it. A factory parameter (typed \`() => IFoo\`) needs ` + `the target registered as a class with ` + `services.add(...) before it can build instances.` : `Cannot inject a factory for "${factoryToken}": it is registered ` + `as a useValue/useFactory override, not a class. A factory builds ` + `its target with \`new\`, so the target must be a class ` + `registration. Resolve it directly instead of as a factory, or ` + `register the class with services.add(...).`);
|
|
132
|
+
this.factoryToken = factoryToken;
|
|
133
|
+
this.reason = reason;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class AsyncDisposalRequiredError extends DiError {
|
|
138
|
+
constructor() {
|
|
139
|
+
super(`Cannot dispose synchronously: this scope owns a Promise-valued ` + `instance (an async useFactory result). Awaiting it is required ` + `before disposal — call disposeAsync() instead of dispose().`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/scope.ts
|
|
144
|
+
function isDisposable(value) {
|
|
145
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.dispose] === "function";
|
|
146
|
+
}
|
|
147
|
+
function isAsyncDisposable(value) {
|
|
148
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value[Symbol.asyncDispose] === "function";
|
|
149
|
+
}
|
|
150
|
+
function isThenable(value) {
|
|
151
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
152
|
+
}
|
|
153
|
+
function isFactoryRef2(slot) {
|
|
154
|
+
return slot !== null && typeof slot === "object" && typeof slot.factory === "string";
|
|
155
|
+
}
|
|
156
|
+
function isScopeRef2(slot) {
|
|
157
|
+
return slot !== null && typeof slot === "object" && slot.scope === true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class Scope {
|
|
161
|
+
name;
|
|
162
|
+
parent;
|
|
163
|
+
baseRegistrations;
|
|
164
|
+
localRegistrations = new Map;
|
|
165
|
+
instances = new Map;
|
|
166
|
+
ownedOrder = [];
|
|
167
|
+
disposed = false;
|
|
168
|
+
constructor(name, parent, baseRegistrations) {
|
|
169
|
+
this.name = name;
|
|
170
|
+
this.parent = parent;
|
|
171
|
+
this.baseRegistrations = baseRegistrations;
|
|
172
|
+
}
|
|
173
|
+
static appendTo(map, token, registration) {
|
|
174
|
+
const existing = map.get(token);
|
|
175
|
+
if (existing === undefined) {
|
|
176
|
+
map.set(token, [registration]);
|
|
177
|
+
} else {
|
|
178
|
+
existing.push(registration);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
createScope(childName) {
|
|
182
|
+
return new Scope(childName, this, this.baseRegistrations);
|
|
183
|
+
}
|
|
184
|
+
appendScopedLocal(token, base) {
|
|
185
|
+
Scope.appendTo(this.localRegistrations, token, base);
|
|
186
|
+
const map = this.localRegistrations;
|
|
187
|
+
return {
|
|
188
|
+
as(scope) {
|
|
189
|
+
if (scope === undefined)
|
|
190
|
+
return;
|
|
191
|
+
Scope.appendTo(map, token, { ...base, scope });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
add(token, ctor) {
|
|
196
|
+
return this.appendScopedLocal(token, {
|
|
197
|
+
kind: "class",
|
|
198
|
+
ctor,
|
|
199
|
+
scope: undefined
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
addFactory(token, factory) {
|
|
203
|
+
return this.appendScopedLocal(token, {
|
|
204
|
+
kind: "factory",
|
|
205
|
+
factory,
|
|
206
|
+
scope: undefined
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
addValue(token, value) {
|
|
210
|
+
Scope.appendTo(this.localRegistrations, token, {
|
|
211
|
+
kind: "value",
|
|
212
|
+
useValue: value
|
|
213
|
+
});
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
resolve(token) {
|
|
217
|
+
if (token === undefined) {
|
|
218
|
+
throw new TypeError("resolve<T>() requires the @fnioc/transformer plugin (no token at " + "runtime). Without it, resolve with an explicit token: " + 'resolve<T>("my:token").');
|
|
219
|
+
}
|
|
220
|
+
return this.resolveWith(token, []);
|
|
221
|
+
}
|
|
222
|
+
resolveFactory(token) {
|
|
223
|
+
return this.makeFactory({ factory: token });
|
|
224
|
+
}
|
|
225
|
+
lookup(token) {
|
|
226
|
+
let node = this;
|
|
227
|
+
while (node !== undefined) {
|
|
228
|
+
const local = node.localRegistrations.get(token);
|
|
229
|
+
if (local !== undefined && local.length > 0)
|
|
230
|
+
return local[local.length - 1];
|
|
231
|
+
node = node.parent;
|
|
232
|
+
}
|
|
233
|
+
const base = this.baseRegistrations.get(token);
|
|
234
|
+
return base !== undefined && base.length > 0 ? base[base.length - 1] : undefined;
|
|
235
|
+
}
|
|
236
|
+
findOwner(scope) {
|
|
237
|
+
let node = this;
|
|
238
|
+
while (node !== undefined) {
|
|
239
|
+
if (node.name === scope)
|
|
240
|
+
return node;
|
|
241
|
+
node = node.parent;
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
chainNames() {
|
|
246
|
+
const names = [];
|
|
247
|
+
let node = this;
|
|
248
|
+
while (node !== undefined) {
|
|
249
|
+
names.push(node.name);
|
|
250
|
+
node = node.parent;
|
|
251
|
+
}
|
|
252
|
+
return names;
|
|
253
|
+
}
|
|
254
|
+
resolveWith(token, stack) {
|
|
255
|
+
if (stack.includes(token)) {
|
|
256
|
+
throw new CircularDependencyError([...stack, token]);
|
|
257
|
+
}
|
|
258
|
+
const registration = this.lookup(token);
|
|
259
|
+
if (registration === undefined) {
|
|
260
|
+
throw new UnregisteredTokenError(token);
|
|
261
|
+
}
|
|
262
|
+
if (registration.kind === "value") {
|
|
263
|
+
return registration.useValue;
|
|
264
|
+
}
|
|
265
|
+
if (registration.scope === undefined) {
|
|
266
|
+
stack.push(token);
|
|
267
|
+
try {
|
|
268
|
+
return this.instantiate(token, registration, this, stack);
|
|
269
|
+
} finally {
|
|
270
|
+
stack.pop();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const owner = this.findOwner(registration.scope);
|
|
274
|
+
if (owner === undefined) {
|
|
275
|
+
throw new MissingScopeError(token, registration.scope, this.chainNames());
|
|
276
|
+
}
|
|
277
|
+
if (owner.instances.has(token)) {
|
|
278
|
+
return owner.instances.get(token);
|
|
279
|
+
}
|
|
280
|
+
stack.push(token);
|
|
281
|
+
try {
|
|
282
|
+
const instance = owner.instantiate(token, registration, owner, stack);
|
|
283
|
+
owner.instances.set(token, instance);
|
|
284
|
+
owner.ownedOrder.push(instance);
|
|
285
|
+
return instance;
|
|
286
|
+
} finally {
|
|
287
|
+
stack.pop();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
instantiate(token, registration, owningScope, stack) {
|
|
291
|
+
if (registration.kind === "factory") {
|
|
292
|
+
return owningScope.invokeFactory(token, registration.factory, stack);
|
|
293
|
+
}
|
|
294
|
+
return owningScope.construct(token, registration.ctor, stack);
|
|
295
|
+
}
|
|
296
|
+
makeScopeView(stack) {
|
|
297
|
+
const owner = this;
|
|
298
|
+
const view = {
|
|
299
|
+
resolve: (depToken) => {
|
|
300
|
+
if (depToken === undefined) {
|
|
301
|
+
throw new TypeError("resolve<T>() requires the @fnioc/transformer plugin (no token at " + "runtime).");
|
|
302
|
+
}
|
|
303
|
+
return owner.resolveWith(depToken, stack);
|
|
304
|
+
},
|
|
305
|
+
resolveFactory: (depToken) => owner.makeFactory({ factory: depToken }),
|
|
306
|
+
createScope: (name) => owner.createScope(name)
|
|
307
|
+
};
|
|
308
|
+
return view;
|
|
309
|
+
}
|
|
310
|
+
invokeFactory(token, factory, stack) {
|
|
311
|
+
const scopeView = this.makeScopeView(stack);
|
|
312
|
+
const record = getDeps(factory);
|
|
313
|
+
if (record === undefined || record.signatures.length === 0) {
|
|
314
|
+
return factory(scopeView);
|
|
315
|
+
}
|
|
316
|
+
const signature2 = this.selectSignature(token, factory.name, record.signatures);
|
|
317
|
+
const args = signature2.map((slot) => {
|
|
318
|
+
if (isScopeRef2(slot))
|
|
319
|
+
return scopeView;
|
|
320
|
+
if (isFactoryRef2(slot))
|
|
321
|
+
return this.makeFactory(slot);
|
|
322
|
+
return this.resolveWith(slot, stack);
|
|
323
|
+
});
|
|
324
|
+
return factory(...args);
|
|
325
|
+
}
|
|
326
|
+
construct(token, ctor, stack) {
|
|
327
|
+
const record = getDeps(ctor);
|
|
328
|
+
if (record === undefined || record.signatures.length === 0) {
|
|
329
|
+
if (ctor.length > 0) {
|
|
330
|
+
throw new MissingMetadataError(token, ctor.name);
|
|
331
|
+
}
|
|
332
|
+
return new ctor;
|
|
333
|
+
}
|
|
334
|
+
const signature2 = this.selectSignature(token, ctor.name, record.signatures);
|
|
335
|
+
const args = signature2.map((slot) => {
|
|
336
|
+
if (isScopeRef2(slot)) {
|
|
337
|
+
return this.makeScopeView(stack);
|
|
338
|
+
}
|
|
339
|
+
if (isFactoryRef2(slot)) {
|
|
340
|
+
return this.makeFactory(slot);
|
|
341
|
+
}
|
|
342
|
+
return this.resolveWith(slot, stack);
|
|
343
|
+
});
|
|
344
|
+
return new ctor(...args);
|
|
345
|
+
}
|
|
346
|
+
makeFactory(ref) {
|
|
347
|
+
const owningScope = this;
|
|
348
|
+
const target = this.lookup(ref.factory);
|
|
349
|
+
if (target === undefined) {
|
|
350
|
+
throw new FactoryTargetError(ref.factory, "unregistered");
|
|
351
|
+
}
|
|
352
|
+
if (target.kind === "value") {
|
|
353
|
+
return () => owningScope.resolveWith(ref.factory, []);
|
|
354
|
+
}
|
|
355
|
+
const depTarget = target.kind === "class" ? target.ctor : target.factory;
|
|
356
|
+
const record = getDeps(depTarget);
|
|
357
|
+
const targetSignature = record === undefined || record.signatures.length === 0 ? undefined : owningScope.selectTargetSignature(record.signatures);
|
|
358
|
+
const parameterized = targetSignature !== undefined && targetSignature.some((slot) => !isFactoryRef2(slot) && !isScopeRef2(slot) && !owningScope.isResolvable(slot));
|
|
359
|
+
if (!parameterized) {
|
|
360
|
+
return () => owningScope.resolveWith(ref.factory, []);
|
|
361
|
+
}
|
|
362
|
+
return (...callArgs) => owningScope.buildPartitioned(target, targetSignature, callArgs);
|
|
363
|
+
}
|
|
364
|
+
buildPartitioned(target, signature2, callerArgs) {
|
|
365
|
+
const stack = [];
|
|
366
|
+
let nextCallerArg = 0;
|
|
367
|
+
const args = signature2.map((slot) => {
|
|
368
|
+
if (isScopeRef2(slot))
|
|
369
|
+
return this.makeScopeView(stack);
|
|
370
|
+
if (isFactoryRef2(slot))
|
|
371
|
+
return this.makeFactory(slot);
|
|
372
|
+
if (!this.isResolvable(slot)) {
|
|
373
|
+
return callerArgs[nextCallerArg++];
|
|
374
|
+
}
|
|
375
|
+
return this.resolveWith(slot, stack);
|
|
376
|
+
});
|
|
377
|
+
return target.kind === "class" ? new target.ctor(...args) : target.factory(...args);
|
|
378
|
+
}
|
|
379
|
+
selectSignature(token, targetName, signatures) {
|
|
380
|
+
const ordered = signatures.map((sig, index) => ({ sig, index })).sort((a, b) => b.sig.length !== a.sig.length ? b.sig.length - a.sig.length : a.index - b.index);
|
|
381
|
+
const unsatisfiable = new Set;
|
|
382
|
+
for (const { sig } of ordered) {
|
|
383
|
+
let satisfiable = true;
|
|
384
|
+
for (const slot of sig) {
|
|
385
|
+
if (isFactoryRef2(slot) || isScopeRef2(slot))
|
|
386
|
+
continue;
|
|
387
|
+
if (!this.isResolvable(slot)) {
|
|
388
|
+
satisfiable = false;
|
|
389
|
+
if (typeof slot === "string")
|
|
390
|
+
unsatisfiable.add(slot);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (satisfiable)
|
|
394
|
+
return sig;
|
|
395
|
+
}
|
|
396
|
+
throw new NoSatisfiableSignatureError(token, targetName, [...unsatisfiable]);
|
|
397
|
+
}
|
|
398
|
+
selectTargetSignature(signatures) {
|
|
399
|
+
return signatures.map((sig, index) => ({ sig, index })).sort((a, b) => b.sig.length !== a.sig.length ? b.sig.length - a.sig.length : a.index - b.index)[0].sig;
|
|
400
|
+
}
|
|
401
|
+
isResolvable(slot) {
|
|
402
|
+
return typeof slot === "string" && this.lookup(slot) !== undefined;
|
|
403
|
+
}
|
|
404
|
+
dispose() {
|
|
405
|
+
if (this.disposed)
|
|
406
|
+
return;
|
|
407
|
+
for (const instance of this.ownedOrder) {
|
|
408
|
+
if (isThenable(instance)) {
|
|
409
|
+
throw new AsyncDisposalRequiredError;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
this.disposed = true;
|
|
413
|
+
for (let i = this.ownedOrder.length - 1;i >= 0; i--) {
|
|
414
|
+
const instance = this.ownedOrder[i];
|
|
415
|
+
if (isDisposable(instance)) {
|
|
416
|
+
instance[Symbol.dispose]();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
this.clear();
|
|
420
|
+
}
|
|
421
|
+
async disposeAsync() {
|
|
422
|
+
if (this.disposed)
|
|
423
|
+
return;
|
|
424
|
+
this.disposed = true;
|
|
425
|
+
const settled = [];
|
|
426
|
+
for (const instance of this.ownedOrder) {
|
|
427
|
+
settled.push(isThenable(instance) ? await instance : instance);
|
|
428
|
+
}
|
|
429
|
+
for (let i = settled.length - 1;i >= 0; i--) {
|
|
430
|
+
const instance = settled[i];
|
|
431
|
+
if (isAsyncDisposable(instance)) {
|
|
432
|
+
await instance[Symbol.asyncDispose]();
|
|
433
|
+
} else if (isDisposable(instance)) {
|
|
434
|
+
instance[Symbol.dispose]();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
this.clear();
|
|
438
|
+
}
|
|
439
|
+
clear() {
|
|
440
|
+
this.instances.clear();
|
|
441
|
+
this.ownedOrder.length = 0;
|
|
442
|
+
}
|
|
443
|
+
[Symbol.dispose]() {
|
|
444
|
+
this.dispose();
|
|
445
|
+
}
|
|
446
|
+
[Symbol.asyncDispose]() {
|
|
447
|
+
return this.disposeAsync();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/builder.ts
|
|
452
|
+
class DiBuilder {
|
|
453
|
+
registrations = new Map;
|
|
454
|
+
rootName;
|
|
455
|
+
constructor(rootName) {
|
|
456
|
+
this.rootName = rootName ?? "singleton";
|
|
457
|
+
}
|
|
458
|
+
append(token, registration) {
|
|
459
|
+
const existing = this.registrations.get(token);
|
|
460
|
+
if (existing === undefined) {
|
|
461
|
+
this.registrations.set(token, [registration]);
|
|
462
|
+
} else {
|
|
463
|
+
existing.push(registration);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
appendScoped(token, base) {
|
|
467
|
+
this.append(token, base);
|
|
468
|
+
const append = (next) => this.append(token, next);
|
|
469
|
+
return {
|
|
470
|
+
as(scope) {
|
|
471
|
+
if (scope === undefined)
|
|
472
|
+
return;
|
|
473
|
+
append({ ...base, scope });
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
add(...args) {
|
|
478
|
+
if (args.length === 1 || typeof args[0] !== "string") {
|
|
479
|
+
throw new TypeError("add<I>(ctor) / add<I>(factory) require the @fnioc/transformer plugin. " + 'Without it, register with an explicit token: add("my:token", MyClass) ' + 'or addFactory("my:token", (scope) => ...).');
|
|
480
|
+
}
|
|
481
|
+
const [token, ctor] = args;
|
|
482
|
+
return this.appendScoped(token, {
|
|
483
|
+
kind: "class",
|
|
484
|
+
ctor,
|
|
485
|
+
scope: undefined
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
addFactory(token, factory) {
|
|
489
|
+
return this.appendScoped(token, {
|
|
490
|
+
kind: "factory",
|
|
491
|
+
factory,
|
|
492
|
+
scope: undefined
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
addValue(...args) {
|
|
496
|
+
if (args.length === 1 || typeof args[0] !== "string") {
|
|
497
|
+
throw new TypeError("addValue<I>(value) requires the @fnioc/transformer plugin. Without it, " + 'register with an explicit token: addValue("my:token", value).');
|
|
498
|
+
}
|
|
499
|
+
const [token, value] = args;
|
|
500
|
+
this.append(token, { kind: "value", useValue: value });
|
|
501
|
+
}
|
|
502
|
+
build() {
|
|
503
|
+
return new Scope(this.rootName, undefined, this.registrations);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
export {
|
|
507
|
+
signature,
|
|
508
|
+
hole,
|
|
509
|
+
forCtor,
|
|
510
|
+
defineDeps,
|
|
511
|
+
UnregisteredTokenError,
|
|
512
|
+
Scope,
|
|
513
|
+
NoSatisfiableSignatureError,
|
|
514
|
+
MissingScopeError,
|
|
515
|
+
MissingMetadataError,
|
|
516
|
+
FactoryTargetError,
|
|
517
|
+
DiError,
|
|
518
|
+
DiBuilder,
|
|
519
|
+
CircularDependencyError,
|
|
520
|
+
AsyncDisposalRequiredError
|
|
521
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fnioc/di",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "The ioc runtime engine: DiBuilder, scopes, resolution, captive-dependency protection, factories, and native disposal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dependency-injection",
|
|
@@ -30,18 +30,15 @@
|
|
|
30
30
|
"dist"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "tsc -
|
|
33
|
+
"build": "tsc --noEmit -p tsconfig.json && bun build.ts",
|
|
34
34
|
"test": "bun test",
|
|
35
|
-
"lint": "
|
|
36
|
-
},
|
|
37
|
-
"peerDependencies": {
|
|
38
|
-
"@fnioc/core": "^1.0.0"
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@fnioc/core": "^1.0.0"
|
|
35
|
+
"lint": "tsc --noEmit -p tsconfig.json"
|
|
42
36
|
},
|
|
43
37
|
"publishConfig": {
|
|
44
38
|
"access": "public",
|
|
45
39
|
"provenance": true
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@rhombus-toolkit/func": "3.5.3"
|
|
46
43
|
}
|
|
47
44
|
}
|
package/dist/builder.d.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { Token } from "@fnioc/core";
|
|
2
|
-
import { Scope } from "./scope.js";
|
|
3
|
-
import type { Ctor, OverrideSpec } from "./types.js";
|
|
4
|
-
/**
|
|
5
|
-
* The continuation returned by `DiBuilder.add`. Carries the just-added
|
|
6
|
-
* registration so `.as()` can attach its lifetime tag in place. An `.add()`
|
|
7
|
-
* with no trailing `.as()` leaves the registration untagged ⇒ transient.
|
|
8
|
-
*
|
|
9
|
-
* `Scopes` is threaded so `.as()` only accepts a declared scope name —
|
|
10
|
-
* compile-time captive-misconfiguration guard at the registration site.
|
|
11
|
-
*/
|
|
12
|
-
export interface AddBuilder<Scopes extends string> {
|
|
13
|
-
/**
|
|
14
|
-
* Attaches the lifetime tag. Must name a declared scope.
|
|
15
|
-
*
|
|
16
|
-
* Two call shapes, by design (PRD §7):
|
|
17
|
-
* - AUTHORED `.as<"singleton">()` — the scope name is a TYPE argument; the
|
|
18
|
-
* `S extends Scopes` bound is the compile-time captive-misconfiguration
|
|
19
|
-
* guard. No value argument is passed; this form is never executed (the
|
|
20
|
-
* transformer rewrites it before it runs).
|
|
21
|
-
* - LOWERED `.as("singleton")` — the transformer rewrites the type
|
|
22
|
-
* argument to a value argument. This is the form the engine executes; the
|
|
23
|
-
* runtime reads the tag from the value arg.
|
|
24
|
-
*
|
|
25
|
-
* `scope` is therefore OPTIONAL at the type level: the authored form supplies
|
|
26
|
-
* it as a type arg only, the lowered form as a value. A bare `.as()` with no
|
|
27
|
-
* type arg leaves `S = Scopes` and is a degenerate (untagged) call — use the
|
|
28
|
-
* type arg.
|
|
29
|
-
*/
|
|
30
|
-
as<S extends Scopes>(scope?: S): void;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* The registration builder.
|
|
34
|
-
*
|
|
35
|
-
* `Scopes` is the user-supplied scope-name union (e.g.
|
|
36
|
-
* `"singleton" | "request"`). `"transient"` is NOT a member — transient is the
|
|
37
|
-
* absence of a tag, not a scope.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* const services = new DiBuilder<"singleton" | "request">();
|
|
42
|
-
* services.add("pkg:ILogger", ConsoleLogger).as("singleton"); // lowered form
|
|
43
|
-
* const root = services.createScope("singleton");
|
|
44
|
-
* const logger = root.resolve<ILogger>("pkg:ILogger");
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export declare class DiBuilder<Scopes extends string = string> {
|
|
48
|
-
private readonly registrations;
|
|
49
|
-
/**
|
|
50
|
-
* Type-only authoring overload — the form the transformer rewrites FROM. The
|
|
51
|
-
* concrete is typed `new (...args: any[]) => I` (plain `new`, so an abstract
|
|
52
|
-
* class is rejected). At runtime the engine only ever receives the
|
|
53
|
-
* string-token form below; this signature exists purely so type-driven
|
|
54
|
-
* authoring type-checks before the transformer lowers it.
|
|
55
|
-
*/
|
|
56
|
-
add<I>(ctor: new (...args: any[]) => I): AddBuilder<Scopes>;
|
|
57
|
-
/**
|
|
58
|
-
* The runtime reality — a string token bound to a concrete constructor. This
|
|
59
|
-
* is what the transformer emits and what the engine actually executes.
|
|
60
|
-
*/
|
|
61
|
-
add(token: Token, ctor: Ctor): AddBuilder<Scopes>;
|
|
62
|
-
/**
|
|
63
|
-
* The plugin-less override path. Registers a token against either a
|
|
64
|
-
* `useFactory` closure (which resolves its own deps from the scope passed to
|
|
65
|
-
* it) or a `useValue` instance. The recommended mechanism for test doubles,
|
|
66
|
-
* third-party instances, and plugin-less wiring.
|
|
67
|
-
*
|
|
68
|
-
* A `useFactory` may carry an optional `tag` so the factory's result is
|
|
69
|
-
* cached at a matching ancestor scope (singleton-style); without a tag it
|
|
70
|
-
* runs on every resolve. A `useValue` is the instance itself — always the
|
|
71
|
-
* same value, no lifetime.
|
|
72
|
-
*/
|
|
73
|
-
register<T>(token: Token, spec: OverrideSpec<T>): this;
|
|
74
|
-
/**
|
|
75
|
-
* Mints the root scope. The root must be a real, app-lifetime object — its
|
|
76
|
-
* name is the lifetime tag that singletons (or whatever the app's longest
|
|
77
|
-
* lifetime is) bind to.
|
|
78
|
-
*/
|
|
79
|
-
createScope(rootScopeName: Scopes): Scope<Scopes>;
|
|
80
|
-
}
|
|
81
|
-
//# sourceMappingURL=builder.d.ts.map
|
package/dist/builder.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAEV,IAAI,EACJ,YAAY,EAGb,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU,CAAC,MAAM,SAAS,MAAM;IAC/C;;;;;;;;;;;;;;;;OAgBG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,SAAS,CAAC,MAAM,SAAS,MAAM,GAAG,MAAM;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAEhE;;;;;;OAMG;IACI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC;IAClE;;;OAGG;IACI,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC;IAsCxD;;;;;;;;;;OAUG;IACI,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI;IAa7D;;;;OAIG;IACI,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;CAGzD"}
|