ripple-di 1.0.1 → 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 +51 -0
- package/dist/index.d.mts +36 -1
- package/dist/index.mjs +65 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +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
|
+
| Continue every current override layer | `withDetachedContext`
|
|
65
|
+
| Run detached with selected overrides | `withDetachedOverrides`
|
|
64
66
|
| Keep one scope open across several operations | `createScope`
|
|
65
67
|
| Shut everything down | `dispose`
|
|
66
68
|
|
|
@@ -172,6 +174,53 @@ Use such a value inside the callback, and use `createScope` when it has to outli
|
|
|
172
174
|
Wrapping the value in an object prevents it from being awaited, but the temporary scope still closes before the caller receives it.
|
|
173
175
|
Anything that scope owned has already been cleaned up.
|
|
174
176
|
|
|
177
|
+
### Continue after the current scope closes
|
|
178
|
+
|
|
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:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { provide, withDetachedOverrides } from "ripple-di"
|
|
201
|
+
|
|
202
|
+
const tenant = useTenant()
|
|
203
|
+
|
|
204
|
+
const backgroundTask = withDetachedOverrides(
|
|
205
|
+
provide(useTenant, tenant),
|
|
206
|
+
() => updateTenantSearchIndex(),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
trackBackgroundTask(backgroundTask)
|
|
210
|
+
```
|
|
211
|
+
|
|
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:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
withDetachedContext(() =>
|
|
220
|
+
runBackgroundTask().finally(finalizeBackgroundTask),
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
175
224
|
## Where a value belongs
|
|
176
225
|
|
|
177
226
|
The same tracked dependency calls that decide when a factory result must be rebuilt also decide which lifecycle owns it and invokes its disposer.
|
|
@@ -581,6 +630,8 @@ Every runtime has the same methods, and each has a module-level counterpart that
|
|
|
581
630
|
- `resolve`
|
|
582
631
|
- `createScope`
|
|
583
632
|
- `withOverrides`
|
|
633
|
+
- `withDetachedContext`
|
|
634
|
+
- `withDetachedOverrides`
|
|
584
635
|
- `createValueOverride`
|
|
585
636
|
- `createOverrideRunner`
|
|
586
637
|
- `dispose`
|
package/dist/index.d.mts
CHANGED
|
@@ -189,6 +189,20 @@ interface Runtime {
|
|
|
189
189
|
* for the callback afterward.
|
|
190
190
|
*/
|
|
191
191
|
withOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
192
|
+
/**
|
|
193
|
+
* Runs a callback in a temporary child of the runtime's current base scope.
|
|
194
|
+
*
|
|
195
|
+
* The callback does not inherit the current ambient scope, but its scope
|
|
196
|
+
* remains owned by the active installation or runtime root.
|
|
197
|
+
*/
|
|
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>>;
|
|
192
206
|
/**
|
|
193
207
|
* Prepares overrides that are applied again to each call of the returned
|
|
194
208
|
* runner.
|
|
@@ -240,6 +254,21 @@ declare function createScope(provisions?: ProvisionInput): Scope;
|
|
|
240
254
|
* callbacks, and are cleaned up when the callback finishes.
|
|
241
255
|
*/
|
|
242
256
|
declare function withOverrides<TCallbackResult>(provisions: ProvisionInput, callback: (scope: Scope) => TCallbackResult): Promise<Awaited<TCallbackResult>>;
|
|
257
|
+
/**
|
|
258
|
+
* Runs a callback with overrides outside the current ambient scope.
|
|
259
|
+
*
|
|
260
|
+
* The temporary scope inherits from the active installation or runtime root
|
|
261
|
+
* and remains part of that lifecycle until the callback finishes.
|
|
262
|
+
*/
|
|
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>>;
|
|
243
272
|
/**
|
|
244
273
|
* Prepares dependency overrides that are applied again to each call of the
|
|
245
274
|
* returned runner.
|
|
@@ -319,6 +348,12 @@ declare class OwnedProvisionReuseError extends RippleError {
|
|
|
319
348
|
readonly dependencyName: string;
|
|
320
349
|
constructor(dependencyName: string);
|
|
321
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
|
+
}
|
|
322
357
|
/** Dependency factories called one another in a cycle. */
|
|
323
358
|
declare class DependencyCycleError extends RippleError {
|
|
324
359
|
readonly path: readonly string[];
|
|
@@ -376,4 +411,4 @@ declare class LeakedChildScopeError extends RippleError {
|
|
|
376
411
|
constructor(scopeName: string, leakedChildCount: number);
|
|
377
412
|
}
|
|
378
413
|
//#endregion
|
|
379
|
-
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, 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;
|
|
@@ -817,6 +854,14 @@ var RuntimeImpl = class {
|
|
|
817
854
|
this.assertScopeManagementAllowed("Runtime.withOverrides");
|
|
818
855
|
return withChildScope(this.currentAmbientScope(), provisions, callback);
|
|
819
856
|
}
|
|
857
|
+
withDetachedOverrides(provisions, callback) {
|
|
858
|
+
this.assertScopeManagementAllowed("Runtime.withDetachedOverrides");
|
|
859
|
+
return withChildScope(this.baseScope(), provisions, callback);
|
|
860
|
+
}
|
|
861
|
+
withDetachedContext(callback) {
|
|
862
|
+
this.assertScopeManagementAllowed("Runtime.withDetachedContext");
|
|
863
|
+
return withDetachedScopeContext(this.baseScope(), this.currentAmbientScope(), callback);
|
|
864
|
+
}
|
|
820
865
|
createOverrideRunner(factory) {
|
|
821
866
|
return createOverrideRunnerFor(this, factory);
|
|
822
867
|
}
|
|
@@ -956,6 +1001,25 @@ function withOverrides(provisions, callback) {
|
|
|
956
1001
|
return globalRuntime.withOverrides(provisions, callback);
|
|
957
1002
|
}
|
|
958
1003
|
/**
|
|
1004
|
+
* Runs a callback with overrides outside the current ambient scope.
|
|
1005
|
+
*
|
|
1006
|
+
* The temporary scope inherits from the active installation or runtime root
|
|
1007
|
+
* and remains part of that lifecycle until the callback finishes.
|
|
1008
|
+
*/
|
|
1009
|
+
function withDetachedOverrides(provisions, callback) {
|
|
1010
|
+
return globalRuntime.withDetachedOverrides(provisions, callback);
|
|
1011
|
+
}
|
|
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
|
+
/**
|
|
959
1023
|
* Prepares dependency overrides that are applied again to each call of the
|
|
960
1024
|
* returned runner.
|
|
961
1025
|
*
|
|
@@ -980,4 +1044,4 @@ function dispose() {
|
|
|
980
1044
|
return globalRuntime.dispose();
|
|
981
1045
|
}
|
|
982
1046
|
//#endregion
|
|
983
|
-
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 };
|
|
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.0
|
|
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",
|