@zudojs/core 1.0.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 +15 -5
- package/dist/application/applicationContext.context.d.ts +12 -1
- package/dist/application/applicationContext.context.js +10 -2
- package/dist/application/createApplication.js +6 -1
- package/dist/configuration/configurationManager.manager.d.ts +10 -0
- package/dist/configuration/configurationManager.manager.js +19 -1
- package/dist/container/container.d.ts +4 -3
- package/dist/container/container.js +15 -10
- package/dist/container/container.lifetime.d.ts +21 -0
- package/dist/container/container.lifetime.js +39 -0
- package/dist/container/scope.d.ts +3 -1
- package/dist/context/provider/contextStorage.storage.d.ts +8 -0
- package/dist/context/provider/contextStorage.storage.js +12 -1
- package/dist/lifecycle/core/lifecycle.d.ts +8 -3
- package/dist/lifecycle/core/lifecycle.js +18 -5
- package/dist/lifecycle/core/lifecycle.rollback.d.ts +18 -0
- package/dist/lifecycle/core/lifecycle.rollback.js +34 -0
- package/dist/logging/core/logEntry.entry.js +12 -1
- package/dist/modules/moduleLifecycle/index.d.ts +1 -1
- package/dist/modules/moduleLifecycle/moduleLifecycle.lifecycle.d.ts +30 -5
- package/dist/modules/moduleLifecycle/moduleLifecycle.lifecycle.js +58 -17
- package/dist/modules/moduleLifecycle/moduleLifecycle.stateMachine.d.ts +1 -1
- package/dist/modules/moduleLifecycle/moduleLifecycle.stateMachine.js +3 -1
- package/dist/modules/moduleLifecycle/moduleLifecycle.type.d.ts +20 -0
- package/dist/modules/moduleLoader/moduleLoader.loader.js +61 -25
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.js +15 -4
- package/dist/runtime/runtimeBootstrap/pipeline/runtimeBootstrap.pipeline.d.ts +3 -1
- package/dist/runtime/runtimeBootstrap/pipeline/runtimeBootstrap.pipeline.js +29 -5
- package/dist/runtime/runtimeOptions/runtimeOptions.defaults.js +3 -1
- package/dist/runtime/runtimeOptions/runtimeOptions.mode.d.ts +14 -0
- package/dist/runtime/runtimeOptions/runtimeOptions.mode.js +16 -0
- package/dist/runtime/runtimeOptions/runtimeOptions.resolver.js +17 -1
- package/dist/runtime/runtimeOptions/runtimeOptions.type.d.ts +18 -2
- package/dist/runtime/runtimeOptions/runtimeOptions.validation.d.ts +1 -1
- package/dist/runtime/runtimeShutdown/pipeline/runtimeShutdown.pipeline.js +10 -2
- package/dist/runtime/runtimeSignals/runtimeSignals.d.ts +14 -3
- package/dist/runtime/runtimeSignals/runtimeSignals.fatal.d.ts +29 -0
- package/dist/runtime/runtimeSignals/runtimeSignals.fatal.js +51 -0
- package/dist/runtime/runtimeSignals/runtimeSignals.js +15 -4
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Application lifecycle management, execution context propagation, and runtime orchestration for Zudojs applications.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-core](https://zudojs.oyinlola.site/docs/packages-core) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-core.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -43,6 +49,8 @@ await app.start(); // restart: a fresh runtime is created
|
|
|
43
49
|
await app.shutdown(); // stop + dispose; cannot be restarted afterwards
|
|
44
50
|
```
|
|
45
51
|
|
|
52
|
+
When `runtime.mode` is omitted, it is derived from `NODE_ENV` with `resolveEnvironment()` from `@zudojs/constants` (`prod` / `Production` → `"production"`, `staging` → `"production"`, `test` → `"test"`, unset → `"development"`).
|
|
53
|
+
|
|
46
54
|
`createApplication` wires the standard graph (container, configuration manager, logger, module registry/loader/lifecycle, runtime, application lifecycle). Every piece can also be assembled by hand:
|
|
47
55
|
|
|
48
56
|
```typescript
|
|
@@ -66,6 +74,8 @@ const app = await Application.create({
|
|
|
66
74
|
});
|
|
67
75
|
```
|
|
68
76
|
|
|
77
|
+
A failed `lifecycle.start()` stops, in reverse order, every participant whose `start()` completed, so a retry starts from the first one again. `dispose()` only reaches participants whose `initialize()` ran.
|
|
78
|
+
|
|
69
79
|
## Runtime
|
|
70
80
|
|
|
71
81
|
`createRuntime(dependencies, options)` returns a single-use runtime:
|
|
@@ -77,14 +87,14 @@ FAILED is reachable from every non-terminal state via fail() or a failure.
|
|
|
77
87
|
|
|
78
88
|
- `start()` loads, initializes, and starts modules through the bootstrap pipeline; `getStatus().bootstrap` reports counts, errors, and duration.
|
|
79
89
|
- `stop()` stops and destroys modules through the shutdown pipeline; `getStatus().shutdown` reports the same.
|
|
80
|
-
- `startup.timeoutMs` / `shutdown.timeoutMs` abort the pipeline with a `RuntimeTimeoutError`; the abandoned pipeline can never surface an unhandled rejection.
|
|
90
|
+
- `startup.timeoutMs` / `shutdown.timeoutMs` abort the pipeline with a `RuntimeTimeoutError`; the abandoned pipeline can never surface an unhandled rejection. A timed-out bootstrap starts no further phase or module hook; the hook still running finishes, and the runtime then stops and destroys whatever it brought up. `stop()` waits for that teardown.
|
|
81
91
|
- `continueOnInitializeError` / `continueOnStartError` (and the stop/destroy equivalents) make the runtime finish with `success: false` and the failures listed instead of throwing.
|
|
82
92
|
- Every error the runtime throws extends `RuntimeError` (which itself extends `RuntimeError` from `@zudojs/errors`), so `isRuntimeError()` from either package recognises it.
|
|
83
93
|
- `runtime.context` is the runtime's immutable `RuntimeExecutionContext` (`executionId` = runtime id, `service` = runtime name, `metadata.runtimeId/runtimeName/runtimeMode/runtimeRole` plus `RuntimeOptions.metadata`); `runtime.timing` holds the state-transition timestamps; `runtime.contextStorage` is the `ContextStorage` the context is established in (`RuntimeDependencies.contextStorage`, default `getDefaultContextStorage()`).
|
|
84
94
|
|
|
85
95
|
### Signals
|
|
86
96
|
|
|
87
|
-
When `signals.handleSigint` / `handleSigterm` / `handleSighup` are on (SIGINT and SIGTERM default to on), the runtime registers handlers on start and removes them on stop, failure, or dispose. The first signal triggers a graceful `stop()`. A second signal during shutdown
|
|
97
|
+
When `signals.handleSigint` / `handleSigterm` / `handleSighup` are on (SIGINT and SIGTERM default to on), the runtime registers handlers on start and removes them on stop, failure, or dispose. The first signal triggers a graceful `stop()`. A second signal during shutdown exits the process with `signals.forceExitCode` (default 1); set `signals.forceExitOnSecondSignal: false` to log and ignore it instead. `handleUncaughtException` / `handleUnhandledRejection` mark the runtime failed, stop it, and then exit the process with code 1, because the installed handler suppresses Node's own crash and the process would otherwise end with code 0. `signals.fatalExitTimeout` (default 10000 ms) bounds a shutdown that hangs; set `signals.exitOnFatalError: false` to stop the runtime and keep the process running. The runtime never calls `process.exit()` otherwise.
|
|
88
98
|
|
|
89
99
|
## Modules
|
|
90
100
|
|
|
@@ -103,7 +113,7 @@ onInitialize → onReady → onShutdown → onDestroy
|
|
|
103
113
|
|
|
104
114
|
`createConfigurationManager({ registry, schemas, loaderOptions, validationOptions, redactorOptions })` loads registered sources by priority, deep-merges them, applies schema defaults, validates, and exposes an immutable `Configuration`.
|
|
105
115
|
|
|
106
|
-
- `manager.on(
|
|
116
|
+
- `manager.on(type, listener)` / `manager.off(...)` receive the `configuration.initializing`, `configuration.loaded`, `configuration.validated`, `configuration.ready`, `configuration.failed`, `configuration.reloading`, and `configuration.reloaded` events (`"*"` subscribes to all of them).
|
|
107
117
|
- `manager.getSection(name)` returns a scoped configuration for a registered section; `manager.reload()` keeps programmatic defaults and serialises overlapping reloads.
|
|
108
118
|
- Values are deep-frozen; `toObject()` returns an independent deep copy. Path lookups never walk the prototype chain, and `getNumber` accepts only plain decimal numbers.
|
|
109
119
|
- Secrets never reach errors or logs: validation, load, and source errors pass through the redactor, which matches whole key words (`password`, `secret`, `token`, `pwd`, `passphrase`, `auth`, `dsn`, `credential`, ...).
|
|
@@ -120,14 +130,14 @@ onInitialize → onReady → onShutdown → onDestroy
|
|
|
120
130
|
|
|
121
131
|
## Execution context
|
|
122
132
|
|
|
123
|
-
There is one execution-context model: the immutable `ExecutionContext` (`createExecutionContext`, `deriveExecutionContext`, `withExecutionMetadata`) propagated by `ContextStorage`, which wraps `AsyncLocalStorage`. `run(context, fn)` and `runDerived(overrides, fn)` establish a context for the callback and everything it awaits, `runWithValues(values, fn)`
|
|
133
|
+
There is one execution-context model: the immutable `ExecutionContext` (`createExecutionContext`, `deriveExecutionContext`, `withExecutionMetadata`) propagated by `ContextStorage`, which wraps `AsyncLocalStorage`. `run(context, fn)` and `runDerived(overrides, fn)` establish a context for the callback and everything it awaits, `runWithValues(context, values, fn)` additionally binds a `ContextValues` collection (read back with `getValues()`; `run()` starts a new execution without the enclosing execution's values, `runDerived()` keeps them), and `capture()` / `runSnapshot(snapshot, fn)` carry a context across queue or timer boundaries. `getDefaultContextStorage()` is the process-wide instance every component uses unless another is injected.
|
|
124
134
|
|
|
125
135
|
Propagation is real at runtime:
|
|
126
136
|
|
|
127
137
|
- `runtime.start()` and `runtime.stop()` run the bootstrap and shutdown pipelines inside `runtime.contextStorage.run(runtime.context, ...)`.
|
|
128
138
|
- Each module hook (`onInitialize`, `onReady`, `onShutdown`, `onDestroy`) runs in a context derived from it: `module` is the module id, `operation` is the hook name, and `{ moduleId, phase }` is merged into `metadata`. `ModuleLifecycleOptions.contextStorage` selects the storage (default: the shared one).
|
|
129
139
|
- `Application.start()` / `stop()` / `shutdown()` run lifecycle participants inside the runtime's context as well.
|
|
130
|
-
- `createApplication` threads one storage (`CreateApplicationOptions.contextStorage`) through the `ApplicationContext` (`getContextStorage()`), the container's `currentScope` (so `"scoped"` providers resolve once per execution context), the module lifecycle, the runtime, and the logger.
|
|
140
|
+
- `createApplication` threads one storage (`CreateApplicationOptions.contextStorage`) through the `ApplicationContext` (`getContextStorage()`), the container's `currentScope` (so `"scoped"` providers resolve once per execution context; resolving one outside any execution context, or from a singleton's factory, throws a `DependencyResolutionError`), the module lifecycle, the runtime, and the logger.
|
|
131
141
|
|
|
132
142
|
```typescript
|
|
133
143
|
import { defineModule, getDefaultContextStorage } from "@zudojs/core";
|
|
@@ -5,7 +5,14 @@ import type { Logger } from "../logging/core/logger.js";
|
|
|
5
5
|
import type { ContextStorage } from "../context/provider/contextStorage.storage.js";
|
|
6
6
|
export interface ApplicationContextOptions {
|
|
7
7
|
readonly container: Container;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The application configuration, or a function returning the
|
|
10
|
+
* current one. Pass a function (for example
|
|
11
|
+
* `() => manager.getConfiguration()`) so that
|
|
12
|
+
* `getConfiguration()` reflects configuration reloads instead of
|
|
13
|
+
* the snapshot taken when the context was created.
|
|
14
|
+
*/
|
|
15
|
+
readonly configuration: Configuration | (() => Configuration);
|
|
9
16
|
readonly modules: ModuleRegistry;
|
|
10
17
|
readonly logger: Logger;
|
|
11
18
|
/**
|
|
@@ -34,6 +41,10 @@ export declare class ApplicationContext {
|
|
|
34
41
|
getContainer(): Container;
|
|
35
42
|
/**
|
|
36
43
|
* Application configuration.
|
|
44
|
+
*
|
|
45
|
+
* When the context was created with a configuration accessor
|
|
46
|
+
* (as `createApplication` does), this returns the configuration
|
|
47
|
+
* manager's current configuration, including reloads.
|
|
37
48
|
*/
|
|
38
49
|
getConfiguration(): Configuration;
|
|
39
50
|
/**
|
|
@@ -13,7 +13,11 @@ export class ApplicationContext {
|
|
|
13
13
|
contextStorage;
|
|
14
14
|
constructor(options) {
|
|
15
15
|
this.container = options.container;
|
|
16
|
-
|
|
16
|
+
const configuration = options.configuration;
|
|
17
|
+
this.configuration =
|
|
18
|
+
typeof configuration === "function"
|
|
19
|
+
? configuration
|
|
20
|
+
: () => configuration;
|
|
17
21
|
this.modules = options.modules;
|
|
18
22
|
this.logger = options.logger;
|
|
19
23
|
this.contextStorage = options.contextStorage ?? getDefaultContextStorage();
|
|
@@ -26,9 +30,13 @@ export class ApplicationContext {
|
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* Application configuration.
|
|
33
|
+
*
|
|
34
|
+
* When the context was created with a configuration accessor
|
|
35
|
+
* (as `createApplication` does), this returns the configuration
|
|
36
|
+
* manager's current configuration, including reloads.
|
|
29
37
|
*/
|
|
30
38
|
getConfiguration() {
|
|
31
|
-
return this.configuration;
|
|
39
|
+
return this.configuration();
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
42
|
* Registered application modules.
|
|
@@ -44,7 +44,12 @@ export async function createApplication(options = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
const context = new ApplicationContext({
|
|
46
46
|
container,
|
|
47
|
-
|
|
47
|
+
/*
|
|
48
|
+
* An accessor rather than a snapshot: after
|
|
49
|
+
* `configuration.reload()` the application context must hand
|
|
50
|
+
* out the reloaded configuration, not the one captured here.
|
|
51
|
+
*/
|
|
52
|
+
configuration: () => configuration.getConfiguration(),
|
|
48
53
|
modules: moduleRegistry,
|
|
49
54
|
logger,
|
|
50
55
|
contextStorage,
|
|
@@ -53,6 +53,7 @@ export declare class ConfigurationManager {
|
|
|
53
53
|
private loadResult;
|
|
54
54
|
private validationReport;
|
|
55
55
|
private missingSetConfigurationWarned;
|
|
56
|
+
private inFlightReload;
|
|
56
57
|
private stateValue;
|
|
57
58
|
constructor(options?: ConfigurationManagerOptions);
|
|
58
59
|
/**
|
|
@@ -72,7 +73,16 @@ export declare class ConfigurationManager {
|
|
|
72
73
|
*/
|
|
73
74
|
getListenerErrors(): readonly unknown[];
|
|
74
75
|
initialize(): Promise<ConfigurationManagerResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Reloads configuration from the registered sources.
|
|
78
|
+
*
|
|
79
|
+
* Overlapping calls are serialised: a reload requested while one
|
|
80
|
+
* is already in progress shares that in-flight reload instead of
|
|
81
|
+
* failing with InvalidStateError. A manager that is not ready yet
|
|
82
|
+
* is initialized instead.
|
|
83
|
+
*/
|
|
75
84
|
reload(): Promise<ConfigurationManagerResult>;
|
|
85
|
+
private performReload;
|
|
76
86
|
getConfiguration(): Configuration;
|
|
77
87
|
getProvider(): ConfigurationProvider;
|
|
78
88
|
getRegistry(): ConfigurationRegistry;
|
|
@@ -31,6 +31,7 @@ export class ConfigurationManager {
|
|
|
31
31
|
loadResult;
|
|
32
32
|
validationReport;
|
|
33
33
|
missingSetConfigurationWarned = false;
|
|
34
|
+
inFlightReload;
|
|
34
35
|
stateValue = ConfigurationManagerState.CREATED;
|
|
35
36
|
constructor(options = {}) {
|
|
36
37
|
this.loader =
|
|
@@ -104,9 +105,26 @@ export class ConfigurationManager {
|
|
|
104
105
|
throw error;
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Reloads configuration from the registered sources.
|
|
110
|
+
*
|
|
111
|
+
* Overlapping calls are serialised: a reload requested while one
|
|
112
|
+
* is already in progress shares that in-flight reload instead of
|
|
113
|
+
* failing with InvalidStateError. A manager that is not ready yet
|
|
114
|
+
* is initialized instead.
|
|
115
|
+
*/
|
|
107
116
|
async reload() {
|
|
117
|
+
if (this.inFlightReload)
|
|
118
|
+
return this.inFlightReload;
|
|
108
119
|
if (this.stateValue !== ConfigurationManagerState.READY)
|
|
109
120
|
return this.initialize();
|
|
121
|
+
const reload = this.performReload().finally(() => {
|
|
122
|
+
this.inFlightReload = undefined;
|
|
123
|
+
});
|
|
124
|
+
this.inFlightReload = reload;
|
|
125
|
+
return reload;
|
|
126
|
+
}
|
|
127
|
+
async performReload() {
|
|
110
128
|
const previousConfiguration = this.configuration;
|
|
111
129
|
const previousLoadResult = this.loadResult;
|
|
112
130
|
const previousValidation = this.validationReport;
|
|
@@ -201,7 +219,7 @@ export class ConfigurationManager {
|
|
|
201
219
|
}
|
|
202
220
|
if (!this.missingSetConfigurationWarned) {
|
|
203
221
|
this.missingSetConfigurationWarned = true;
|
|
204
|
-
|
|
222
|
+
process.emitWarning("ConfigurationManager: the configured ConfigurationProvider does not implement setConfiguration(); loaded configuration will not be pushed into the provider.", { type: "ZudojsCoreWarning", code: "ZUDOJS_CONFIG_PROVIDER_NO_SET" });
|
|
205
223
|
}
|
|
206
224
|
}
|
|
207
225
|
emit(event) {
|
|
@@ -12,8 +12,8 @@ export interface ContainerOptions {
|
|
|
12
12
|
* new Container({ currentScope: () => storage.get() })
|
|
13
13
|
*
|
|
14
14
|
* "scoped" providers resolve to one instance per returned object.
|
|
15
|
-
* When it returns undefined and no explicit scope is active,
|
|
16
|
-
*
|
|
15
|
+
* When it returns undefined and no explicit scope is active, resolving
|
|
16
|
+
* a scoped provider throws a DependencyResolutionError.
|
|
17
17
|
*/
|
|
18
18
|
readonly currentScope?: () => object | undefined;
|
|
19
19
|
}
|
|
@@ -26,10 +26,11 @@ export interface ContainerOptions {
|
|
|
26
26
|
*/
|
|
27
27
|
export declare class Container {
|
|
28
28
|
private readonly providers;
|
|
29
|
-
private readonly scopedInstances;
|
|
30
29
|
private readonly currentScope;
|
|
31
30
|
private readonly resolving;
|
|
32
31
|
private activeScope;
|
|
32
|
+
/** The singleton under construction, if any; see assertScopedResolvable. */
|
|
33
|
+
private captor;
|
|
33
34
|
constructor(options?: ContainerOptions);
|
|
34
35
|
/**
|
|
35
36
|
* Registers a provider in the container.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ProviderNotFoundError, ProviderAlreadyRegisteredError, InvalidProviderError, DependencyResolutionError, } from "../errors/exceptions.js";
|
|
2
|
+
import { assertScopedResolvable } from "./container.lifetime.js";
|
|
2
3
|
/**
|
|
3
4
|
* Dependency Injection container for Zudojs applications.
|
|
4
5
|
*
|
|
@@ -8,10 +9,11 @@ import { ProviderNotFoundError, ProviderAlreadyRegisteredError, InvalidProviderE
|
|
|
8
9
|
*/
|
|
9
10
|
export class Container {
|
|
10
11
|
providers = new Map();
|
|
11
|
-
scopedInstances = new WeakMap();
|
|
12
12
|
currentScope;
|
|
13
13
|
resolving = [];
|
|
14
14
|
activeScope;
|
|
15
|
+
/** The singleton under construction, if any; see assertScopedResolvable. */
|
|
16
|
+
captor;
|
|
15
17
|
constructor(options = {}) {
|
|
16
18
|
this.currentScope = options.currentScope;
|
|
17
19
|
}
|
|
@@ -29,6 +31,7 @@ export class Container {
|
|
|
29
31
|
provider: provider,
|
|
30
32
|
scope,
|
|
31
33
|
resolved: false,
|
|
34
|
+
scopedInstances: new WeakMap(),
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
37
|
/**
|
|
@@ -80,10 +83,12 @@ export class Container {
|
|
|
80
83
|
if (registration.scope === "singleton" && registration.resolved) {
|
|
81
84
|
return registration.instance;
|
|
82
85
|
}
|
|
86
|
+
if (registration.scope === "scoped") {
|
|
87
|
+
assertScopedResolvable(token, this.captor, scopeKey, this.resolving);
|
|
88
|
+
}
|
|
83
89
|
if (registration.scope === "scoped" && scopeKey) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return cache.get(token);
|
|
90
|
+
if (registration.scopedInstances.has(scopeKey)) {
|
|
91
|
+
return registration.scopedInstances.get(scopeKey);
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
const instance = this.createInstance(token, registration, scopeKey);
|
|
@@ -92,12 +97,7 @@ export class Container {
|
|
|
92
97
|
registration.resolved = true;
|
|
93
98
|
}
|
|
94
99
|
else if (registration.scope === "scoped" && scopeKey) {
|
|
95
|
-
|
|
96
|
-
if (!cache) {
|
|
97
|
-
cache = new Map();
|
|
98
|
-
this.scopedInstances.set(scopeKey, cache);
|
|
99
|
-
}
|
|
100
|
-
cache.set(token, instance);
|
|
100
|
+
registration.scopedInstances.set(scopeKey, instance);
|
|
101
101
|
}
|
|
102
102
|
return instance;
|
|
103
103
|
}
|
|
@@ -115,8 +115,12 @@ export class Container {
|
|
|
115
115
|
return provider.useValue;
|
|
116
116
|
}
|
|
117
117
|
const previousScope = this.activeScope;
|
|
118
|
+
const previousCaptor = this.captor;
|
|
118
119
|
this.resolving.push(token);
|
|
119
120
|
this.activeScope = scopeKey;
|
|
121
|
+
if (registration.scope === "singleton") {
|
|
122
|
+
this.captor = token;
|
|
123
|
+
}
|
|
120
124
|
try {
|
|
121
125
|
if ("useFactory" in provider) {
|
|
122
126
|
return provider.useFactory(this);
|
|
@@ -131,6 +135,7 @@ export class Container {
|
|
|
131
135
|
finally {
|
|
132
136
|
this.resolving.pop();
|
|
133
137
|
this.activeScope = previousScope;
|
|
138
|
+
this.captor = previousCaptor;
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Token } from "./token.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rejects a "scoped" resolution that would outlive or escape its scope.
|
|
4
|
+
*
|
|
5
|
+
* - A scoped provider resolved (directly or transitively) while a
|
|
6
|
+
* singleton is being constructed would be captured by that singleton
|
|
7
|
+
* and served to every later scope: one request's user, tenant or
|
|
8
|
+
* transaction leaking into all the others. This is the captive
|
|
9
|
+
* dependency error mature DI containers raise.
|
|
10
|
+
* - A scoped provider resolved with no active scope has nothing to be
|
|
11
|
+
* scoped to. It used to behave as transient, which hid the missing
|
|
12
|
+
* scope; it now fails like `@zudojs/container`'s ScopedResolutionError.
|
|
13
|
+
*
|
|
14
|
+
* @param token - The scoped token being resolved.
|
|
15
|
+
* @param captor - The singleton currently under construction, if any.
|
|
16
|
+
* @param scopeKey - The active scope key, if any.
|
|
17
|
+
* @param chain - The current resolution chain, for the diagnostic.
|
|
18
|
+
* @throws DependencyResolutionError when either rule is broken.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertScopedResolvable(token: Token<unknown>, captor: Token<unknown> | undefined, scopeKey: object | undefined, chain: readonly Token<unknown>[]): void;
|
|
21
|
+
//# sourceMappingURL=container.lifetime.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DependencyResolutionError } from "../errors/exceptions.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rejects a "scoped" resolution that would outlive or escape its scope.
|
|
4
|
+
*
|
|
5
|
+
* - A scoped provider resolved (directly or transitively) while a
|
|
6
|
+
* singleton is being constructed would be captured by that singleton
|
|
7
|
+
* and served to every later scope: one request's user, tenant or
|
|
8
|
+
* transaction leaking into all the others. This is the captive
|
|
9
|
+
* dependency error mature DI containers raise.
|
|
10
|
+
* - A scoped provider resolved with no active scope has nothing to be
|
|
11
|
+
* scoped to. It used to behave as transient, which hid the missing
|
|
12
|
+
* scope; it now fails like `@zudojs/container`'s ScopedResolutionError.
|
|
13
|
+
*
|
|
14
|
+
* @param token - The scoped token being resolved.
|
|
15
|
+
* @param captor - The singleton currently under construction, if any.
|
|
16
|
+
* @param scopeKey - The active scope key, if any.
|
|
17
|
+
* @param chain - The current resolution chain, for the diagnostic.
|
|
18
|
+
* @throws DependencyResolutionError when either rule is broken.
|
|
19
|
+
*/
|
|
20
|
+
export function assertScopedResolvable(token, captor, scopeKey, chain) {
|
|
21
|
+
if (captor !== undefined) {
|
|
22
|
+
throw new DependencyResolutionError(`Captive dependency: singleton "${describe(captor)}" cannot depend on ` +
|
|
23
|
+
`scoped "${describe(token)}". Register the consumer as "scoped" or ` +
|
|
24
|
+
`"transient", or resolve the scoped dependency lazily per call.`, [...chain, token]);
|
|
25
|
+
}
|
|
26
|
+
if (scopeKey === undefined) {
|
|
27
|
+
throw new DependencyResolutionError(`Scoped provider "${describe(token)}" was resolved outside any scope. ` +
|
|
28
|
+
`Resolve it through container.createScope() or inside an execution ` +
|
|
29
|
+
`context.`, [...chain, token]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function describe(token) {
|
|
33
|
+
if (typeof token === "function")
|
|
34
|
+
return token.name || "anonymous class";
|
|
35
|
+
if (typeof token === "symbol")
|
|
36
|
+
return token.description ?? token.toString();
|
|
37
|
+
return String(token);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=container.lifetime.js.map
|
|
@@ -15,7 +15,9 @@ export type Scope =
|
|
|
15
15
|
* example the current ExecutionContext of an HTTP request,
|
|
16
16
|
* background job, message consumption, or RPC call).
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Resolving a scoped provider with no active scope, or from inside a
|
|
19
|
+
* singleton's construction (a captive dependency), throws a
|
|
20
|
+
* DependencyResolutionError.
|
|
19
21
|
*/
|
|
20
22
|
| "scoped"
|
|
21
23
|
/**
|
|
@@ -30,6 +30,14 @@ export declare class ContextStorage {
|
|
|
30
30
|
*
|
|
31
31
|
* The context is automatically available to all asynchronous
|
|
32
32
|
* operations created within the callback.
|
|
33
|
+
*
|
|
34
|
+
* A new execution starts without ContextValues: values bound by an
|
|
35
|
+
* enclosing `runWithValues` belong to that execution (its tenant,
|
|
36
|
+
* user, transaction) and must not leak into an unrelated one started
|
|
37
|
+
* from inside its async scope, such as a listener or consumer created
|
|
38
|
+
* during a request. Re-entering the execution that is already current
|
|
39
|
+
* keeps its values. Use `runDerived` or `runWithValues` to carry
|
|
40
|
+
* values on purpose.
|
|
33
41
|
*/
|
|
34
42
|
run<T>(context: ExecutionContext, callback: () => T): T;
|
|
35
43
|
/**
|
|
@@ -24,9 +24,20 @@ export class ContextStorage {
|
|
|
24
24
|
*
|
|
25
25
|
* The context is automatically available to all asynchronous
|
|
26
26
|
* operations created within the callback.
|
|
27
|
+
*
|
|
28
|
+
* A new execution starts without ContextValues: values bound by an
|
|
29
|
+
* enclosing `runWithValues` belong to that execution (its tenant,
|
|
30
|
+
* user, transaction) and must not leak into an unrelated one started
|
|
31
|
+
* from inside its async scope, such as a listener or consumer created
|
|
32
|
+
* during a request. Re-entering the execution that is already current
|
|
33
|
+
* keeps its values. Use `runDerived` or `runWithValues` to carry
|
|
34
|
+
* values on purpose.
|
|
27
35
|
*/
|
|
28
36
|
run(context, callback) {
|
|
29
|
-
|
|
37
|
+
if (context === this.storage.getStore()) {
|
|
38
|
+
return this.storage.run(context, callback);
|
|
39
|
+
}
|
|
40
|
+
return this.storage.run(context, () => this.valuesStorage.exit(callback));
|
|
30
41
|
}
|
|
31
42
|
/**
|
|
32
43
|
* Returns the current execution context.
|
|
@@ -39,9 +39,12 @@ export interface LifecycleOptions {
|
|
|
39
39
|
*
|
|
40
40
|
* CREATED → INITIALIZING → INITIALIZED → STARTING → RUNNING
|
|
41
41
|
* RUNNING → STOPPING → STOPPED → (STARTING → RUNNING) restart
|
|
42
|
-
* any phase failure → FAILED; initialize()
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* any phase failure → FAILED; initialize() retries from FAILED resume
|
|
43
|
+
* with the participant that failed. A failed start() is rolled back:
|
|
44
|
+
* every participant whose start() completed is stopped in reverse
|
|
45
|
+
* order, so a retry starts again from the first one. dispose() only
|
|
46
|
+
* reaches participants whose initialize() ran (including one that
|
|
47
|
+
* threw, which may have acquired resources before failing).
|
|
45
48
|
*
|
|
46
49
|
* Concurrent callers of the same phase share the in-flight promise.
|
|
47
50
|
*/
|
|
@@ -52,6 +55,8 @@ export declare class Lifecycle {
|
|
|
52
55
|
private readonly continueOnShutdownError;
|
|
53
56
|
private initializedCount;
|
|
54
57
|
private startedCount;
|
|
58
|
+
/** Participants whose initialize() was invoked, whether or not it threw. */
|
|
59
|
+
private initializeAttempted;
|
|
55
60
|
private disposed;
|
|
56
61
|
private failedPhase;
|
|
57
62
|
private initializePromise;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InvalidStateError } from "../../errors/exceptions.js";
|
|
2
|
+
import { rollbackStartedParticipants } from "./lifecycle.rollback.js";
|
|
2
3
|
/** Lifecycle states supported by the Zudojs application runtime. */
|
|
3
4
|
export const LifecycleState = {
|
|
4
5
|
CREATED: "created",
|
|
@@ -18,9 +19,12 @@ export const LifecycleState = {
|
|
|
18
19
|
*
|
|
19
20
|
* CREATED → INITIALIZING → INITIALIZED → STARTING → RUNNING
|
|
20
21
|
* RUNNING → STOPPING → STOPPED → (STARTING → RUNNING) restart
|
|
21
|
-
* any phase failure → FAILED; initialize()
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* any phase failure → FAILED; initialize() retries from FAILED resume
|
|
23
|
+
* with the participant that failed. A failed start() is rolled back:
|
|
24
|
+
* every participant whose start() completed is stopped in reverse
|
|
25
|
+
* order, so a retry starts again from the first one. dispose() only
|
|
26
|
+
* reaches participants whose initialize() ran (including one that
|
|
27
|
+
* threw, which may have acquired resources before failing).
|
|
24
28
|
*
|
|
25
29
|
* Concurrent callers of the same phase share the in-flight promise.
|
|
26
30
|
*/
|
|
@@ -31,6 +35,8 @@ export class Lifecycle {
|
|
|
31
35
|
continueOnShutdownError;
|
|
32
36
|
initializedCount = 0;
|
|
33
37
|
startedCount = 0;
|
|
38
|
+
/** Participants whose initialize() was invoked, whether or not it threw. */
|
|
39
|
+
initializeAttempted = 0;
|
|
34
40
|
disposed = false;
|
|
35
41
|
failedPhase;
|
|
36
42
|
initializePromise;
|
|
@@ -194,7 +200,8 @@ export class Lifecycle {
|
|
|
194
200
|
}
|
|
195
201
|
this.disposed = true;
|
|
196
202
|
const errors = [];
|
|
197
|
-
|
|
203
|
+
// A participant whose initialize() never ran has nothing to release.
|
|
204
|
+
for (let i = this.initializeAttempted - 1; i >= 0; i--) {
|
|
198
205
|
const participant = this.participants[i];
|
|
199
206
|
try {
|
|
200
207
|
this.logger?.debug("Disposing lifecycle participant", {
|
|
@@ -213,6 +220,7 @@ export class Lifecycle {
|
|
|
213
220
|
}
|
|
214
221
|
this.initializedCount = 0;
|
|
215
222
|
this.startedCount = 0;
|
|
223
|
+
this.initializeAttempted = 0;
|
|
216
224
|
if (errors.length > 0 && !this.continueOnShutdownError) {
|
|
217
225
|
this.state = LifecycleState.FAILED;
|
|
218
226
|
this.failedPhase = "dispose";
|
|
@@ -255,6 +263,7 @@ export class Lifecycle {
|
|
|
255
263
|
this.logger?.debug("Initializing lifecycle participant", {
|
|
256
264
|
participant: participant.name,
|
|
257
265
|
});
|
|
266
|
+
this.initializeAttempted = Math.max(this.initializeAttempted, i + 1);
|
|
258
267
|
await participant.initialize?.();
|
|
259
268
|
this.initializedCount = i + 1;
|
|
260
269
|
}
|
|
@@ -285,9 +294,13 @@ export class Lifecycle {
|
|
|
285
294
|
this.logger?.info("Application startup completed");
|
|
286
295
|
}
|
|
287
296
|
catch (error) {
|
|
297
|
+
this.logger?.error("Application startup failed", error);
|
|
298
|
+
// Roll back rather than leave earlier participants running until
|
|
299
|
+
// someone remembers to call shutdown().
|
|
300
|
+
await rollbackStartedParticipants(this.participants, this.startedCount, this.logger);
|
|
301
|
+
this.startedCount = 0;
|
|
288
302
|
this.state = LifecycleState.FAILED;
|
|
289
303
|
this.failedPhase = "start";
|
|
290
|
-
this.logger?.error("Application startup failed", error);
|
|
291
304
|
throw error;
|
|
292
305
|
}
|
|
293
306
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Logger } from "../../logging/core/logger.js";
|
|
2
|
+
import type { LifecycleParticipant } from "./lifecycle.js";
|
|
3
|
+
/**
|
|
4
|
+
* Stops, in reverse order, the first `count` participants after a
|
|
5
|
+
* failed start.
|
|
6
|
+
*
|
|
7
|
+
* Only participants whose `start()` completed are stopped; the one
|
|
8
|
+
* that threw is left to its own error handling, as `stop()` has always
|
|
9
|
+
* done. Stop failures are logged and returned; they never mask the
|
|
10
|
+
* start error.
|
|
11
|
+
*
|
|
12
|
+
* @param participants - Registered participants, in start order.
|
|
13
|
+
* @param count - How many leading participants started successfully.
|
|
14
|
+
* @param logger - Optional logger for rollback diagnostics.
|
|
15
|
+
* @returns The errors raised by `stop()` hooks during the rollback.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rollbackStartedParticipants(participants: readonly LifecycleParticipant[], count: number, logger: Logger | undefined): Promise<readonly unknown[]>;
|
|
18
|
+
//# sourceMappingURL=lifecycle.rollback.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stops, in reverse order, the first `count` participants after a
|
|
3
|
+
* failed start.
|
|
4
|
+
*
|
|
5
|
+
* Only participants whose `start()` completed are stopped; the one
|
|
6
|
+
* that threw is left to its own error handling, as `stop()` has always
|
|
7
|
+
* done. Stop failures are logged and returned; they never mask the
|
|
8
|
+
* start error.
|
|
9
|
+
*
|
|
10
|
+
* @param participants - Registered participants, in start order.
|
|
11
|
+
* @param count - How many leading participants started successfully.
|
|
12
|
+
* @param logger - Optional logger for rollback diagnostics.
|
|
13
|
+
* @returns The errors raised by `stop()` hooks during the rollback.
|
|
14
|
+
*/
|
|
15
|
+
export async function rollbackStartedParticipants(participants, count, logger) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
18
|
+
const participant = participants[i];
|
|
19
|
+
try {
|
|
20
|
+
logger?.debug("Rolling back lifecycle participant", {
|
|
21
|
+
participant: participant.name,
|
|
22
|
+
});
|
|
23
|
+
await participant.stop?.();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
errors.push(error);
|
|
27
|
+
logger?.error("Failed to roll back lifecycle participant", error, {
|
|
28
|
+
participant: participant.name,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return errors;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=lifecycle.rollback.js.map
|
|
@@ -123,7 +123,18 @@ export function sanitizeLogValue(value, seen = new WeakSet(), depth = 0) {
|
|
|
123
123
|
}
|
|
124
124
|
const result = {};
|
|
125
125
|
for (const [key, item] of Object.entries(objectValue)) {
|
|
126
|
-
|
|
126
|
+
/*
|
|
127
|
+
* Assigning result["__proto__"] would replace the prototype
|
|
128
|
+
* (and drop the value from serialized output) instead of
|
|
129
|
+
* storing the key, so own "__proto__" keys — as produced by
|
|
130
|
+
* JSON.parse on untrusted input — are defined explicitly.
|
|
131
|
+
*/
|
|
132
|
+
Object.defineProperty(result, key, {
|
|
133
|
+
value: sanitizeLogValue(item, seen, depth + 1),
|
|
134
|
+
enumerable: true,
|
|
135
|
+
writable: true,
|
|
136
|
+
configurable: true,
|
|
137
|
+
});
|
|
127
138
|
}
|
|
128
139
|
return result;
|
|
129
140
|
}
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
* state maps) is internal to this subsystem and intentionally
|
|
9
9
|
* not exported from this barrel.
|
|
10
10
|
*/
|
|
11
|
-
export { type ModuleLifecyclePhase, type ModuleLifecycleState, type ModuleLifecycleHookName, type ModuleLifecycleStep, type ModuleLifecycleHooks, type ModuleLifecycleOptions, ModuleLifecycleError, type ModuleLifecycleResult, type ModuleLifecycleSkip, } from "./moduleLifecycle.type.js";
|
|
11
|
+
export { type ModuleLifecyclePhase, type ModuleLifecycleState, type ModuleLifecycleHookName, type ModuleLifecycleStep, type ModuleLifecycleHooks, type ModuleLifecycleOptions, type ModuleLifecyclePhaseOptions, ModuleLifecycleError, type ModuleLifecycleResult, type ModuleLifecycleSkip, } from "./moduleLifecycle.type.js";
|
|
12
12
|
export { ModuleLifecycleManager, createModuleLifecycleManager, } from "./moduleLifecycle.lifecycle.js";
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|