di-bag 0.1.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/LICENSE +21 -0
- package/README.md +280 -0
- package/dist/acquisition-context.d.ts +42 -0
- package/dist/acquisition-context.js +19 -0
- package/dist/acquisition-family.d.ts +32 -0
- package/dist/acquisition-family.js +135 -0
- package/dist/acquisition-mode.d.ts +34 -0
- package/dist/acquisition-mode.js +35 -0
- package/dist/acquisition.d.ts +44 -0
- package/dist/acquisition.js +395 -0
- package/dist/alias-types.d.ts +22 -0
- package/dist/alias-types.js +2 -0
- package/dist/aliases.d.ts +4 -0
- package/dist/aliases.js +24 -0
- package/dist/composition.d.ts +41 -0
- package/dist/composition.js +45 -0
- package/dist/contribution-types.d.ts +57 -0
- package/dist/contribution-types.js +2 -0
- package/dist/contributions.d.ts +3 -0
- package/dist/contributions.js +11 -0
- package/dist/dependency-references.d.ts +52 -0
- package/dist/dependency-references.js +53 -0
- package/dist/di-bag.d.ts +247 -0
- package/dist/di-bag.js +211 -0
- package/dist/errors.d.ts +66 -0
- package/dist/errors.js +89 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +10 -0
- package/dist/inspection.d.ts +36 -0
- package/dist/inspection.js +2 -0
- package/dist/lifetime-types.d.ts +214 -0
- package/dist/lifetime-types.js +2 -0
- package/dist/lifetime.d.ts +42 -0
- package/dist/lifetime.js +31 -0
- package/dist/module-types.d.ts +182 -0
- package/dist/module-types.js +2 -0
- package/dist/module.d.ts +45 -0
- package/dist/module.js +118 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.js +22 -0
- package/dist/observers.d.ts +71 -0
- package/dist/observers.js +58 -0
- package/dist/persistent-map.d.ts +29 -0
- package/dist/persistent-map.js +146 -0
- package/dist/persistent-sequence.d.ts +9 -0
- package/dist/persistent-sequence.js +20 -0
- package/dist/plugins.d.ts +32 -0
- package/dist/plugins.js +82 -0
- package/dist/provider-execution.d.ts +59 -0
- package/dist/provider-execution.js +271 -0
- package/dist/provider-operations.d.ts +59 -0
- package/dist/provider-operations.js +38 -0
- package/dist/provider.d.ts +199 -0
- package/dist/provider.js +158 -0
- package/dist/registration.d.ts +32 -0
- package/dist/registration.js +45 -0
- package/dist/replacement-types.d.ts +30 -0
- package/dist/replacement-types.js +2 -0
- package/dist/runtime.d.ts +90 -0
- package/dist/runtime.js +428 -0
- package/dist/scope-selection.d.ts +6 -0
- package/dist/scope-selection.js +62 -0
- package/dist/scope-types.d.ts +58 -0
- package/dist/scope-types.js +2 -0
- package/dist/startup.d.ts +14 -0
- package/dist/startup.js +141 -0
- package/dist/token-types.d.ts +67 -0
- package/dist/token-types.js +2 -0
- package/dist/tokens.d.ts +47 -0
- package/dist/tokens.js +69 -0
- package/dist/types.d.ts +174 -0
- package/dist/types.js +2 -0
- package/package.json +64 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScopeAcquisitions = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const errors_2 = require("./errors");
|
|
6
|
+
const provider_execution_1 = require("./provider-execution");
|
|
7
|
+
const acquisition_family_1 = require("./acquisition-family");
|
|
8
|
+
/** Mutable, runtime-local attempts. Binding descriptions never carry ownership. */
|
|
9
|
+
class ScopeAcquisitions {
|
|
10
|
+
graph;
|
|
11
|
+
context;
|
|
12
|
+
parent;
|
|
13
|
+
ownerId = Symbol('owner');
|
|
14
|
+
cache = new Map();
|
|
15
|
+
attempts = new Map();
|
|
16
|
+
retired = new Map();
|
|
17
|
+
failures = [];
|
|
18
|
+
invocationSequence = 0;
|
|
19
|
+
// Insertion order is successful ownership acceptance order, including promises.
|
|
20
|
+
owned = new Map();
|
|
21
|
+
state = 'open';
|
|
22
|
+
closing;
|
|
23
|
+
controller;
|
|
24
|
+
acquisitionContext;
|
|
25
|
+
cancellationStarted = false;
|
|
26
|
+
cancellationCause;
|
|
27
|
+
shared;
|
|
28
|
+
family;
|
|
29
|
+
constructor(graph, context, parent, shared = []) {
|
|
30
|
+
this.graph = graph;
|
|
31
|
+
this.context = context;
|
|
32
|
+
this.parent = parent;
|
|
33
|
+
this.shared = new Set(shared);
|
|
34
|
+
this.family = parent?.family ?? new acquisition_family_1.AcquisitionFamily();
|
|
35
|
+
}
|
|
36
|
+
owner(bindingId) {
|
|
37
|
+
if (this.parent && this.shared.has(bindingId))
|
|
38
|
+
return this.parent;
|
|
39
|
+
if (this.graph.registration(bindingId).lifetime.kind !== 'root')
|
|
40
|
+
return this;
|
|
41
|
+
// A child override introduces a new identity absent from older graphs.
|
|
42
|
+
// Inherited identities retain the earliest graph and its dependency context.
|
|
43
|
+
let owner = this;
|
|
44
|
+
for (let ancestor = this.parent; ancestor; ancestor = ancestor.parent) {
|
|
45
|
+
if (ancestor.graph.hasBinding(bindingId))
|
|
46
|
+
owner = ancestor;
|
|
47
|
+
}
|
|
48
|
+
return owner;
|
|
49
|
+
}
|
|
50
|
+
resolve(key) {
|
|
51
|
+
this.assertOpen();
|
|
52
|
+
return this.takeExposed(this.resolveBinding(this.graph.publicBinding(key)));
|
|
53
|
+
}
|
|
54
|
+
resolveAll(key) {
|
|
55
|
+
this.assertOpen();
|
|
56
|
+
return this.resolveCollection(key);
|
|
57
|
+
}
|
|
58
|
+
resolveCollection(key, from) {
|
|
59
|
+
return Object.freeze(this.graph.contributionBindings(key).map(id => this.takeExposed(this.resolveBinding(id, from))));
|
|
60
|
+
}
|
|
61
|
+
async acquire(key) {
|
|
62
|
+
this.assertOpen();
|
|
63
|
+
const attempt = this.resolveBinding(this.graph.publicBinding(key));
|
|
64
|
+
this.takeExposed(attempt);
|
|
65
|
+
await attempt.execution.ready();
|
|
66
|
+
}
|
|
67
|
+
takeExposed(attempt) {
|
|
68
|
+
const value = attempt.exposed;
|
|
69
|
+
// Aliases and owner routing have already selected the canonical lifetime.
|
|
70
|
+
if (attempt.transient)
|
|
71
|
+
attempt.exposed = undefined;
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
inspect(bindingId, path = []) {
|
|
75
|
+
if (this.parent && this.shared.has(bindingId))
|
|
76
|
+
return this.parent.inspect(bindingId, path);
|
|
77
|
+
const alias = this.graph.registration(bindingId).alias;
|
|
78
|
+
if (alias !== undefined) {
|
|
79
|
+
this.assertAliasPath(bindingId, path);
|
|
80
|
+
return this.inspect(this.graph.dependency(bindingId, alias), [...path, bindingId]);
|
|
81
|
+
}
|
|
82
|
+
const owner = this.owner(bindingId);
|
|
83
|
+
if (owner !== this)
|
|
84
|
+
return owner.inspect(bindingId);
|
|
85
|
+
const snapshots = [];
|
|
86
|
+
for (const attempt of this.attempts.values()) {
|
|
87
|
+
if (attempt.bindingId !== bindingId)
|
|
88
|
+
continue;
|
|
89
|
+
snapshots.push(Object.freeze({
|
|
90
|
+
acquisitionId: attempt.id,
|
|
91
|
+
state: attempt.state,
|
|
92
|
+
acquisitionMetadata: attempt.execution.inspectFrames(),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
return Object.freeze(snapshots);
|
|
96
|
+
}
|
|
97
|
+
isTransient(bindingId, path = []) {
|
|
98
|
+
if (this.parent && this.shared.has(bindingId))
|
|
99
|
+
return this.parent.isTransient(bindingId, path);
|
|
100
|
+
const description = this.graph.registration(bindingId);
|
|
101
|
+
if (description.alias === undefined)
|
|
102
|
+
return description.lifetime.kind === 'transient';
|
|
103
|
+
this.assertAliasPath(bindingId, path);
|
|
104
|
+
return this.isTransient(this.graph.dependency(bindingId, description.alias), [...path, bindingId]);
|
|
105
|
+
}
|
|
106
|
+
/** Relationship uses the effective owner graph; frames use canonical attempts. */
|
|
107
|
+
inspectDescription(bindingId) {
|
|
108
|
+
if (this.parent && this.shared.has(bindingId))
|
|
109
|
+
return this.parent.inspectDescription(bindingId);
|
|
110
|
+
const description = this.graph.registration(bindingId);
|
|
111
|
+
if (description.alias !== undefined) {
|
|
112
|
+
const target = this.graph.dependency(bindingId, description.alias);
|
|
113
|
+
return { registrationMetadata: description.metadata, aliasTarget: Object.freeze({ bindingId: target, label: this.graph.label(target) }) };
|
|
114
|
+
}
|
|
115
|
+
const owner = this.owner(bindingId);
|
|
116
|
+
return owner === this ? { registrationMetadata: description.metadata } : owner.inspectDescription(bindingId);
|
|
117
|
+
}
|
|
118
|
+
assertAliasPath(bindingId, path) {
|
|
119
|
+
if (path.includes(bindingId))
|
|
120
|
+
throw (0, errors_1.libraryError)('DI_BAG_CYCLE', `alias cycle: ${[...path, bindingId].map(id => this.graph.label(id)).join(' -> ')}`, { path: Object.freeze([...path, bindingId].map(id => this.graph.label(id))) });
|
|
121
|
+
}
|
|
122
|
+
assertOpen() {
|
|
123
|
+
if (this.state !== 'open')
|
|
124
|
+
throw (0, errors_1.libraryError)(this.state === 'closing' ? 'DI_BAG_CLOSING' : 'DI_BAG_CLOSED', `bag is ${this.state}`, { state: this.state });
|
|
125
|
+
}
|
|
126
|
+
close(beforeDispose, cause) {
|
|
127
|
+
if (this.closing)
|
|
128
|
+
return this.closing;
|
|
129
|
+
this.state = 'closing';
|
|
130
|
+
// Publish the barrier before invoking any finalizer, including reentrant ones.
|
|
131
|
+
this.closing = Promise.resolve().then(() => {
|
|
132
|
+
// Every descendant admission gate is closed before abort listeners run.
|
|
133
|
+
this.cancellationStarted = true;
|
|
134
|
+
this.cancellationCause = cause;
|
|
135
|
+
this.controller?.abort(cause);
|
|
136
|
+
return this.disposeAll(beforeDispose);
|
|
137
|
+
});
|
|
138
|
+
return this.closing;
|
|
139
|
+
}
|
|
140
|
+
getContext() {
|
|
141
|
+
if (!this.acquisitionContext) {
|
|
142
|
+
this.controller = new AbortController();
|
|
143
|
+
if (this.cancellationStarted)
|
|
144
|
+
this.controller.abort(this.cancellationCause);
|
|
145
|
+
this.acquisitionContext = Object.freeze({ signal: this.controller.signal });
|
|
146
|
+
}
|
|
147
|
+
return this.acquisitionContext;
|
|
148
|
+
}
|
|
149
|
+
resolveBinding(bindingId, from, path = []) {
|
|
150
|
+
// Sharing an alias borrows its lexical parent graph before following targets.
|
|
151
|
+
if (this.parent && this.shared.has(bindingId))
|
|
152
|
+
return this.parent.resolveBinding(bindingId, from, path);
|
|
153
|
+
const description = this.graph.registration(bindingId);
|
|
154
|
+
if (description.alias !== undefined) {
|
|
155
|
+
this.assertAliasPath(bindingId, path);
|
|
156
|
+
return this.resolveBinding(this.graph.dependency(bindingId, description.alias), from, [...path, bindingId]);
|
|
157
|
+
}
|
|
158
|
+
const { lifetime } = description;
|
|
159
|
+
// Validate before routing/cache lookup; retained proxies keep their boundary.
|
|
160
|
+
if (lifetime.kind === 'scoped' && from?.strictRoot !== undefined) {
|
|
161
|
+
throw (0, errors_1.libraryError)('DI_BAG_LIFETIME_DEPENDENCY', `root lifetime cannot capture scoped dependency: ${from.strictRoot} -> ${this.graph.label(bindingId)}`, { consumer: from.strictRoot, dependency: this.graph.label(bindingId), lifetime: 'root' });
|
|
162
|
+
}
|
|
163
|
+
const owner = this.owner(bindingId);
|
|
164
|
+
if (owner !== this)
|
|
165
|
+
return owner.resolveBinding(bindingId, from);
|
|
166
|
+
const cached = this.cache.get(bindingId);
|
|
167
|
+
if (cached) {
|
|
168
|
+
if (from)
|
|
169
|
+
this.family.recordEdge(from, cached);
|
|
170
|
+
// A factory or then getter can reenter through public resolve as well.
|
|
171
|
+
if (cached.state === 'creating')
|
|
172
|
+
this.family.ancestry(bindingId, this.ownerId, cached.label, from);
|
|
173
|
+
return cached;
|
|
174
|
+
}
|
|
175
|
+
const ancestry = this.family.ancestry(bindingId, this.ownerId, this.graph.label(bindingId), from);
|
|
176
|
+
const execution = new provider_execution_1.ProviderExecution({
|
|
177
|
+
accepted: () => { this.owned.set(attempt.id, attempt); },
|
|
178
|
+
settled: () => {
|
|
179
|
+
if (attempt.execution.state === 'failed') {
|
|
180
|
+
this.observeAttempt(attempt, 'acquisition-failed', attempt.execution.error);
|
|
181
|
+
this.retire(attempt);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
attempt.state = 'ready';
|
|
185
|
+
this.family.deactivate(attempt);
|
|
186
|
+
this.observeAttempt(attempt, 'acquisition-ready');
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
drained: () => {
|
|
190
|
+
if (attempt.execution instanceof provider_execution_1.ProviderExecution)
|
|
191
|
+
attempt.execution = attempt.execution.compact();
|
|
192
|
+
},
|
|
193
|
+
...(this.context.observers ? {
|
|
194
|
+
cleanupStarted: () => this.observeAttempt(attempt, 'cleanup-started'),
|
|
195
|
+
cleanupCompleted: (outcome) => {
|
|
196
|
+
this.context.observers.emit({ ...this.eventFields(attempt), kind: 'cleanup-completed', outcome });
|
|
197
|
+
},
|
|
198
|
+
} : {}),
|
|
199
|
+
invoking: () => this.invocationSequence++,
|
|
200
|
+
cleanupFailed: (sequence, error) => {
|
|
201
|
+
if (this.context.observers)
|
|
202
|
+
this.context.observers.emit({ ...this.eventFields(attempt), kind: 'cleanup-failed', disposalSequence: sequence, error });
|
|
203
|
+
this.failures.push({ sequence, acquisitionId: attempt.id, bindingId: attempt.bindingId, label: attempt.label, error });
|
|
204
|
+
},
|
|
205
|
+
}, description, this.context);
|
|
206
|
+
const attempt = {
|
|
207
|
+
id: Symbol(this.graph.label(bindingId)),
|
|
208
|
+
bindingId,
|
|
209
|
+
ownerId: this.ownerId,
|
|
210
|
+
label: this.graph.label(bindingId),
|
|
211
|
+
dependencies: new Set(),
|
|
212
|
+
ancestry,
|
|
213
|
+
strictRoot: lifetime.kind === 'root'
|
|
214
|
+
? lifetime.allowScopedDependencies ? undefined : this.graph.label(bindingId)
|
|
215
|
+
: from?.strictRoot,
|
|
216
|
+
state: 'creating',
|
|
217
|
+
transient: lifetime.kind === 'transient',
|
|
218
|
+
exposed: undefined,
|
|
219
|
+
execution,
|
|
220
|
+
};
|
|
221
|
+
if (lifetime.kind !== 'transient')
|
|
222
|
+
this.cache.set(bindingId, attempt);
|
|
223
|
+
this.attempts.set(attempt.id, attempt);
|
|
224
|
+
this.family.add(attempt);
|
|
225
|
+
if (from)
|
|
226
|
+
this.family.recordEdge(from, attempt);
|
|
227
|
+
const read = (key, optional = false, all = false) => {
|
|
228
|
+
// Only this attempt's in-flight factory can discover dependencies in close.
|
|
229
|
+
if (this.state === 'closed' || (this.state === 'closing' && !attempt.execution.sourceInFlight)) {
|
|
230
|
+
throw (0, errors_1.libraryError)(this.state === 'closing' ? 'DI_BAG_CLOSING' : 'DI_BAG_CLOSED', `bag is ${this.state}`, { state: this.state });
|
|
231
|
+
}
|
|
232
|
+
if (all)
|
|
233
|
+
return this.resolveCollection(key, attempt);
|
|
234
|
+
const target = this.graph.findDependency(bindingId, key);
|
|
235
|
+
if (target === undefined && !optional) {
|
|
236
|
+
const path = this.family.dependencyPath(attempt, String(key));
|
|
237
|
+
throw (0, errors_1.libraryError)('DI_BAG_MISSING_DEPENDENCY', `Cannot resolve ${JSON.stringify(attempt.label)}: dependency ${JSON.stringify(String(key))} is not registered. Resolution path: ${path.join(' -> ')}.`, { operation: 'resolve', consumer: attempt.label, dependency: key, path });
|
|
238
|
+
}
|
|
239
|
+
return target === undefined ? undefined : this.takeExposed(this.resolveBinding(target, attempt));
|
|
240
|
+
};
|
|
241
|
+
const references = new Map(description.references.map(reference => [reference.slot, reference]));
|
|
242
|
+
const deps = new Proxy(Object.create(null), {
|
|
243
|
+
get: (_, key) => {
|
|
244
|
+
const reference = typeof key === 'symbol' ? references.get(key) : undefined;
|
|
245
|
+
if (reference)
|
|
246
|
+
return reference.kind === 'lazy' ? () => read(reference.key)
|
|
247
|
+
: read(reference.key, reference.kind === 'optional', reference.kind === 'all');
|
|
248
|
+
if (typeof key === 'symbol' && !description.tokenKeys.includes(key))
|
|
249
|
+
return undefined;
|
|
250
|
+
return read(key);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
this.observeAttempt(attempt, 'acquisition-started');
|
|
254
|
+
this.family.enter(attempt);
|
|
255
|
+
const directSource = !description.contextual && !description.operations.length;
|
|
256
|
+
try {
|
|
257
|
+
let value;
|
|
258
|
+
if (directSource) {
|
|
259
|
+
const { create } = description;
|
|
260
|
+
value = create(deps);
|
|
261
|
+
execution.publishSource(value, description);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
value = execution.evaluate(description, deps, () => this.getContext());
|
|
265
|
+
}
|
|
266
|
+
attempt.exposed = value;
|
|
267
|
+
attempt.state = attempt.execution.state;
|
|
268
|
+
if (attempt.state === 'ready') {
|
|
269
|
+
this.family.deactivate(attempt);
|
|
270
|
+
this.observeAttempt(attempt, 'acquisition-ready');
|
|
271
|
+
}
|
|
272
|
+
attempt.execution = execution.compact();
|
|
273
|
+
return attempt;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (directSource)
|
|
277
|
+
execution.sourceInFlight = false;
|
|
278
|
+
this.observeAttempt(attempt, 'acquisition-failed', error);
|
|
279
|
+
this.retire(attempt);
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
this.family.leave();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
eventFields(attempt) {
|
|
287
|
+
const description = this.graph.registration(attempt.bindingId);
|
|
288
|
+
return {
|
|
289
|
+
scopeId: this.ownerId, bindingId: attempt.bindingId, acquisitionId: attempt.id,
|
|
290
|
+
label: attempt.label, lifetime: description.lifetime.kind,
|
|
291
|
+
registrationMetadata: Object.freeze({ ...description.metadata }), acquisitionMetadata: attempt.execution.inspectFrames(),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
observeAttempt(attempt, kind, error) {
|
|
295
|
+
if (!this.context.observers)
|
|
296
|
+
return;
|
|
297
|
+
const fields = this.eventFields(attempt);
|
|
298
|
+
const event = kind === 'acquisition-failed' ? { ...fields, kind, error } : { ...fields, kind };
|
|
299
|
+
this.context.observers.emit(event);
|
|
300
|
+
}
|
|
301
|
+
retire(attempt) {
|
|
302
|
+
if (this.cache.get(attempt.bindingId) === attempt)
|
|
303
|
+
this.cache.delete(attempt.bindingId);
|
|
304
|
+
attempt.state = 'failed';
|
|
305
|
+
this.family.deactivate(attempt);
|
|
306
|
+
attempt.exposed = undefined;
|
|
307
|
+
// No consumer acquired this failed exposed value. Keep this attempt's
|
|
308
|
+
// outgoing edges and pending work, but abandon unsuccessful incoming reads.
|
|
309
|
+
this.family.retireIncoming(attempt);
|
|
310
|
+
if (this.retired.has(attempt.id))
|
|
311
|
+
return;
|
|
312
|
+
const release = () => {
|
|
313
|
+
this.family.release(attempt);
|
|
314
|
+
attempt.dependencies.clear();
|
|
315
|
+
attempt.execution.release();
|
|
316
|
+
this.attempts.delete(attempt.id);
|
|
317
|
+
this.owned.delete(attempt.id);
|
|
318
|
+
this.retired.delete(attempt.id);
|
|
319
|
+
};
|
|
320
|
+
// Incoming IDs may dangle; never substitute a cached retry's identity.
|
|
321
|
+
if (!attempt.execution.hasOwnership && !attempt.execution.work.length) {
|
|
322
|
+
release();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const cleanup = attempt.execution.dispose().then(release);
|
|
326
|
+
this.retired.set(attempt.id, cleanup);
|
|
327
|
+
}
|
|
328
|
+
async disposeAll(beforeDispose) {
|
|
329
|
+
let failures = [];
|
|
330
|
+
try {
|
|
331
|
+
if (beforeDispose)
|
|
332
|
+
await beforeDispose;
|
|
333
|
+
// Sources and projections can still acquire dependencies or retire work.
|
|
334
|
+
while (true) {
|
|
335
|
+
const work = [...this.retired.values(), ...[...this.attempts.values()].flatMap(attempt => attempt.execution.work)];
|
|
336
|
+
if (!work.length)
|
|
337
|
+
break;
|
|
338
|
+
await Promise.all(work);
|
|
339
|
+
}
|
|
340
|
+
const ordered = [];
|
|
341
|
+
const visited = new Set();
|
|
342
|
+
const stack = [];
|
|
343
|
+
const enter = (id) => {
|
|
344
|
+
if (visited.has(id))
|
|
345
|
+
return;
|
|
346
|
+
visited.add(id);
|
|
347
|
+
const attempt = this.attempts.get(id);
|
|
348
|
+
if (!attempt)
|
|
349
|
+
return;
|
|
350
|
+
stack.push({ attempt, dependencies: attempt.dependencies.values() });
|
|
351
|
+
};
|
|
352
|
+
// Explicit DFS frames preserve dependency/insertion order without using
|
|
353
|
+
// the JavaScript call stack for a potentially deep acquisition graph.
|
|
354
|
+
for (const id of this.owned.keys()) {
|
|
355
|
+
enter(id);
|
|
356
|
+
while (stack.length) {
|
|
357
|
+
const frame = stack[stack.length - 1];
|
|
358
|
+
const next = frame.dependencies.next();
|
|
359
|
+
if (!next.done) {
|
|
360
|
+
enter(next.value);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
stack.pop();
|
|
364
|
+
if (frame.attempt.execution.hasOwnership)
|
|
365
|
+
ordered.push(frame.attempt);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const attempt of ordered.reverse()) {
|
|
369
|
+
attempt.state = 'disposing';
|
|
370
|
+
await attempt.execution.dispose();
|
|
371
|
+
attempt.state = 'disposed';
|
|
372
|
+
}
|
|
373
|
+
failures = [...this.failures].sort((a, b) => a.sequence - b.sequence)
|
|
374
|
+
.map(({ acquisitionId, bindingId, label, error }) => ({ acquisitionId, bindingId, label, error }));
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
this.state = 'closed';
|
|
378
|
+
for (const attempt of this.attempts.values()) {
|
|
379
|
+
this.family.retireIncoming(attempt);
|
|
380
|
+
this.family.release(attempt);
|
|
381
|
+
attempt.dependencies.clear();
|
|
382
|
+
attempt.exposed = undefined;
|
|
383
|
+
attempt.execution.release();
|
|
384
|
+
}
|
|
385
|
+
this.cache.clear();
|
|
386
|
+
this.attempts.clear();
|
|
387
|
+
this.retired.clear();
|
|
388
|
+
this.failures.length = 0;
|
|
389
|
+
this.owned.clear();
|
|
390
|
+
}
|
|
391
|
+
if (failures.length > 0)
|
|
392
|
+
throw new errors_2.DiBagCleanupError(failures);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
exports.ScopeAcquisitions = ScopeAcquisitions;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Provider, ProviderOutput } from './provider';
|
|
2
|
+
import type { Registrations } from './registration';
|
|
3
|
+
import type { TokenBase, TokenService } from './tokens';
|
|
4
|
+
import type { SelectionKey, TokenDependencyContract, ValidToken, WrongToken } from './token-types';
|
|
5
|
+
import type { Singleton, Unsatisfied } from './types';
|
|
6
|
+
export type AliasSelection = string | TokenBase;
|
|
7
|
+
export type AliasAdmission<T> = Singleton<T> extends true ? unknown : ValidToken<T> extends true ? unknown : Unsatisfied<'alias requires one singleton name or genuine token', {}>;
|
|
8
|
+
export type AliasTarget<R extends Registrations, T> = T extends string ? T extends keyof R ? unknown : Unsatisfied<'alias requires an existing named target', {}> : [WrongToken<T, R>] extends [never] ? unknown : Unsatisfied<'token dependency has an incompatible or opaque contract', {}>;
|
|
9
|
+
/** Resolve the service type exposed by a possible alias target. */
|
|
10
|
+
export type AliasOutput<R extends Registrations, T> = T extends string ? T extends keyof R ? ProviderOutput<R[T]> : never : TokenService<T>;
|
|
11
|
+
export type AliasDestination<R extends Registrations, D, T> = D extends TokenBase ? [AliasOutput<R, T>] extends [TokenService<D>] ? unknown : Unsatisfied<'alias output is not assignable to destination service', {}> : unknown;
|
|
12
|
+
/** A provider contract that forwards a destination to a canonical target acquisition. */
|
|
13
|
+
export type AliasRegistration<R extends Registrations, D, T> = Provider<(this: void, deps: T extends string ? Record<T, AliasOutput<R, T>> : Record<never, never>) => AliasOutput<R, T>, Readonly<object>, readonly unknown[], TokenDependencyContract<T extends TokenBase ? readonly [T] : readonly [], D extends TokenBase ? D : never> & {
|
|
14
|
+
readonly alias: SelectionKey<T>;
|
|
15
|
+
}, unknown>;
|
|
16
|
+
/** The single registration-map entry introduced by an alias operation. */
|
|
17
|
+
export type AliasEntries<R extends Registrations, D, T> = Record<SelectionKey<D>, AliasRegistration<R, D, T>>;
|
|
18
|
+
/** Reflected generic methods cannot introduce a checked singleton destination. */
|
|
19
|
+
export type AliasEntry<R extends Registrations, D, T> = unknown extends AliasAdmission<D> & AliasAdmission<T> ? {
|
|
20
|
+
key: SelectionKey<D>;
|
|
21
|
+
registration: AliasRegistration<R, D, T>;
|
|
22
|
+
} : never;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Registration } from './registration';
|
|
2
|
+
import type { BindingKey } from './runtime';
|
|
3
|
+
/** Authenticate both selections before constructing any retained registration. */
|
|
4
|
+
export declare function aliasEntry(destination: unknown, target: unknown, hasKey: (key: BindingKey) => boolean): readonly [BindingKey, Registration];
|
package/dist/aliases.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aliasEntry = aliasEntry;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const provider_1 = require("./provider");
|
|
6
|
+
const provider_operations_1 = require("./provider-operations");
|
|
7
|
+
const tokens_1 = require("./tokens");
|
|
8
|
+
/** Authenticate both selections before constructing any retained registration. */
|
|
9
|
+
function aliasEntry(destination, target, hasKey) {
|
|
10
|
+
const key = typeof destination === 'string' ? destination : (0, tokens_1.readTokenKey)(destination);
|
|
11
|
+
const targetKey = typeof target === 'string' ? target : (0, tokens_1.readTokenKey)(target);
|
|
12
|
+
if (hasKey(key))
|
|
13
|
+
throw (0, errors_1.libraryError)('DI_BAG_DUPLICATE_REGISTRATION', `duplicate registration: ${String(key)}`, { operation: 'alias', key });
|
|
14
|
+
if (typeof target === 'string' && !hasKey(targetKey))
|
|
15
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_ALIAS', 'alias requires an existing named target', { operation: 'alias', target: targetKey });
|
|
16
|
+
const handle = (0, provider_1.createProvider)();
|
|
17
|
+
// This source is unreachable: canonical routing happens before evaluation.
|
|
18
|
+
// Raw prevents an alias from inventing Promise-classification requirements.
|
|
19
|
+
(0, provider_operations_1.retainDescription)(handle, Object.freeze({
|
|
20
|
+
...(0, provider_operations_1.sourceDescription)(() => { throw (0, errors_1.libraryError)('DI_BAG_INTERNAL_STATE', 'alias source cannot execute', {}); }, undefined, [], 'raw'),
|
|
21
|
+
alias: targetKey,
|
|
22
|
+
}));
|
|
23
|
+
return [key, handle];
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Acquired, AcquisitionMode, NativeOutput, StageOptions } from './acquisition-mode';
|
|
2
|
+
import type { Provider } from './provider';
|
|
3
|
+
import type { DependencyReference } from './dependency-references';
|
|
4
|
+
import type { TokenArguments, ReferenceGraph, DependencyTupleAdmission } from './token-types';
|
|
5
|
+
import type { Unsatisfied } from './types';
|
|
6
|
+
type OutputFactory<O> = () => O;
|
|
7
|
+
/** Compile-time admission that checks supplied token values against a callable's parameter tuple. */
|
|
8
|
+
export type CompositionArguments<A extends readonly unknown[], P extends readonly unknown[]> = [A] extends [P] ? unknown : Unsatisfied<'composition arguments must match the declared parameter tuple', {
|
|
9
|
+
supplied: A;
|
|
10
|
+
parameters: P;
|
|
11
|
+
}>;
|
|
12
|
+
/** A receiver-free positional callback matching the values supplied by a dependency tuple. */
|
|
13
|
+
export type CompositionFunction<T extends readonly DependencyReference[], O = unknown> = TokenArguments<T> extends [...infer A] ? (this: void, ...args: A) => O : never;
|
|
14
|
+
/**
|
|
15
|
+
* Adapt a positional function without awaiting its arguments or return value.
|
|
16
|
+
* @param tokens - A finite tuple of typed tokens and dependency references.
|
|
17
|
+
* @param callback - The receiver-free function to call in tuple order.
|
|
18
|
+
* @param modeOptions - Optional acquisition mode for the function result.
|
|
19
|
+
* @returns A lazy provider retaining the dependency graph and exact return type.
|
|
20
|
+
* @typeParam F - The exact positional function signature retained by the provider.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fromFunction<const T extends readonly DependencyReference[], F extends CompositionFunction<NoInfer<T>, 'nativePromise' extends M ? Promise<unknown> : unknown>, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, callback: F & CompositionArguments<TokenArguments<NoInfer<T>>, Parameters<NoInfer<F>>> & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<OutputFactory<ReturnType<F>>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<ReturnType<F>, M>>;
|
|
23
|
+
/**
|
|
24
|
+
* Adapt a positional function whose parameters exactly match the selected dependency values.
|
|
25
|
+
* @param tokens - A finite tuple of typed tokens and dependency references.
|
|
26
|
+
* @param callback - The function invoked once per provider acquisition with no receiver.
|
|
27
|
+
* @param modeOptions - Optional `auto`, `raw`, or `nativePromise` result classification.
|
|
28
|
+
* @returns A reusable provider; no dependency or result is implicitly awaited.
|
|
29
|
+
* @typeParam F - The exact positional function signature retained by the provider.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fromFunction<const T extends readonly DependencyReference[], F extends CompositionFunction<NoInfer<T>>, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, callback: F & CompositionArguments<TokenArguments<NoInfer<T>>, Parameters<NoInfer<F>>> & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<OutputFactory<ReturnType<F>>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<ReturnType<F>, M>>;
|
|
32
|
+
/**
|
|
33
|
+
* Adapt a concrete constructor while preserving its prototype, private fields, and `new.target`.
|
|
34
|
+
* @param tokens - A finite tuple whose dependency values match the constructor parameters.
|
|
35
|
+
* @param constructor - The concrete class or constructable function to instantiate.
|
|
36
|
+
* @param modeOptions - Optional acquisition mode for the constructed result.
|
|
37
|
+
* @returns A lazy provider that constructs one instance per acquisition attempt.
|
|
38
|
+
* @throws When the supplied runtime value is not constructable.
|
|
39
|
+
*/
|
|
40
|
+
export declare function fromClass<const T extends readonly DependencyReference[], C extends new (...args: TokenArguments<NoInfer<T>>) => unknown, M extends AcquisitionMode = 'auto'>(tokens: T & DependencyTupleAdmission<T>, constructor: C & CompositionArguments<TokenArguments<NoInfer<T>>, ConstructorParameters<NoInfer<C>>> & NativeOutput<InstanceType<NoInfer<C>>, NoInfer<M>>, ...modeOptions: StageOptions<M>): Provider<() => InstanceType<C>, Readonly<{}>, readonly [], ReferenceGraph<T>, Acquired<InstanceType<C>, M>>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fromFunction = fromFunction;
|
|
4
|
+
exports.fromClass = fromClass;
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const acquisition_mode_1 = require("./acquisition-mode");
|
|
7
|
+
const provider_1 = require("./provider");
|
|
8
|
+
const provider_operations_1 = require("./provider-operations");
|
|
9
|
+
const dependency_references_1 = require("./dependency-references");
|
|
10
|
+
function fromFunction(tokens, callback, ...modeOptions) {
|
|
11
|
+
const mode = (0, acquisition_mode_1.acquisitionMode)(modeOptions[0]);
|
|
12
|
+
const references = (0, dependency_references_1.snapshotReferences)(tokens);
|
|
13
|
+
if (typeof callback !== 'function')
|
|
14
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FUNCTION', 'fromFunction callback must be a function', { operation: 'fromFunction' });
|
|
15
|
+
const create = (deps) => Reflect.apply(callback, undefined, references.map(reference => Reflect.get(deps, reference.slot)));
|
|
16
|
+
const handle = (0, provider_1.createProvider)();
|
|
17
|
+
(0, provider_operations_1.retainDescription)(handle, (0, provider_operations_1.sourceDescription)(create, undefined, references.map(reference => reference.key), mode, false, references));
|
|
18
|
+
return handle;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Adapt a concrete constructor while preserving its prototype, private fields, and `new.target`.
|
|
22
|
+
* @param tokens - A finite tuple whose dependency values match the constructor parameters.
|
|
23
|
+
* @param constructor - The concrete class or constructable function to instantiate.
|
|
24
|
+
* @param modeOptions - Optional acquisition mode for the constructed result.
|
|
25
|
+
* @returns A lazy provider that constructs one instance per acquisition attempt.
|
|
26
|
+
* @throws When the supplied runtime value is not constructable.
|
|
27
|
+
*/
|
|
28
|
+
function fromClass(tokens, constructor, ...modeOptions) {
|
|
29
|
+
const mode = (0, acquisition_mode_1.acquisitionMode)(modeOptions[0]);
|
|
30
|
+
const references = (0, dependency_references_1.snapshotReferences)(tokens);
|
|
31
|
+
// A Proxy is constructable exactly when its target is. Its inert trap avoids
|
|
32
|
+
// running the target or reading its prototype, even when the target is a Proxy.
|
|
33
|
+
if (typeof constructor !== 'function')
|
|
34
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_CONSTRUCTOR', 'fromClass requires a concrete constructor', { operation: 'fromClass' });
|
|
35
|
+
try {
|
|
36
|
+
Reflect.construct(new Proxy(constructor, { construct: () => ({}) }), []);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_CONSTRUCTOR', 'fromClass requires a concrete constructor', { operation: 'fromClass' });
|
|
40
|
+
}
|
|
41
|
+
const create = (deps) => Reflect.construct(constructor, references.map(reference => Reflect.get(deps, reference.slot)));
|
|
42
|
+
const handle = (0, provider_1.createProvider)();
|
|
43
|
+
(0, provider_operations_1.retainDescription)(handle, (0, provider_operations_1.sourceDescription)(create, undefined, references.map(reference => reference.key), mode, false, references));
|
|
44
|
+
return handle;
|
|
45
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Registration, Registrations } from './registration';
|
|
2
|
+
import type { TokenBase, TokenKey, TokenService } from './tokens';
|
|
3
|
+
import type { ValidToken, TokenTupleAdmission, BindingOutput } from './token-types';
|
|
4
|
+
import type { CheckDependencyCompatibility, CheckDependencyCompleteness, Unsatisfied, Entry, RegistrationsFromEntries } from './types';
|
|
5
|
+
import type { ProviderCollectionTokens } from './provider';
|
|
6
|
+
import type { Module } from './module';
|
|
7
|
+
import type { RegistrationConstraints, PublicProvider, NeedConstraint, CheckedConstraints } from './module-types';
|
|
8
|
+
import type { LexicalContext, ModuleScope, Enclosed, RenamedContext } from './lifetime-types';
|
|
9
|
+
declare const contributionSite: unique symbol;
|
|
10
|
+
/** Each union member retains one independently checked provider and its group. */
|
|
11
|
+
export type Contribution<T extends TokenBase = TokenBase, V extends Registration = Registration, L = undefined> = {
|
|
12
|
+
readonly kind: 'contribution';
|
|
13
|
+
readonly token: T;
|
|
14
|
+
readonly registration: V;
|
|
15
|
+
readonly context: L;
|
|
16
|
+
};
|
|
17
|
+
/** The erased contribution contract retained by checked builders and modules. */
|
|
18
|
+
export type ContributionConstraint = Contribution<TokenBase, Registration, unknown>;
|
|
19
|
+
type Groups<C> = Extract<C, ContributionConstraint>;
|
|
20
|
+
type Same<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false;
|
|
21
|
+
type WrongMember<T, G> = G extends ContributionConstraint ? TokenKey<T> extends TokenKey<G['token']> ? Same<T, G['token']> extends true ? never : TokenKey<T> : never : never;
|
|
22
|
+
export type WrongGroup<T, C> = T extends unknown ? ValidToken<T> extends true ? WrongMember<T, Groups<C>> : 'opaque collection contract' : never;
|
|
23
|
+
export type CollectionMember<T, C> = [WrongGroup<T, C>] extends [never] ? unknown : Unsatisfied<'collection token has an incompatible or opaque contract', {}>;
|
|
24
|
+
type ContributionGraph<A extends Registrations, V extends Registration> = A & Record<typeof contributionSite, V>;
|
|
25
|
+
type WrongProvider<C, A extends Registrations> = C extends ContributionConstraint ? unknown extends CheckDependencyCompatibility<ContributionGraph<A, C['registration']>> ? never : C : never;
|
|
26
|
+
type ContributionFailures<C, A extends Registrations> = C extends ContributionConstraint ? {
|
|
27
|
+
readonly consumer: TokenKey<C['token']>;
|
|
28
|
+
readonly diagnostic: CheckDependencyCompatibility<ContributionGraph<A, C['registration']>>;
|
|
29
|
+
} : never;
|
|
30
|
+
type MissingProvider<C, A extends Registrations> = C extends ContributionConstraint ? unknown extends CheckDependencyCompleteness<ContributionGraph<A, C['registration']>> ? never : C : never;
|
|
31
|
+
type AllNeeds<C, A extends Registrations> = ProviderCollectionTokens<A[keyof A]> | (C extends ContributionConstraint ? ProviderCollectionTokens<C['registration']> : C extends {
|
|
32
|
+
kind: 'all';
|
|
33
|
+
token: infer T;
|
|
34
|
+
} ? T : never);
|
|
35
|
+
type GroupErrors<C, A extends Registrations> = WrongGroup<Groups<C>['token'] | AllNeeds<C, A>, C>;
|
|
36
|
+
export type CheckedContributions<C, A extends Registrations> = [Groups<C>] extends [never] ? unknown : [GroupErrors<C, A>] extends [never] ? [WrongProvider<C, A>] extends [never] ? unknown : Unsatisfied<'contribution service is incompatible with its consumer dependency contract', {
|
|
37
|
+
readonly failures: ContributionFailures<WrongProvider<C, A>, A>;
|
|
38
|
+
}> : Unsatisfied<'collection token has an incompatible or opaque contract', {}>;
|
|
39
|
+
export type CompleteContributions<C, A extends Registrations> = [MissingProvider<C, A>] extends [never] ? unknown : Unsatisfied<'required service registrations are missing', {
|
|
40
|
+
readonly contributions: MissingProvider<C, A>;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Retain a contribution's provider checks and lexical private-service context when
|
|
44
|
+
* its builder seals. A contribution retained from an inner installation is already
|
|
45
|
+
* projected; sealing only encloses its scope in this module's scope.
|
|
46
|
+
*/
|
|
47
|
+
export type ModuleContributionConstraints<C, R extends Registrations, P extends keyof R> = C extends ContributionConstraint ? C['context'] extends LexicalContext ? Contribution<C['token'], C['registration'], Enclosed<C['context'], ModuleScope<R, P>>> : Contribution<C['token'], PublicProvider<C['registration']>, ModuleScope<R, P> & {
|
|
48
|
+
readonly registration: C['registration'];
|
|
49
|
+
}> | RegistrationConstraints<C['registration'], R, P> : never;
|
|
50
|
+
export type RenamedContribution<C extends ContributionConstraint, Old extends string, New extends string> = C['context'] extends LexicalContext ? Contribution<C['token'], C['registration'], RenamedContext<C['context'], Old, New>> : C;
|
|
51
|
+
/** Project a module's typed-token collections as readonly service arrays. */
|
|
52
|
+
export type ModuleContributions<M> = M extends Module<infer _P, infer _R, infer C, infer _D> ? Readonly<{
|
|
53
|
+
[T in Groups<C>['token'] as TokenKey<T>]: ReadonlyArray<TokenService<T>>;
|
|
54
|
+
}> : never;
|
|
55
|
+
/** The checked generic `contribute` callable exposed by a builder. */
|
|
56
|
+
export type BuilderContribute<E extends Entry, C extends NeedConstraint> = <T extends TokenBase, V extends Registration>(token: T & TokenTupleAdmission<readonly [T]>, registration: V & Registration & BindingOutput<NoInfer<T>, NoInfer<V>> & CheckedConstraints<C | Contribution<NoInfer<T>, NoInfer<V>>, RegistrationsFromEntries<E>>, ...invalid: [T] extends [never] ? [never] : [V] extends [never] ? [never] : []) => import('./di-bag').Builder<E, C | Contribution<T, V>>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.contributionEntry = contributionEntry;
|
|
4
|
+
const registration_1 = require("./registration");
|
|
5
|
+
const tokens_1 = require("./tokens");
|
|
6
|
+
/** Authenticate and snapshot one entry before creating its independent binding. */
|
|
7
|
+
function contributionEntry(token, registration) {
|
|
8
|
+
const key = (0, tokens_1.readTokenKey)(token);
|
|
9
|
+
(0, registration_1.normalize)(registration);
|
|
10
|
+
return Object.freeze([key, registration]);
|
|
11
|
+
}
|