ripple-di 1.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/dist/index.mjs ADDED
@@ -0,0 +1,948 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { isPromise } from "node:util/types";
3
+ //#region src/errors.ts
4
+ /** Base class for errors thrown by Ripple DI. */
5
+ var RippleError = class extends Error {};
6
+ /** The dependency was requested without a provider or built-in factory. */
7
+ var MissingProviderError = class extends RippleError {
8
+ dependencyName;
9
+ path;
10
+ constructor(dependencyName, path) {
11
+ super(path.length > 1 ? `Dependency "${dependencyName}" has no provider while resolving ${path.join(" → ")}.` : `Dependency "${dependencyName}" has no provider.`);
12
+ this.dependencyName = dependencyName;
13
+ this.path = path;
14
+ this.name = "MissingProviderError";
15
+ }
16
+ };
17
+ /** A dependency was used with a different runtime than the one that defined it. */
18
+ var CrossRuntimeDependencyError = class extends RippleError {
19
+ dependencyName;
20
+ dependencyRuntimeName;
21
+ requestedRuntimeName;
22
+ constructor(dependencyName, dependencyRuntimeName, requestedRuntimeName) {
23
+ super(dependencyRuntimeName === requestedRuntimeName ? `Dependency "${dependencyName}" belongs to a different runtime instance also named "${dependencyRuntimeName}".` : `Dependency "${dependencyName}" belongs to runtime "${dependencyRuntimeName}" and cannot be used in runtime "${requestedRuntimeName}".`);
24
+ this.dependencyName = dependencyName;
25
+ this.dependencyRuntimeName = dependencyRuntimeName;
26
+ this.requestedRuntimeName = requestedRuntimeName;
27
+ this.name = "CrossRuntimeDependencyError";
28
+ }
29
+ };
30
+ /** `install()` was called while another installation or scope was unfinished. */
31
+ var InstallationConflictError = class extends RippleError {
32
+ runtimeName;
33
+ reason;
34
+ scopeNames;
35
+ constructor(runtimeName, reason, scopeNames = []) {
36
+ super(reason === "active-installation" ? `Runtime "${runtimeName}" already has an active installation. Close it before calling install() again.` : reason === "closing-installation" ? `Runtime "${runtimeName}" is still closing its previous installation. Await Installation.close() before calling install() again.` : `Runtime "${runtimeName}" still has unfinished ${scopeNames.length === 1 ? "scope" : "scopes"}: ${scopeNames.map((name) => `"${name}"`).join(", ")}. Close ${scopeNames.length === 1 ? "it" : "them"} before calling install().`);
37
+ this.runtimeName = runtimeName;
38
+ this.reason = reason;
39
+ this.scopeNames = scopeNames;
40
+ this.name = "InstallationConflictError";
41
+ }
42
+ };
43
+ /** The same dependency appears more than once in one provision list. */
44
+ var DuplicateProviderError = class extends RippleError {
45
+ dependencyName;
46
+ constructor(dependencyName) {
47
+ super(`Dependency "${dependencyName}" has more than one provision in the same scope.`);
48
+ this.dependencyName = dependencyName;
49
+ this.name = "DuplicateProviderError";
50
+ }
51
+ };
52
+ /** One owned-value provision was installed for more than one owner. */
53
+ var OwnedProvisionReuseError = class extends RippleError {
54
+ dependencyName;
55
+ constructor(dependencyName) {
56
+ super(`The owned-value provision for dependency "${dependencyName}" has already been installed in a scope or installation.`);
57
+ this.dependencyName = dependencyName;
58
+ this.name = "OwnedProvisionReuseError";
59
+ }
60
+ };
61
+ /** Dependency factories called one another in a cycle. */
62
+ var DependencyCycleError = class extends RippleError {
63
+ path;
64
+ constructor(path) {
65
+ super(`Dependency cycle: ${path.join(" → ")}.`);
66
+ this.path = path;
67
+ this.name = "DependencyCycleError";
68
+ }
69
+ };
70
+ /** A dependency factory returned a `Promise` instead of the value. */
71
+ var AsyncFactoryError = class extends RippleError {
72
+ dependencyName;
73
+ path;
74
+ constructor(dependencyName, path) {
75
+ super(`Factory for dependency "${dependencyName}" returned a Promise while resolving ${path.join(" → ")}. Factories are synchronous because dependency reads made after an await are not tracked. Wrap the result in asValue() when the promise itself is the value.`);
76
+ this.dependencyName = dependencyName;
77
+ this.path = path;
78
+ this.name = "AsyncFactoryError";
79
+ }
80
+ };
81
+ /** A dependency factory threw an error available through this error's `cause`. */
82
+ var FactoryError = class extends RippleError {
83
+ dependencyName;
84
+ path;
85
+ constructor(dependencyName, path, cause) {
86
+ super(`Factory for dependency "${dependencyName}" failed while resolving ${path.join(" → ")}.`, { cause });
87
+ this.dependencyName = dependencyName;
88
+ this.path = path;
89
+ this.name = "FactoryError";
90
+ }
91
+ };
92
+ /** An operation tried to use a scope that is retiring, closing, or closed. */
93
+ var ScopeClosedError = class extends RippleError {
94
+ targetName;
95
+ scopeName;
96
+ scopeId;
97
+ state;
98
+ constructor(targetName, scopeName, scopeId, state) {
99
+ super(`Scope "${scopeName}" (#${scopeId}) is ${state}; cannot use "${targetName}".`);
100
+ this.targetName = targetName;
101
+ this.scopeName = scopeName;
102
+ this.scopeId = scopeId;
103
+ this.state = state;
104
+ this.name = "ScopeClosedError";
105
+ }
106
+ };
107
+ /** A factory tried to read explicitly from a different scope. */
108
+ var CrossScopeResolutionError = class extends RippleError {
109
+ dependencyName;
110
+ factoryScopeName;
111
+ requestedScopeName;
112
+ constructor(dependencyName, factoryScopeName, requestedScopeName) {
113
+ super(`Factory in scope "${factoryScopeName}" cannot explicitly resolve "${dependencyName}" from scope "${requestedScopeName}".`);
114
+ this.dependencyName = dependencyName;
115
+ this.factoryScopeName = factoryScopeName;
116
+ this.requestedScopeName = requestedScopeName;
117
+ this.name = "CrossScopeResolutionError";
118
+ }
119
+ };
120
+ /** A dependency factory tried to manage runtime context or lifecycle. */
121
+ var FactoryScopeOperationError = class extends RippleError {
122
+ dependencyName;
123
+ operation;
124
+ constructor(dependencyName, operation) {
125
+ super(`Factory for dependency "${dependencyName}" cannot call ${operation}.`);
126
+ this.dependencyName = dependencyName;
127
+ this.operation = operation;
128
+ this.name = "FactoryScopeOperationError";
129
+ }
130
+ };
131
+ /** Code running in or started by a disposer used the runtime being closed. */
132
+ var DisposerContextError = class extends RippleError {
133
+ targetName;
134
+ scopeName;
135
+ scopeId;
136
+ constructor(targetName, scopeName, scopeId) {
137
+ super(`Cannot use "${targetName}" from code running in or started by a disposer for scope "${scopeName}" (#${scopeId}).`);
138
+ this.targetName = targetName;
139
+ this.scopeName = scopeName;
140
+ this.scopeId = scopeId;
141
+ this.name = "DisposerContextError";
142
+ }
143
+ };
144
+ /**
145
+ * A `withOverrides` callback created child scopes and returned without closing
146
+ * them.
147
+ *
148
+ * Ripple DI force-closes the leaked scopes before throwing this error.
149
+ */
150
+ var LeakedChildScopeError = class extends RippleError {
151
+ scopeName;
152
+ leakedChildCount;
153
+ constructor(scopeName, leakedChildCount) {
154
+ super(`Scope "${scopeName}" leaked ${leakedChildCount} child ${leakedChildCount === 1 ? "scope" : "scopes"}.`);
155
+ this.scopeName = scopeName;
156
+ this.leakedChildCount = leakedChildCount;
157
+ this.name = "LeakedChildScopeError";
158
+ }
159
+ };
160
+ //#endregion
161
+ //#region src/dependency.ts
162
+ let nextDependencyId = 1;
163
+ const dependencyNodes = /* @__PURE__ */ new WeakMap();
164
+ /** Fully initialized metadata object captured by its own callable dependency. */
165
+ var DependencyNodeImpl = class {
166
+ id = nextDependencyId++;
167
+ name;
168
+ runtime;
169
+ defaultFactory;
170
+ dispose;
171
+ dependency;
172
+ constructor(definition) {
173
+ this.name = definition.name ?? `dependency#${this.id}`;
174
+ this.runtime = definition.runtime;
175
+ this.defaultFactory = definition.defaultFactory;
176
+ this.dispose = definition.dispose;
177
+ this.dependency = (() => this.runtime.readCallable(this));
178
+ }
179
+ };
180
+ /** Creates a property-free callable whose metadata lives only in a `WeakMap`. */
181
+ function createDependency(definition) {
182
+ const node = new DependencyNodeImpl(definition);
183
+ dependencyNodes.set(node.dependency, node);
184
+ return node.dependency;
185
+ }
186
+ function nodeOf(dependency) {
187
+ const node = dependencyNodes.get(dependency);
188
+ if (!node) throw new TypeError("Value is not a dependency created by this copy of ripple-di. If it came from ripple-di, the package may be installed or bundled more than once.");
189
+ return node;
190
+ }
191
+ //#endregion
192
+ //#region src/value.ts
193
+ const markedValues = /* @__PURE__ */ new WeakMap();
194
+ /**
195
+ * Uses a factory result as the dependency value even when it is a `Promise`.
196
+ *
197
+ * A factory that returns a promise is normally rejected, because dependency
198
+ * reads made after an `await` are not tracked.
199
+ * Wrap the result when the promise itself is the value the dependency holds.
200
+ * A value that merely implements `then`, such as a query builder, is an
201
+ * ordinary value and does not need this.
202
+ */
203
+ function asValue(value) {
204
+ const marked = {};
205
+ markedValues.set(marked, { value });
206
+ return marked;
207
+ }
208
+ /** Returns the boxed payload, or `undefined` for an ordinary factory result. */
209
+ function markedValueOf(result) {
210
+ return typeof result === "object" && result !== null ? markedValues.get(result) : void 0;
211
+ }
212
+ //#endregion
213
+ //#region src/provide.ts
214
+ const provisionRecords = /* @__PURE__ */ new WeakMap();
215
+ const claimedOwnedProvisions = /* @__PURE__ */ new WeakSet();
216
+ function createProvision(dependency, spec) {
217
+ nodeOf(dependency);
218
+ const provision = {};
219
+ provisionRecords.set(provision, {
220
+ dependency,
221
+ spec
222
+ });
223
+ return provision;
224
+ }
225
+ function provisionOf(provision) {
226
+ const record = provisionRecords.get(provision);
227
+ if (!record) throw new TypeError("Value is not a provision created by this copy of ripple-di. If it came from ripple-di, the package may be installed or bundled more than once.");
228
+ return record;
229
+ }
230
+ /** Reads provision input as the list the rest of the library works with. */
231
+ function provisionListOf(input) {
232
+ return Array.isArray(input) ? input : [input];
233
+ }
234
+ /**
235
+ * Transfers each owned-value provision to one owner after list validation.
236
+ *
237
+ * Checking the complete list before recording claims keeps failed scope
238
+ * creation atomic.
239
+ */
240
+ function claimOwnedProvisions(provisions) {
241
+ const owned = provisions.filter((provision) => provisionOf(provision).spec.kind === "owned-value");
242
+ for (const provision of owned) if (claimedOwnedProvisions.has(provision)) throw new OwnedProvisionReuseError(nodeOf(provisionOf(provision).dependency).name);
243
+ for (const provision of owned) claimedOwnedProvisions.add(provision);
244
+ }
245
+ /**
246
+ * Uses an existing value for a dependency in a scope or installation.
247
+ *
248
+ * Ripple DI cleans up the value only when `options.dispose` is provided.
249
+ * Pass `true` to reuse cleanup configured by `defineDependency`, or pass a
250
+ * callback to override it.
251
+ * A function passed as the value remains a value rather than becoming a factory.
252
+ * A provision that transfers ownership can be used in only one scope or
253
+ * installation.
254
+ */
255
+ function provide(dependency, value, options = {}) {
256
+ if (markedValueOf(value)) throw new TypeError(`Dependency "${nodeOf(dependency).name}" was given an asValue() marker instead of a value. Pass the value itself to provide(), or return the marker from provideFactory().`);
257
+ if (options.dispose) {
258
+ const node = nodeOf(dependency);
259
+ const dispose = options.dispose === true ? node.dispose : options.dispose;
260
+ if (!dispose) throw new TypeError(`Dependency "${node.name}" has no dispose callback to reuse.`);
261
+ return createProvision(dependency, {
262
+ kind: "owned-value",
263
+ value,
264
+ dispose
265
+ });
266
+ }
267
+ return createProvision(dependency, {
268
+ kind: "value",
269
+ value
270
+ });
271
+ }
272
+ /**
273
+ * Creates a dependency override on demand in the receiving scope or
274
+ * installation.
275
+ *
276
+ * The factory must be synchronous.
277
+ * Calls to other dependencies inside it are tracked automatically.
278
+ * The receiving scope or installation owns the created value.
279
+ * When the dependency has a configured `dispose` callback, that callback
280
+ * cleans up the override when its owner closes.
281
+ */
282
+ function provideFactory(dependency, factory) {
283
+ return createProvision(dependency, {
284
+ kind: "factory",
285
+ factory
286
+ });
287
+ }
288
+ //#endregion
289
+ //#region src/evaluation.ts
290
+ const evaluationStack = [];
291
+ function currentEvaluation() {
292
+ return evaluationStack.at(-1);
293
+ }
294
+ /** Rejects scope management while a synchronous factory frame is active. */
295
+ function assertOutsideEvaluation(runtime, operation) {
296
+ const frame = currentEvaluation();
297
+ if (frame?.runtime === runtime) throw new FactoryScopeOperationError(frame.node.name, operation);
298
+ }
299
+ function pushEvaluation(frame) {
300
+ evaluationStack.push(frame);
301
+ }
302
+ function popEvaluation(frame) {
303
+ if (evaluationStack.pop() !== frame) throw new Error("ripple-di evaluation stack became inconsistent.");
304
+ }
305
+ function cycleStart(node, providerStamp) {
306
+ return evaluationStack.findIndex((frame) => frame.node === node && frame.providerStamp.identity === providerStamp.identity);
307
+ }
308
+ function resolutionPath(ending) {
309
+ const path = evaluationStack.map((frame) => frame.node.name);
310
+ if (ending && evaluationStack.at(-1)?.node !== ending) path.push(ending.name);
311
+ return path;
312
+ }
313
+ function framesFrom(index) {
314
+ return evaluationStack.slice(index);
315
+ }
316
+ //#endregion
317
+ //#region src/graph.ts
318
+ /** Internal key for traversing scope ancestry without exposing parent handles. */
319
+ const scopeParent = Symbol("scope-parent");
320
+ //#endregion
321
+ //#region src/resolution.ts
322
+ /** Internal resolution failure that must not be attributed to a user factory. */
323
+ var ResolutionInvariantError = class extends RippleError {
324
+ constructor(message) {
325
+ super(`Internal ripple-di resolution invariant failed: ${message}`);
326
+ this.name = "ResolutionInvariantError";
327
+ }
328
+ };
329
+ /** Public and callable reads are tracked against the frame that initiated them. */
330
+ function resolveTracked(scope, node) {
331
+ const consumerFrame = currentEvaluation();
332
+ try {
333
+ assertCompatible(scope, node);
334
+ if (consumerFrame && consumerFrame.runtime !== scope.runtime) throw new CrossRuntimeDependencyError(node.name, node.runtime.name, consumerFrame.runtime.name);
335
+ assertPubliclyReadable(scope, node.name);
336
+ if (consumerFrame && consumerFrame.scope !== scope) throw new CrossScopeResolutionError(node.name, consumerFrame.scope.name, scope.name);
337
+ const resolved = resolveUntracked(scope, node);
338
+ if (consumerFrame) recordDependency(consumerFrame, node, resolved.stamp);
339
+ return resolved.value;
340
+ } catch (error) {
341
+ if (consumerFrame) consumerFrame.hasFailedDependencyRead = true;
342
+ throw error;
343
+ }
344
+ }
345
+ /**
346
+ * Resolves without creating a dependency edge.
347
+ *
348
+ * Ancestor validation uses this path so implementation reads never become
349
+ * dependencies of the consumer factory currently on the synchronous stack.
350
+ */
351
+ function resolveUntracked(requestedScope, node) {
352
+ assertCompatible(requestedScope, node);
353
+ const cached = requestedScope.viewCache.get(asUnknownNode(node));
354
+ if (cached) {
355
+ const typed = cached;
356
+ assertUsableRef(typed, node, requestedScope);
357
+ return typed;
358
+ }
359
+ const provider = findEffectiveProvider(requestedScope, node);
360
+ if (!provider) throw new MissingProviderError(node.name, resolutionPath(asUnknownNode(node)));
361
+ if (provider.spec.kind === "value" || provider.spec.kind === "owned-value") {
362
+ const resolved = {
363
+ value: provider.spec.value,
364
+ stamp: provider.stamp
365
+ };
366
+ requestedScope.viewCache.set(asUnknownNode(node), resolved);
367
+ return resolved;
368
+ }
369
+ const reusable = findReusableAncestorCell(requestedScope, node, provider);
370
+ if (reusable) {
371
+ requestedScope.viewCache.set(asUnknownNode(node), reusable);
372
+ return reusable;
373
+ }
374
+ return materialize(requestedScope, node, provider);
375
+ }
376
+ function findEffectiveProvider(scope, node) {
377
+ for (let cursor = scope; cursor; cursor = cursor[scopeParent]) {
378
+ const explicit = cursor.bindings.get(asUnknownNode(node));
379
+ if (explicit) return explicit;
380
+ }
381
+ return scope.runtime.getDefaultProvider(node);
382
+ }
383
+ function findReusableAncestorCell(requestedScope, node, provider) {
384
+ for (let cursor = requestedScope[scopeParent]; cursor; cursor = cursor[scopeParent]) {
385
+ const candidate = cursor.ownedCells.get(asUnknownNode(node));
386
+ if (candidate && isReusable(candidate, provider, requestedScope)) return candidate;
387
+ }
388
+ }
389
+ function isReusable(cell, provider, requestedScope) {
390
+ if (cell.state !== "ready" || !cell.reusable || cell.owner.state !== "active" && cell.owner.state !== "retiring" || cell.providerStamp.identity !== provider.stamp.identity) return false;
391
+ for (const record of cell.dependencies) try {
392
+ if (resolveUntracked(requestedScope, nodeOf(record.dependency)).stamp.identity !== record.stamp.identity) return false;
393
+ } catch {
394
+ return false;
395
+ }
396
+ return true;
397
+ }
398
+ function materialize(requestedScope, node, provider) {
399
+ if (provider.spec.kind !== "factory") throw new Error("ripple-di attempted to materialize a non-factory provider.");
400
+ const start = cycleStart(asUnknownNode(node), provider.stamp);
401
+ if (start >= 0) throw new DependencyCycleError([...framesFrom(start).map((frame) => frame.node.name), node.name]);
402
+ const frame = {
403
+ runtime: requestedScope.runtime,
404
+ scope: requestedScope,
405
+ node: asUnknownNode(node),
406
+ providerStamp: provider.stamp,
407
+ dependencies: /* @__PURE__ */ new Map(),
408
+ hasFailedDependencyRead: false
409
+ };
410
+ pushEvaluation(frame);
411
+ let value;
412
+ try {
413
+ const produced = provider.spec.factory();
414
+ const marked = markedValueOf(produced);
415
+ if (marked) value = marked.value;
416
+ else if (isPromise(produced)) throw new AsyncFactoryError(node.name, resolutionPath());
417
+ else value = produced;
418
+ } catch (error) {
419
+ if (error instanceof RippleError) throw error;
420
+ throw new FactoryError(node.name, resolutionPath(), error);
421
+ } finally {
422
+ popEvaluation(frame);
423
+ }
424
+ const dependencies = [...frame.dependencies].map(([dependencyNode, stamp]) => ({
425
+ dependency: dependencyNode.dependency,
426
+ stamp
427
+ }));
428
+ const reusable = !frame.hasFailedDependencyRead;
429
+ const owner = !reusable ? requestedScope : deepestScope([provider.stamp.home, ...dependencies.map((record) => record.stamp.home)]);
430
+ if (!isAncestorOrSelf(owner, requestedScope)) throw new Error(`ripple-di owner invariant failed for dependency "${node.name}".`);
431
+ const cell = {
432
+ dependency: node.dependency,
433
+ node,
434
+ value,
435
+ owner,
436
+ stamp: {
437
+ kind: "cell",
438
+ identity: Symbol(`${node.name}:cell`),
439
+ dependency: node.dependency,
440
+ home: owner
441
+ },
442
+ providerStamp: provider.stamp,
443
+ dependencies,
444
+ reusable,
445
+ state: "ready"
446
+ };
447
+ owner.publish(cell, requestedScope);
448
+ return cell;
449
+ }
450
+ function recordDependency(frame, node, stamp) {
451
+ const unknownNode = asUnknownNode(node);
452
+ const previous = frame.dependencies.get(unknownNode);
453
+ if (previous && previous.identity !== stamp.identity) throw new ResolutionInvariantError(`Dependency "${node.name}" changed identity while evaluating "${frame.node.name}".`);
454
+ frame.dependencies.set(unknownNode, stamp);
455
+ }
456
+ function assertCompatible(scope, node) {
457
+ if (scope.runtime !== node.runtime) throw new CrossRuntimeDependencyError(node.name, node.runtime.name, scope.runtime.name);
458
+ }
459
+ function assertPubliclyReadable(scope, dependencyName) {
460
+ const owner = scope.runtime.teardown.getStore();
461
+ if (owner) throw new DisposerContextError(dependencyName, owner.name, owner.id);
462
+ if (scope.state !== "active") throw new ScopeClosedError(dependencyName, scope.name, scope.id, scope.state);
463
+ }
464
+ function assertUsableRef(resolved, node, requestedScope) {
465
+ if (resolved.stamp.kind !== "cell") return;
466
+ const cell = resolved;
467
+ if (cell.state !== "ready" || cell.owner.state !== "active" && cell.owner.state !== "retiring") throw new ScopeClosedError(node.name, requestedScope.name, requestedScope.id, requestedScope.state);
468
+ }
469
+ function deepestScope(scopes) {
470
+ return scopes.reduce((deepest, scope) => scope.depth > deepest.depth ? scope : deepest);
471
+ }
472
+ function isAncestorOrSelf(ancestor, scope) {
473
+ for (let cursor = scope; cursor; cursor = cursor[scopeParent]) if (cursor === ancestor) return true;
474
+ return false;
475
+ }
476
+ function asUnknownNode(node) {
477
+ return node;
478
+ }
479
+ //#endregion
480
+ //#region src/scope.ts
481
+ let nextScopeId = 1;
482
+ /**
483
+ * Immutable binding overlay and physical owner of locally published cells.
484
+ *
485
+ * The binding Map is populated only during construction, while caches,
486
+ * children, finalizers, and lifecycle state evolve until teardown completes.
487
+ */
488
+ var ScopeImpl = class ScopeImpl {
489
+ runtime;
490
+ id = nextScopeId++;
491
+ name;
492
+ depth;
493
+ bindings = /* @__PURE__ */ new Map();
494
+ viewCache = /* @__PURE__ */ new Map();
495
+ ownedCells = /* @__PURE__ */ new Map();
496
+ finalizers = [];
497
+ children = /* @__PURE__ */ new Set();
498
+ state = "active";
499
+ lifecycle = createDeferred();
500
+ closingStarted = false;
501
+ [scopeParent];
502
+ constructor(runtime, parent, records) {
503
+ this.runtime = runtime;
504
+ this[scopeParent] = parent;
505
+ this.depth = parent ? parent.depth + 1 : 0;
506
+ this.name = `${runtime.name}/scope-${this.id}`;
507
+ for (const record of records) this.install(record);
508
+ parent?.children.add(this);
509
+ }
510
+ resolve(dependency) {
511
+ const node = nodeOf(dependency);
512
+ return resolveTracked(this, node);
513
+ }
514
+ createScope(provisions = []) {
515
+ this.runtime.assertScopeManagementAllowed("Scope.createScope");
516
+ this.assertActive("<createScope>");
517
+ const records = validateProvisions(this.runtime, provisionListOf(provisions));
518
+ return new ScopeImpl(this.runtime, this, records);
519
+ }
520
+ run(callback) {
521
+ this.runtime.assertScopeManagementAllowed("Scope.run");
522
+ this.assertActive("<run>");
523
+ return this.runtime.ambient.run(this, callback);
524
+ }
525
+ withOverrides(provisions, callback) {
526
+ this.runtime.assertScopeManagementAllowed("Scope.withOverrides");
527
+ return withChildScope(this, provisions, callback);
528
+ }
529
+ retire() {
530
+ this.runtime.assertScopeManagementAllowed("Scope.retire");
531
+ if (this.state === "active") {
532
+ this.state = "retiring";
533
+ if (this.children.size === 0) this.beginClose(false);
534
+ }
535
+ return this.lifecycle.promise;
536
+ }
537
+ close() {
538
+ this.runtime.assertScopeManagementAllowed("Scope.close");
539
+ if (this.state !== "closed" && this.state !== "closing") {
540
+ this.state = "closing";
541
+ this.beginClose(true);
542
+ }
543
+ return this.lifecycle.promise;
544
+ }
545
+ publish(cell, requestedScope) {
546
+ if (this.state !== "active" && this.state !== "retiring") throw new ScopeClosedError(cell.node.name, this.name, this.id, this.state);
547
+ this.ownedCells.set(cell.node, cell);
548
+ this.viewCache.set(cell.node, cell);
549
+ requestedScope.viewCache.set(cell.node, cell);
550
+ if (cell.node.dispose) {
551
+ const dispose = cell.node.dispose;
552
+ this.finalizers.push({
553
+ cell,
554
+ run: () => dispose(cell.value)
555
+ });
556
+ }
557
+ }
558
+ install(record) {
559
+ const node = nodeOf(record.dependency);
560
+ const bound = {
561
+ spec: record.spec,
562
+ stamp: {
563
+ kind: "binding",
564
+ identity: Symbol(`${node.name}:binding`),
565
+ dependency: record.dependency,
566
+ home: this
567
+ }
568
+ };
569
+ this.bindings.set(node, bound);
570
+ if (record.spec.kind === "owned-value") {
571
+ const { dispose, value } = record.spec;
572
+ this.finalizers.push({ run: () => dispose(value) });
573
+ }
574
+ }
575
+ assertActive(operation) {
576
+ if (this.state !== "active") throw new ScopeClosedError(operation, this.name, this.id, this.state);
577
+ }
578
+ beginClose(force) {
579
+ if (this.closingStarted) return;
580
+ this.closingStarted = true;
581
+ this.state = "closing";
582
+ this.performClose(force);
583
+ }
584
+ async performClose(force) {
585
+ const errors = [];
586
+ try {
587
+ if (force) for (const child of [...this.children]) try {
588
+ await child.close();
589
+ } catch (error) {
590
+ collectError(errors, error);
591
+ }
592
+ for (let index = this.finalizers.length - 1; index >= 0; index -= 1) {
593
+ const finalizer = this.finalizers[index];
594
+ if (!finalizer) continue;
595
+ if (finalizer.cell) finalizer.cell.state = "disposing";
596
+ try {
597
+ await this.runtime.ambient.run(this, () => this.runtime.teardown.run(this, () => finalizer.run()));
598
+ } catch (error) {
599
+ collectError(errors, error);
600
+ } finally {
601
+ if (finalizer.cell) finalizer.cell.state = "disposed";
602
+ }
603
+ }
604
+ } finally {
605
+ this.finalizers.length = 0;
606
+ this.viewCache.clear();
607
+ this.ownedCells.clear();
608
+ this.bindings.clear();
609
+ this.state = "closed";
610
+ this[scopeParent]?.childClosed(this);
611
+ }
612
+ if (errors.length > 0) this.lifecycle.reject(new AggregateError(errors, `Errors while closing scope "${this.name}".`));
613
+ else this.lifecycle.resolve();
614
+ }
615
+ childClosed(child) {
616
+ this.children.delete(child);
617
+ if (this.state === "retiring" && this.children.size === 0) this.beginClose(false);
618
+ }
619
+ };
620
+ /**
621
+ * Resolves and validates the complete provision list before scope construction.
622
+ *
623
+ * This boundary prevents duplicate or foreign input from acquiring membership,
624
+ * installing bindings, or registering owned-value finalizers partially.
625
+ */
626
+ function validateProvisions(runtime, provisions) {
627
+ const records = provisions.map(provisionOf);
628
+ const seen = /* @__PURE__ */ new Set();
629
+ for (const record of records) {
630
+ const node = nodeOf(record.dependency);
631
+ if (node.runtime !== runtime) throw new CrossRuntimeDependencyError(node.name, node.runtime.name, runtime.name);
632
+ if (seen.has(node)) throw new DuplicateProviderError(node.name);
633
+ seen.add(node);
634
+ }
635
+ claimOwnedProvisions(provisions);
636
+ return records;
637
+ }
638
+ /**
639
+ * Executes a callback in a temporary ambient child and guarantees force cleanup.
640
+ *
641
+ * Callback errors, leaked children, and teardown failures are preserved together
642
+ * when more than one condition occurs.
643
+ */
644
+ async function withChildScope(parent, provisions, callback) {
645
+ const child = parent.createScope(provisions);
646
+ const errors = [];
647
+ let callbackFailed = false;
648
+ let result;
649
+ try {
650
+ result = await child.run(() => callback(child));
651
+ } catch (error) {
652
+ callbackFailed = true;
653
+ errors.push(error);
654
+ }
655
+ if (child.children.size > 0) errors.push(new LeakedChildScopeError(child.name, child.children.size));
656
+ try {
657
+ await child.close();
658
+ } catch (error) {
659
+ collectError(errors, error);
660
+ }
661
+ if (errors.length === 1) throw errors[0];
662
+ if (errors.length > 1) throw new AggregateError(errors, `Errors in scoped callback "${child.name}".`);
663
+ if (callbackFailed) throw new Error("ripple-di lost a scoped callback error.");
664
+ return result;
665
+ }
666
+ function createDeferred() {
667
+ let settle;
668
+ let reject;
669
+ return {
670
+ promise: new Promise((resolve, rejectPromise) => {
671
+ settle = resolve;
672
+ reject = rejectPromise;
673
+ }),
674
+ resolve: settle,
675
+ reject
676
+ };
677
+ }
678
+ function collectError(errors, error) {
679
+ if (error instanceof AggregateError) errors.push(...error.errors);
680
+ else errors.push(error);
681
+ }
682
+ //#endregion
683
+ //#region src/overrides.ts
684
+ /** Immutable layer that knows the layers it was extended from. */
685
+ var OverrideRunnerImpl = class {
686
+ runtime;
687
+ base;
688
+ factory;
689
+ constructor(runtime, base, factory) {
690
+ this.runtime = runtime;
691
+ this.base = base;
692
+ this.factory = factory;
693
+ }
694
+ run(callback) {
695
+ this.runtime.assertScopeManagementAllowed("OverrideRunner.run");
696
+ return this.enterLayers(this.runtime.currentAmbientScope(), callback);
697
+ }
698
+ wrap(callback) {
699
+ const runner = this;
700
+ return function wrapped(...args) {
701
+ return runner.run(() => callback.apply(this, args));
702
+ };
703
+ }
704
+ extend(factory) {
705
+ return createRunner(this.runtime, this, factory);
706
+ }
707
+ /**
708
+ * Enters the extended layers from the outside in.
709
+ *
710
+ * Staying async keeps a failing provision factory a rejection instead of a
711
+ * synchronous throw, exactly like an invalid provision list.
712
+ */
713
+ async enterLayers(parent, callback) {
714
+ const enter = (scope) => withChildScope(scope, this.factory(), callback);
715
+ return this.base ? this.base.enterLayers(parent, enter) : enter(parent);
716
+ }
717
+ };
718
+ function createRunner(runtime, base, factory) {
719
+ if (typeof factory !== "function") throw new TypeError("An override runner needs a function returning provisions, so that every call builds its provisions again.");
720
+ return new OverrideRunnerImpl(runtime, base, factory);
721
+ }
722
+ /** Creates the first layer of a runner for one runtime. */
723
+ function createOverrideRunnerFor(runtime, factory) {
724
+ return createRunner(runtime, void 0, factory);
725
+ }
726
+ /** Creates a value override bound to one dependency of one runtime. */
727
+ function createValueOverrideFor(runtime, dependency, options) {
728
+ return (value, callback) => {
729
+ runtime.assertScopeManagementAllowed("ValueOverride");
730
+ return withChildScope(runtime.currentAmbientScope(), provide(dependency, value, options), callback);
731
+ };
732
+ }
733
+ //#endregion
734
+ //#region src/runtime.ts
735
+ let nextRuntimeId = 1;
736
+ /** Concrete owner of one independent dependency graph. */
737
+ var RuntimeImpl = class {
738
+ id = nextRuntimeId++;
739
+ name;
740
+ ambient = new AsyncLocalStorage();
741
+ teardown = new AsyncLocalStorage();
742
+ root;
743
+ defaults = /* @__PURE__ */ new Map();
744
+ activeInstallation;
745
+ closingInstallation;
746
+ constructor(options = {}) {
747
+ this.name = options.name ?? `runtime-${this.id}`;
748
+ this.root = new ScopeImpl(this, void 0, []);
749
+ }
750
+ defineDependency(factoryOrOptions, maybeOptions) {
751
+ const isFactory = typeof factoryOrOptions === "function";
752
+ const factory = isFactory ? factoryOrOptions : void 0;
753
+ const options = (isFactory ? maybeOptions : factoryOrOptions) ?? {};
754
+ return createDependency({
755
+ name: options.name,
756
+ runtime: this,
757
+ defaultFactory: factory,
758
+ dispose: options.dispose
759
+ });
760
+ }
761
+ install(provisions) {
762
+ this.assertScopeManagementAllowed("Runtime.install");
763
+ if (this.root.state !== "active") throw new ScopeClosedError("Runtime.install", this.root.name, this.root.id, this.root.state);
764
+ if (this.activeInstallation) throw new InstallationConflictError(this.name, "active-installation");
765
+ if (this.closingInstallation) throw new InstallationConflictError(this.name, "closing-installation");
766
+ if (this.root.children.size > 0) throw new InstallationConflictError(this.name, "live-scopes", [...this.root.children].map((scope) => scope.name));
767
+ const installation = new InstallationImpl(this, this.root.createScope(provisions));
768
+ this.activeInstallation = installation;
769
+ return installation;
770
+ }
771
+ resolve(dependency) {
772
+ const node = nodeOf(dependency);
773
+ this.assertOwnDependency(node);
774
+ return resolveTracked(this.currentScope(node.name), node);
775
+ }
776
+ createScope(provisions = []) {
777
+ this.assertScopeManagementAllowed("Runtime.createScope");
778
+ return this.currentAmbientScope().createScope(provisions);
779
+ }
780
+ withOverrides(provisions, callback) {
781
+ this.assertScopeManagementAllowed("Runtime.withOverrides");
782
+ return withChildScope(this.currentAmbientScope(), provisions, callback);
783
+ }
784
+ createOverrideRunner(factory) {
785
+ return createOverrideRunnerFor(this, factory);
786
+ }
787
+ createValueOverride(dependency, options = {}) {
788
+ this.assertOwnDependency(nodeOf(dependency));
789
+ return createValueOverrideFor(this, dependency, options);
790
+ }
791
+ dispose() {
792
+ this.assertScopeManagementAllowed("Runtime.dispose");
793
+ this.activeInstallation = void 0;
794
+ return this.root.close();
795
+ }
796
+ closeInstallation(installation) {
797
+ if (this.activeInstallation === installation) {
798
+ this.activeInstallation = void 0;
799
+ this.closingInstallation = installation;
800
+ return installation.scope.close().then(() => {
801
+ this.finishClosingInstallation(installation);
802
+ }, (error) => {
803
+ this.finishClosingInstallation(installation);
804
+ throw error;
805
+ });
806
+ }
807
+ return installation.scope.close();
808
+ }
809
+ assertScopeManagementAllowed(operation) {
810
+ assertOutsideEvaluation(this, operation);
811
+ const owner = this.teardown.getStore();
812
+ if (owner) throw new DisposerContextError(operation, owner.name, owner.id);
813
+ }
814
+ currentAmbientScope() {
815
+ return this.ambient.getStore() ?? this.baseScope();
816
+ }
817
+ readCallable(node) {
818
+ const frame = currentEvaluation();
819
+ if (frame) {
820
+ if (frame.runtime !== this) {
821
+ frame.hasFailedDependencyRead = true;
822
+ throw new CrossRuntimeDependencyError(node.name, this.name, frame.runtime.name);
823
+ }
824
+ return resolveTracked(frame.scope, node);
825
+ }
826
+ return resolveTracked(this.ambient.getStore() ?? this.baseScope(), node);
827
+ }
828
+ getDefaultProvider(node) {
829
+ if (!node.defaultFactory) return;
830
+ const unknownNode = node;
831
+ let provider = this.defaults.get(unknownNode);
832
+ if (!provider) {
833
+ provider = {
834
+ spec: {
835
+ kind: "factory",
836
+ factory: node.defaultFactory
837
+ },
838
+ stamp: {
839
+ kind: "binding",
840
+ identity: Symbol(`${node.name}:default-binding`),
841
+ dependency: node.dependency,
842
+ home: this.root
843
+ }
844
+ };
845
+ this.defaults.set(unknownNode, provider);
846
+ }
847
+ return provider;
848
+ }
849
+ currentScope(operation) {
850
+ const frame = currentEvaluation();
851
+ if (frame) {
852
+ if (frame.runtime !== this) {
853
+ frame.hasFailedDependencyRead = true;
854
+ throw new CrossRuntimeDependencyError(operation, this.name, frame.runtime.name);
855
+ }
856
+ return frame.scope;
857
+ }
858
+ return this.ambient.getStore() ?? this.baseScope();
859
+ }
860
+ baseScope() {
861
+ return this.activeInstallation?.scope ?? this.root;
862
+ }
863
+ finishClosingInstallation(installation) {
864
+ if (this.closingInstallation === installation) this.closingInstallation = void 0;
865
+ }
866
+ assertOwnDependency(node) {
867
+ if (node.runtime !== this) throw new CrossRuntimeDependencyError(node.name, node.runtime.name, this.name);
868
+ }
869
+ };
870
+ var InstallationImpl = class {
871
+ runtime;
872
+ scope;
873
+ closePromise;
874
+ constructor(runtime, scope) {
875
+ this.runtime = runtime;
876
+ this.scope = scope;
877
+ }
878
+ close() {
879
+ this.runtime.assertScopeManagementAllowed("Installation.close");
880
+ this.closePromise ??= this.runtime.closeInstallation(this);
881
+ return this.closePromise;
882
+ }
883
+ };
884
+ /**
885
+ * Creates an independent dependency graph.
886
+ *
887
+ * Define its dependencies through the methods on the returned runtime.
888
+ */
889
+ function createRuntime(options = {}) {
890
+ return new RuntimeImpl(options);
891
+ }
892
+ const globalRuntime = new RuntimeImpl({ name: "global" });
893
+ function defineDependency(factoryOrOptions, maybeOptions) {
894
+ if (typeof factoryOrOptions === "function") return globalRuntime.defineDependency(factoryOrOptions, maybeOptions);
895
+ return globalRuntime.defineDependency(factoryOrOptions);
896
+ }
897
+ /**
898
+ * Installs long-lived providers for module-level dependencies.
899
+ *
900
+ * Scoped overrides still take priority. Close the returned installation to
901
+ * remove its providers and clean up everything created from them.
902
+ */
903
+ function install(provisions) {
904
+ return globalRuntime.install(provisions);
905
+ }
906
+ /** Returns a dependency value from the current scope. */
907
+ function resolve(dependency) {
908
+ return globalRuntime.resolve(dependency);
909
+ }
910
+ /** Creates a manually managed scope with optional dependency overrides. */
911
+ function createScope(provisions = []) {
912
+ return globalRuntime.createScope(provisions);
913
+ }
914
+ /**
915
+ * Runs a callback with temporary dependency overrides.
916
+ *
917
+ * Overrides remain active across `await`, stay isolated from concurrent
918
+ * callbacks, and are cleaned up when the callback finishes.
919
+ */
920
+ function withOverrides(provisions, callback) {
921
+ return globalRuntime.withOverrides(provisions, callback);
922
+ }
923
+ /**
924
+ * Prepares dependency overrides that are applied again to each call of the
925
+ * returned runner.
926
+ *
927
+ * The factory runs again for every call, so a call can own the values it
928
+ * provides.
929
+ */
930
+ function createOverrideRunner(factory) {
931
+ return globalRuntime.createOverrideRunner(factory);
932
+ }
933
+ /**
934
+ * Prepares a helper that replaces one dependency with a value for one callback.
935
+ *
936
+ * Use it for a dependency that application code supplies the same way in many
937
+ * places, such as a client or a request context.
938
+ * Ownership options given here apply to every value the helper receives.
939
+ */
940
+ function createValueOverride(dependency, options) {
941
+ return globalRuntime.createValueOverride(dependency, options);
942
+ }
943
+ /** Closes every scope and cleans up every owned value. */
944
+ function dispose() {
945
+ return globalRuntime.dispose();
946
+ }
947
+ //#endregion
948
+ export { AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, DependencyCycleError, DisposerContextError, DuplicateProviderError, FactoryError, FactoryScopeOperationError, InstallationConflictError, LeakedChildScopeError, MissingProviderError, OwnedProvisionReuseError, RippleError, ScopeClosedError, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withOverrides };