ripple-di 1.1.0 → 1.2.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 +33 -10
- package/dist/index.d.mts +22 -1
- package/dist/index.mjs +52 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,7 +61,8 @@ The package is published as ESM.
|
|
|
61
61
|
| Define an input, derived value, or service | `defineDependency`
|
|
62
62
|
| Supply the application's values at startup | `install` with `provide` or `provideFactory`
|
|
63
63
|
| Replace values for one callback | `withOverrides` with `provide` or `provideFactory`
|
|
64
|
-
|
|
|
64
|
+
| Continue every current override layer | `withDetachedContext`
|
|
65
|
+
| Run detached with selected overrides | `withDetachedOverrides`
|
|
65
66
|
| Keep one scope open across several operations | `createScope`
|
|
66
67
|
| Shut everything down | `dispose`
|
|
67
68
|
|
|
@@ -173,11 +174,27 @@ Use such a value inside the callback, and use `createScope` when it has to outli
|
|
|
173
174
|
Wrapping the value in an object prevents it from being awaited, but the temporary scope still closes before the caller receives it.
|
|
174
175
|
Anything that scope owned has already been cleaned up.
|
|
175
176
|
|
|
176
|
-
###
|
|
177
|
+
### Continue after the current scope closes
|
|
177
178
|
|
|
178
|
-
`withDetachedOverrides`
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
`withDetachedContext` and `withDetachedOverrides` run work outside the current ambient scope, so a request or another scoped operation can close while that work continues.
|
|
180
|
+
|
|
181
|
+
Use `withDetachedContext` to continue with every override layer that is active when you call it:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { withDetachedContext } from "ripple-di"
|
|
185
|
+
|
|
186
|
+
const backgroundTask = withDetachedContext(() =>
|
|
187
|
+
updateTenantSearchIndex(),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
trackBackgroundTask(backgroundTask)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
It reproduces those layers in new scopes without copying cached dependency values.
|
|
194
|
+
Borrowed values keep their identity, while factory provisions run again and their results belong to the new scopes.
|
|
195
|
+
If a layer owns an existing provided value, the call rejects with `DetachedContextOwnedProvisionError` because the value cannot belong to both contexts.
|
|
196
|
+
|
|
197
|
+
Use `withDetachedOverrides` when the work should receive only selected values, especially across a security-sensitive boundary:
|
|
181
198
|
|
|
182
199
|
```ts
|
|
183
200
|
import { provide, withDetachedOverrides } from "ripple-di"
|
|
@@ -192,12 +209,17 @@ const backgroundTask = withDetachedOverrides(
|
|
|
192
209
|
trackBackgroundTask(backgroundTask)
|
|
193
210
|
```
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
Both functions create scopes beneath the active installation, or beneath the runtime root when no installation is active.
|
|
213
|
+
Closing the installation or calling `dispose()` force-closes them, and an unfinished root child prevents `install()`.
|
|
214
|
+
|
|
215
|
+
The scope remains current while the callback runs and while Ripple DI awaits its result.
|
|
216
|
+
The returned promise settles after cleanup, so code that needs the detached context, including finalization, belongs inside the callback:
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
|
|
218
|
+
```ts
|
|
219
|
+
withDetachedContext(() =>
|
|
220
|
+
runBackgroundTask().finally(finalizeBackgroundTask),
|
|
221
|
+
)
|
|
222
|
+
```
|
|
201
223
|
|
|
202
224
|
## Where a value belongs
|
|
203
225
|
|
|
@@ -608,6 +630,7 @@ Every runtime has the same methods, and each has a module-level counterpart that
|
|
|
608
630
|
- `resolve`
|
|
609
631
|
- `createScope`
|
|
610
632
|
- `withOverrides`
|
|
633
|
+
- `withDetachedContext`
|
|
611
634
|
- `withDetachedOverrides`
|
|
612
635
|
- `createValueOverride`
|
|
613
636
|
- `createOverrideRunner`
|
package/dist/index.d.mts
CHANGED
|
@@ -196,6 +196,13 @@ interface Runtime {
|
|
|
196
196
|
* remains owned by the active installation or runtime root.
|
|
197
197
|
*/
|
|
198
198
|
withDetachedOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
199
|
+
/**
|
|
200
|
+
* Continues the current dependency context outside its original scope.
|
|
201
|
+
*
|
|
202
|
+
* The runtime reproduces every current override layer beneath its active
|
|
203
|
+
* installation or root without copying cached dependency values.
|
|
204
|
+
*/
|
|
205
|
+
withDetachedContext<TCallbackResult>(callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
199
206
|
/**
|
|
200
207
|
* Prepares overrides that are applied again to each call of the returned
|
|
201
208
|
* runner.
|
|
@@ -254,6 +261,14 @@ declare function withOverrides<TCallbackResult>(provisions: ProvisionInput, call
|
|
|
254
261
|
* and remains part of that lifecycle until the callback finishes.
|
|
255
262
|
*/
|
|
256
263
|
declare function withDetachedOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
264
|
+
/**
|
|
265
|
+
* Continues the current dependency context outside its original scope.
|
|
266
|
+
*
|
|
267
|
+
* Current override layers are reproduced beneath the active installation or
|
|
268
|
+
* runtime root without copying cached values, and are cleaned up after the
|
|
269
|
+
* callback finishes.
|
|
270
|
+
*/
|
|
271
|
+
declare function withDetachedContext<TCallbackResult>(callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
257
272
|
/**
|
|
258
273
|
* Prepares dependency overrides that are applied again to each call of the
|
|
259
274
|
* returned runner.
|
|
@@ -333,6 +348,12 @@ declare class OwnedProvisionReuseError extends RippleError {
|
|
|
333
348
|
readonly dependencyName: string;
|
|
334
349
|
constructor(dependencyName: string);
|
|
335
350
|
}
|
|
351
|
+
/** A detached context cannot reproduce a provision that owns its value. */
|
|
352
|
+
declare class DetachedContextOwnedProvisionError extends RippleError {
|
|
353
|
+
readonly dependencyName: string;
|
|
354
|
+
readonly scopeName: string;
|
|
355
|
+
constructor(dependencyName: string, scopeName: string);
|
|
356
|
+
}
|
|
336
357
|
/** Dependency factories called one another in a cycle. */
|
|
337
358
|
declare class DependencyCycleError extends RippleError {
|
|
338
359
|
readonly path: readonly string[];
|
|
@@ -390,4 +411,4 @@ declare class LeakedChildScopeError extends RippleError {
|
|
|
390
411
|
constructor(scopeName: string, leakedChildCount: number);
|
|
391
412
|
}
|
|
392
413
|
//#endregion
|
|
393
|
-
export { type AsValue, AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, type Dependency, DependencyCycleError, type DependencyOptions, type Disposer, DisposerContextError, DuplicateProviderError, FactoryError, type FactoryResult, FactoryScopeOperationError, Installation, InstallationConflictError, LeakedChildScopeError, MissingProviderError, type OverrideRunner, OwnedProvisionReuseError, type ProvideOptions, type Provision, type ProvisionFactory, type ProvisionInput, RippleError, Runtime, RuntimeOptions, type Scope, ScopeClosedError, type ScopeState, type ValueOverride, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withDetachedOverrides, withOverrides };
|
|
414
|
+
export { type AsValue, AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, type Dependency, DependencyCycleError, type DependencyOptions, DetachedContextOwnedProvisionError, type Disposer, DisposerContextError, DuplicateProviderError, FactoryError, type FactoryResult, FactoryScopeOperationError, Installation, InstallationConflictError, LeakedChildScopeError, MissingProviderError, type OverrideRunner, OwnedProvisionReuseError, type ProvideOptions, type Provision, type ProvisionFactory, type ProvisionInput, RippleError, Runtime, RuntimeOptions, type Scope, ScopeClosedError, type ScopeState, type ValueOverride, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withDetachedContext, withDetachedOverrides, withOverrides };
|
package/dist/index.mjs
CHANGED
|
@@ -58,6 +58,17 @@ var OwnedProvisionReuseError = class extends RippleError {
|
|
|
58
58
|
this.name = "OwnedProvisionReuseError";
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
+
/** A detached context cannot reproduce a provision that owns its value. */
|
|
62
|
+
var DetachedContextOwnedProvisionError = class extends RippleError {
|
|
63
|
+
dependencyName;
|
|
64
|
+
scopeName;
|
|
65
|
+
constructor(dependencyName, scopeName) {
|
|
66
|
+
super(`Cannot detach context from scope "${scopeName}" because its provision for dependency "${dependencyName}" owns an existing value. Use a borrowed value or factory provision when the context must be reproducible.`);
|
|
67
|
+
this.dependencyName = dependencyName;
|
|
68
|
+
this.scopeName = scopeName;
|
|
69
|
+
this.name = "DetachedContextOwnedProvisionError";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
61
72
|
/** Dependency factories called one another in a cycle. */
|
|
62
73
|
var DependencyCycleError = class extends RippleError {
|
|
63
74
|
path;
|
|
@@ -695,6 +706,32 @@ async function withChildScope(parent, provisions, callback) {
|
|
|
695
706
|
if (callbackFailed) throw new Error("ripple-di lost a scoped callback error.");
|
|
696
707
|
return result;
|
|
697
708
|
}
|
|
709
|
+
/** Reproduces the current scope layers beneath a separate lifecycle parent. */
|
|
710
|
+
async function withDetachedScopeContext(base, current, callback) {
|
|
711
|
+
const snapshots = snapshotDetachedLayers(base, current);
|
|
712
|
+
return await replayDetachedLayers(base, snapshots.length > 0 ? snapshots : [[]], 0, callback);
|
|
713
|
+
}
|
|
714
|
+
/** Captures immutable provider recipes without retaining scope caches. */
|
|
715
|
+
function snapshotDetachedLayers(base, current) {
|
|
716
|
+
if (current.state !== "active") throw new ScopeClosedError("Runtime.withDetachedContext", current.name, current.id, current.state);
|
|
717
|
+
const scopes = [];
|
|
718
|
+
let cursor = current;
|
|
719
|
+
while (cursor !== base) {
|
|
720
|
+
if (!cursor) throw new Error(`Scope "${current.name}" is not beneath base scope "${base.name}".`);
|
|
721
|
+
if (cursor.state === "closing" || cursor.state === "closed") throw new ScopeClosedError("Runtime.withDetachedContext", cursor.name, cursor.id, cursor.state);
|
|
722
|
+
scopes.push(cursor);
|
|
723
|
+
cursor = cursor[scopeParent];
|
|
724
|
+
}
|
|
725
|
+
scopes.reverse();
|
|
726
|
+
for (const scope of scopes) for (const binding of scope.bindings.values()) if (binding.spec.kind === "owned-value") throw new DetachedContextOwnedProvisionError(nodeOf(binding.stamp.dependency).name, scope.name);
|
|
727
|
+
return scopes.map((scope) => [...scope.bindings.values()].map((binding) => binding.spec.kind === "factory" ? provideFactory(binding.stamp.dependency, binding.spec.factory) : provide(binding.stamp.dependency, binding.spec.value)));
|
|
728
|
+
}
|
|
729
|
+
/** Enters reproduced layers from the original outermost layer inward. */
|
|
730
|
+
async function replayDetachedLayers(parent, layers, index, callback) {
|
|
731
|
+
const provisions = layers[index];
|
|
732
|
+
if (!provisions) throw new Error("ripple-di lost a detached context layer.");
|
|
733
|
+
return await withChildScope(parent, provisions, (scope) => index === layers.length - 1 ? callback(scope) : replayDetachedLayers(scope, layers, index + 1, callback));
|
|
734
|
+
}
|
|
698
735
|
function createDeferred() {
|
|
699
736
|
let settle;
|
|
700
737
|
let reject;
|
|
@@ -821,6 +858,10 @@ var RuntimeImpl = class {
|
|
|
821
858
|
this.assertScopeManagementAllowed("Runtime.withDetachedOverrides");
|
|
822
859
|
return withChildScope(this.baseScope(), provisions, callback);
|
|
823
860
|
}
|
|
861
|
+
withDetachedContext(callback) {
|
|
862
|
+
this.assertScopeManagementAllowed("Runtime.withDetachedContext");
|
|
863
|
+
return withDetachedScopeContext(this.baseScope(), this.currentAmbientScope(), callback);
|
|
864
|
+
}
|
|
824
865
|
createOverrideRunner(factory) {
|
|
825
866
|
return createOverrideRunnerFor(this, factory);
|
|
826
867
|
}
|
|
@@ -969,6 +1010,16 @@ function withDetachedOverrides(provisions, callback) {
|
|
|
969
1010
|
return globalRuntime.withDetachedOverrides(provisions, callback);
|
|
970
1011
|
}
|
|
971
1012
|
/**
|
|
1013
|
+
* Continues the current dependency context outside its original scope.
|
|
1014
|
+
*
|
|
1015
|
+
* Current override layers are reproduced beneath the active installation or
|
|
1016
|
+
* runtime root without copying cached values, and are cleaned up after the
|
|
1017
|
+
* callback finishes.
|
|
1018
|
+
*/
|
|
1019
|
+
function withDetachedContext(callback) {
|
|
1020
|
+
return globalRuntime.withDetachedContext(callback);
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
972
1023
|
* Prepares dependency overrides that are applied again to each call of the
|
|
973
1024
|
* returned runner.
|
|
974
1025
|
*
|
|
@@ -993,4 +1044,4 @@ function dispose() {
|
|
|
993
1044
|
return globalRuntime.dispose();
|
|
994
1045
|
}
|
|
995
1046
|
//#endregion
|
|
996
|
-
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, withDetachedOverrides, withOverrides };
|
|
1047
|
+
export { AsyncFactoryError, CrossRuntimeDependencyError, CrossScopeResolutionError, DependencyCycleError, DetachedContextOwnedProvisionError, DisposerContextError, DuplicateProviderError, FactoryError, FactoryScopeOperationError, InstallationConflictError, LeakedChildScopeError, MissingProviderError, OwnedProvisionReuseError, RippleError, ScopeClosedError, asValue, createOverrideRunner, createRuntime, createScope, createValueOverride, defineDependency, dispose, install, provide, provideFactory, resolve, withDetachedContext, withDetachedOverrides, withOverrides };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ripple-di",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "Scoped dependency injection for TypeScript with automatic dependency tracking, lifecycle-aware cleanup, and no container lookups in application code.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dependency-injection",
|