@zudojs/core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- 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 +18 -0
- package/dist/container/container.d.ts +0 -1
- package/dist/container/container.js +4 -10
- 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.type.d.ts +13 -0
- package/dist/modules/moduleLoader/moduleLoader.loader.js +61 -25
- package/dist/runtime/runtimeBootstrap/pipeline/runtimeBootstrap.pipeline.js +15 -2
- package/dist/runtime/runtimeOptions/runtimeOptions.resolver.js +8 -0
- package/dist/runtime/runtimeShutdown/pipeline/runtimeShutdown.pipeline.js +10 -2
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ onInitialize → onReady → onShutdown → onDestroy
|
|
|
103
103
|
|
|
104
104
|
`createConfigurationManager({ registry, schemas, loaderOptions, validationOptions, redactorOptions })` loads registered sources by priority, deep-merges them, applies schema defaults, validates, and exposes an immutable `Configuration`.
|
|
105
105
|
|
|
106
|
-
- `manager.on(
|
|
106
|
+
- `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
107
|
- `manager.getSection(name)` returns a scoped configuration for a registered section; `manager.reload()` keeps programmatic defaults and serialises overlapping reloads.
|
|
108
108
|
- 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
109
|
- 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,7 +120,7 @@ onInitialize → onReady → onShutdown → onDestroy
|
|
|
120
120
|
|
|
121
121
|
## Execution context
|
|
122
122
|
|
|
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)`
|
|
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(context, values, fn)` additionally binds a `ContextValues` collection (read back with `getValues()`), 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
124
|
|
|
125
125
|
Propagation is real at runtime:
|
|
126
126
|
|
|
@@ -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;
|
|
@@ -8,7 +8,6 @@ import { ProviderNotFoundError, ProviderAlreadyRegisteredError, InvalidProviderE
|
|
|
8
8
|
*/
|
|
9
9
|
export class Container {
|
|
10
10
|
providers = new Map();
|
|
11
|
-
scopedInstances = new WeakMap();
|
|
12
11
|
currentScope;
|
|
13
12
|
resolving = [];
|
|
14
13
|
activeScope;
|
|
@@ -29,6 +28,7 @@ export class Container {
|
|
|
29
28
|
provider: provider,
|
|
30
29
|
scope,
|
|
31
30
|
resolved: false,
|
|
31
|
+
scopedInstances: new WeakMap(),
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
@@ -81,9 +81,8 @@ export class Container {
|
|
|
81
81
|
return registration.instance;
|
|
82
82
|
}
|
|
83
83
|
if (registration.scope === "scoped" && scopeKey) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return cache.get(token);
|
|
84
|
+
if (registration.scopedInstances.has(scopeKey)) {
|
|
85
|
+
return registration.scopedInstances.get(scopeKey);
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
const instance = this.createInstance(token, registration, scopeKey);
|
|
@@ -92,12 +91,7 @@ export class Container {
|
|
|
92
91
|
registration.resolved = true;
|
|
93
92
|
}
|
|
94
93
|
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);
|
|
94
|
+
registration.scopedInstances.set(scopeKey, instance);
|
|
101
95
|
}
|
|
102
96
|
return instance;
|
|
103
97
|
}
|
|
@@ -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
|
|
@@ -2,7 +2,8 @@ import type { Module, ModuleId } from "../module.js";
|
|
|
2
2
|
import type { ModuleDefinition } from "../moduleDefinition.definition.js";
|
|
3
3
|
import type { ModuleRegistration, ModuleRegistry } from "../moduleRegistry/index.js";
|
|
4
4
|
import type { ModuleLoader } from "../moduleLoader/index.js";
|
|
5
|
-
import type { ModuleLifecycleOptions, ModuleLifecycleResult, ModuleLifecycleState } from "./moduleLifecycle.type.js";
|
|
5
|
+
import type { ModuleLifecycleOptions, ModuleLifecyclePhaseOptions, ModuleLifecycleResult, ModuleLifecycleState } from "./moduleLifecycle.type.js";
|
|
6
|
+
import type { ModuleDependency } from "../moduleDependency/moduleDependency.type.js";
|
|
6
7
|
/**
|
|
7
8
|
* Module lifecycle manager.
|
|
8
9
|
* Initializes, starts, stops, and destroys modules in dependency order.
|
|
@@ -15,10 +16,10 @@ export declare class ModuleLifecycleManager {
|
|
|
15
16
|
private readonly states;
|
|
16
17
|
private operation;
|
|
17
18
|
constructor(registry: ModuleRegistry, loader: ModuleLoader, options?: ModuleLifecycleOptions);
|
|
18
|
-
initialize(): Promise<ModuleLifecycleResult>;
|
|
19
|
-
start(): Promise<ModuleLifecycleResult>;
|
|
20
|
-
stop(): Promise<ModuleLifecycleResult>;
|
|
21
|
-
destroy(): Promise<ModuleLifecycleResult>;
|
|
19
|
+
initialize(options?: ModuleLifecyclePhaseOptions): Promise<ModuleLifecycleResult>;
|
|
20
|
+
start(options?: ModuleLifecyclePhaseOptions): Promise<ModuleLifecycleResult>;
|
|
21
|
+
stop(options?: ModuleLifecyclePhaseOptions): Promise<ModuleLifecycleResult>;
|
|
22
|
+
destroy(options?: ModuleLifecyclePhaseOptions): Promise<ModuleLifecycleResult>;
|
|
22
23
|
/**
|
|
23
24
|
* Rolls back modules that completed earlier phases after a
|
|
24
25
|
* startup failure.
|
|
@@ -55,6 +56,14 @@ export declare class ModuleLifecycleManager {
|
|
|
55
56
|
* optional dependency on the given module.
|
|
56
57
|
*/
|
|
57
58
|
private getLoadedDependents;
|
|
59
|
+
/**
|
|
60
|
+
* Dependencies of a registered module: those declared on the
|
|
61
|
+
* definition plus any the loaded instance declares itself
|
|
62
|
+
* (`Module.dependencies`, e.g. via the BaseModule constructor).
|
|
63
|
+
* Instance-declared dependencies are required; a definition
|
|
64
|
+
* entry for the same id wins so optional/version flags survive.
|
|
65
|
+
*/
|
|
66
|
+
private resolveDependencies;
|
|
58
67
|
startApplication(): Promise<{
|
|
59
68
|
readonly initialized: ModuleLifecycleResult;
|
|
60
69
|
readonly started: ModuleLifecycleResult;
|
|
@@ -69,11 +78,27 @@ export declare class ModuleLifecycleManager {
|
|
|
69
78
|
isInitialized(moduleId: ModuleId): boolean;
|
|
70
79
|
isStarted(moduleId: ModuleId): boolean;
|
|
71
80
|
isDestroyed(moduleId: ModuleId): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Builds the dependency graph over the loaded modules only.
|
|
83
|
+
*
|
|
84
|
+
* Lifecycle phases run for loaded modules, so a registered but
|
|
85
|
+
* unloaded definition (autoLoad: false) must not be able to
|
|
86
|
+
* break them with a missing or circular dependency of its own.
|
|
87
|
+
* Dependencies of a loaded module that are not loaded are kept
|
|
88
|
+
* as edges so the ordering reports them as missing.
|
|
89
|
+
*/
|
|
72
90
|
private createGraph;
|
|
73
91
|
private getStartupOrder;
|
|
74
92
|
private getShutdownOrder;
|
|
75
93
|
private runExclusive;
|
|
76
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolves the dependencies of a registration: definition-declared
|
|
97
|
+
* dependencies plus the ids the loaded instance declares through
|
|
98
|
+
* `Module.dependencies`. Shared by the lifecycle manager and the
|
|
99
|
+
* loader so both agree on the graph.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveRegistrationDependencies(registry: ModuleRegistry, registration: ModuleRegistration): readonly ModuleDependency[];
|
|
77
102
|
/** Creates a module lifecycle manager. */
|
|
78
103
|
export declare function createModuleLifecycleManager(registry: ModuleRegistry, loader: ModuleLoader, options?: ModuleLifecycleOptions): ModuleLifecycleManager;
|
|
79
104
|
//# sourceMappingURL=moduleLifecycle.lifecycle.d.ts.map
|
|
@@ -25,11 +25,11 @@ export class ModuleLifecycleManager {
|
|
|
25
25
|
};
|
|
26
26
|
this.contextStorage = options.contextStorage ?? getDefaultContextStorage();
|
|
27
27
|
}
|
|
28
|
-
async initialize() {
|
|
28
|
+
async initialize(options = {}) {
|
|
29
29
|
return this.runExclusive(async () => {
|
|
30
30
|
ensureStateSynchronized(this.registry, this.states);
|
|
31
31
|
try {
|
|
32
|
-
return await executeLifecyclePhase(this.getStartupOrder(), "initialize", "initializing", "initialized", this.options.continueOnInitializeError, this.registry, this.loader, this.states, this.contextStorage);
|
|
32
|
+
return await executeLifecyclePhase(this.getStartupOrder(), "initialize", "initializing", "initialized", options.continueOnError ?? this.options.continueOnInitializeError, this.registry, this.loader, this.states, this.contextStorage);
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
35
|
await this.rollbackAfterFailure(error, { stopFirst: false });
|
|
@@ -37,11 +37,11 @@ export class ModuleLifecycleManager {
|
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
-
async start() {
|
|
40
|
+
async start(options = {}) {
|
|
41
41
|
return this.runExclusive(async () => {
|
|
42
42
|
ensureStateSynchronized(this.registry, this.states);
|
|
43
43
|
try {
|
|
44
|
-
return await executeLifecyclePhase(this.getStartupOrder(), "start", "starting", "started", this.options.continueOnStartError, this.registry, this.loader, this.states, this.contextStorage);
|
|
44
|
+
return await executeLifecyclePhase(this.getStartupOrder(), "start", "starting", "started", options.continueOnError ?? this.options.continueOnStartError, this.registry, this.loader, this.states, this.contextStorage);
|
|
45
45
|
}
|
|
46
46
|
catch (error) {
|
|
47
47
|
await this.rollbackAfterFailure(error, { stopFirst: true });
|
|
@@ -49,16 +49,16 @@ export class ModuleLifecycleManager {
|
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
-
async stop() {
|
|
52
|
+
async stop(options = {}) {
|
|
53
53
|
return this.runExclusive(async () => {
|
|
54
54
|
ensureStateSynchronized(this.registry, this.states);
|
|
55
|
-
return executeLifecyclePhase(this.getShutdownOrder(), "stop", "stopping", "stopped", this.options.continueOnStopError, this.registry, this.loader, this.states, this.contextStorage);
|
|
55
|
+
return executeLifecyclePhase(this.getShutdownOrder(), "stop", "stopping", "stopped", options.continueOnError ?? this.options.continueOnStopError, this.registry, this.loader, this.states, this.contextStorage);
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
-
async destroy() {
|
|
58
|
+
async destroy(options = {}) {
|
|
59
59
|
return this.runExclusive(async () => {
|
|
60
60
|
ensureStateSynchronized(this.registry, this.states);
|
|
61
|
-
return executeLifecyclePhase(this.getShutdownOrder(), "destroy", "destroying", "destroyed", this.options.continueOnDestroyError, this.registry, this.loader, this.states, this.contextStorage);
|
|
61
|
+
return executeLifecyclePhase(this.getShutdownOrder(), "destroy", "destroying", "destroyed", options.continueOnError ?? this.options.continueOnDestroyError, this.registry, this.loader, this.states, this.contextStorage);
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
@@ -178,14 +178,22 @@ export class ModuleLifecycleManager {
|
|
|
178
178
|
continue;
|
|
179
179
|
if (registration.state !== "loaded")
|
|
180
180
|
continue;
|
|
181
|
-
const dependsOnModule = this.
|
|
182
|
-
.getDependencies(registration.definition.id)
|
|
183
|
-
.some((dependency) => dependency.id === moduleId);
|
|
181
|
+
const dependsOnModule = this.resolveDependencies(registration).some((dependency) => dependency.id === moduleId);
|
|
184
182
|
if (dependsOnModule)
|
|
185
183
|
dependents.push(registration.definition.id);
|
|
186
184
|
}
|
|
187
185
|
return dependents;
|
|
188
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Dependencies of a registered module: those declared on the
|
|
189
|
+
* definition plus any the loaded instance declares itself
|
|
190
|
+
* (`Module.dependencies`, e.g. via the BaseModule constructor).
|
|
191
|
+
* Instance-declared dependencies are required; a definition
|
|
192
|
+
* entry for the same id wins so optional/version flags survive.
|
|
193
|
+
*/
|
|
194
|
+
resolveDependencies(registration) {
|
|
195
|
+
return resolveRegistrationDependencies(this.registry, registration);
|
|
196
|
+
}
|
|
189
197
|
async startApplication() {
|
|
190
198
|
/*
|
|
191
199
|
* When continueOn*Error is disabled, phase failures are
|
|
@@ -220,21 +228,31 @@ export class ModuleLifecycleManager {
|
|
|
220
228
|
isDestroyed(moduleId) {
|
|
221
229
|
return isModuleDestroyed(moduleId, this.states);
|
|
222
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Builds the dependency graph over the loaded modules only.
|
|
233
|
+
*
|
|
234
|
+
* Lifecycle phases run for loaded modules, so a registered but
|
|
235
|
+
* unloaded definition (autoLoad: false) must not be able to
|
|
236
|
+
* break them with a missing or circular dependency of its own.
|
|
237
|
+
* Dependencies of a loaded module that are not loaded are kept
|
|
238
|
+
* as edges so the ordering reports them as missing.
|
|
239
|
+
*/
|
|
223
240
|
createGraph() {
|
|
224
|
-
const nodes = this.registry
|
|
241
|
+
const nodes = this.registry
|
|
242
|
+
.getAll()
|
|
243
|
+
.filter((r) => r.state === "loaded")
|
|
244
|
+
.map((r) => ({
|
|
225
245
|
id: r.definition.id,
|
|
226
|
-
dependencies: this.
|
|
246
|
+
dependencies: this.resolveDependencies(r),
|
|
227
247
|
version: r.definition.version,
|
|
228
248
|
}));
|
|
229
249
|
return createModuleDependencyGraph(nodes);
|
|
230
250
|
}
|
|
231
251
|
getStartupOrder() {
|
|
232
|
-
|
|
233
|
-
return Object.freeze(order.filter((id) => this.registry.get(id)?.state === "loaded"));
|
|
252
|
+
return resolveModuleStartupOrder(this.createGraph());
|
|
234
253
|
}
|
|
235
254
|
getShutdownOrder() {
|
|
236
|
-
|
|
237
|
-
return Object.freeze(order.filter((id) => this.registry.get(id)?.state === "loaded"));
|
|
255
|
+
return resolveModuleShutdownOrder(this.createGraph());
|
|
238
256
|
}
|
|
239
257
|
async runExclusive(operation) {
|
|
240
258
|
while (this.operation)
|
|
@@ -253,6 +271,29 @@ export class ModuleLifecycleManager {
|
|
|
253
271
|
}
|
|
254
272
|
}
|
|
255
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Resolves the dependencies of a registration: definition-declared
|
|
276
|
+
* dependencies plus the ids the loaded instance declares through
|
|
277
|
+
* `Module.dependencies`. Shared by the lifecycle manager and the
|
|
278
|
+
* loader so both agree on the graph.
|
|
279
|
+
*/
|
|
280
|
+
export function resolveRegistrationDependencies(registry, registration) {
|
|
281
|
+
const declared = registry.getDependencies(registration.definition.id);
|
|
282
|
+
const instanceDependencies = registration.instance?.dependencies ?? [];
|
|
283
|
+
if (instanceDependencies.length === 0)
|
|
284
|
+
return declared;
|
|
285
|
+
const seen = new Set(declared.map((dependency) => dependency.id));
|
|
286
|
+
const merged = [...declared];
|
|
287
|
+
for (const id of instanceDependencies) {
|
|
288
|
+
if (typeof id !== "string" || id.length === 0 || seen.has(id))
|
|
289
|
+
continue;
|
|
290
|
+
if (id === registration.definition.id)
|
|
291
|
+
continue;
|
|
292
|
+
seen.add(id);
|
|
293
|
+
merged.push(Object.freeze({ id, optional: false }));
|
|
294
|
+
}
|
|
295
|
+
return Object.freeze(merged);
|
|
296
|
+
}
|
|
256
297
|
/** Creates a module lifecycle manager. */
|
|
257
298
|
export function createModuleLifecycleManager(registry, loader, options = {}) {
|
|
258
299
|
return new ModuleLifecycleManager(registry, loader, options);
|
|
@@ -86,6 +86,19 @@ export interface ModuleLifecycleOptions {
|
|
|
86
86
|
*/
|
|
87
87
|
readonly contextStorage?: ContextStorage;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Per-call overrides accepted by the ModuleLifecycleManager phase
|
|
91
|
+
* methods (initialize/start/stop/destroy).
|
|
92
|
+
*/
|
|
93
|
+
export interface ModuleLifecyclePhaseOptions {
|
|
94
|
+
/**
|
|
95
|
+
* Overrides the manager's continueOn*Error setting for this one
|
|
96
|
+
* phase run. The runtime passes its own startup/shutdown flags
|
|
97
|
+
* here so a hand-assembled manager cannot disagree with the
|
|
98
|
+
* runtime that drives it.
|
|
99
|
+
*/
|
|
100
|
+
readonly continueOnError?: boolean;
|
|
101
|
+
}
|
|
89
102
|
import { ModuleOperationError } from "../moduleError/moduleError.lifecycle.js";
|
|
90
103
|
/**
|
|
91
104
|
* Error thrown when a module lifecycle operation fails.
|
|
@@ -76,6 +76,56 @@ export class ModuleLoader {
|
|
|
76
76
|
*/
|
|
77
77
|
async loadClosure(requested, options) {
|
|
78
78
|
const closure = this.collectClosure(requested);
|
|
79
|
+
const loaded = [];
|
|
80
|
+
const alreadyLoaded = [];
|
|
81
|
+
const order = [];
|
|
82
|
+
/*
|
|
83
|
+
* Instances may declare dependencies of their own
|
|
84
|
+
* (Module.dependencies) that the definition does not list.
|
|
85
|
+
* Those are only known after instantiation, so the closure is
|
|
86
|
+
* extended and loaded in rounds until nothing new appears.
|
|
87
|
+
*/
|
|
88
|
+
let pending = new Set(closure.keys());
|
|
89
|
+
while (pending.size > 0) {
|
|
90
|
+
const graph = this.createGraph([...closure.values()]);
|
|
91
|
+
const roundOrder = resolveModuleStartupOrder(graph).filter((id) => pending.has(id));
|
|
92
|
+
const discovered = [];
|
|
93
|
+
for (const moduleId of roundOrder) {
|
|
94
|
+
const registration = this.registry.get(moduleId);
|
|
95
|
+
if (!registration)
|
|
96
|
+
throw new ModuleLoadError(moduleId, new Error(`Module "${moduleId}" disappeared from the registry during loading.`));
|
|
97
|
+
order.push(moduleId);
|
|
98
|
+
let instance;
|
|
99
|
+
if (registration.state === "loaded" && registration.instance) {
|
|
100
|
+
instance = registration.instance;
|
|
101
|
+
alreadyLoaded.push(instance);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
instance = await this.instantiate(registration.definition);
|
|
105
|
+
loaded.push(instance);
|
|
106
|
+
}
|
|
107
|
+
for (const dependencyId of instance.dependencies ?? []) {
|
|
108
|
+
if (closure.has(dependencyId))
|
|
109
|
+
continue;
|
|
110
|
+
if (!this.registry.has(dependencyId))
|
|
111
|
+
throw new MissingModuleDependencyError(moduleId, dependencyId);
|
|
112
|
+
discovered.push(dependencyId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const extra = this.collectClosure(discovered);
|
|
116
|
+
pending = new Set();
|
|
117
|
+
for (const [id, definition] of extra) {
|
|
118
|
+
if (closure.has(id))
|
|
119
|
+
continue;
|
|
120
|
+
closure.set(id, definition);
|
|
121
|
+
pending.add(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/*
|
|
125
|
+
* Skipped modules are determined after loading so that an
|
|
126
|
+
* autoLoad:false module pulled in by an instance-declared
|
|
127
|
+
* dependency is reported as loaded, not skipped.
|
|
128
|
+
*/
|
|
79
129
|
const skipped = [];
|
|
80
130
|
if (options.reportSkipped) {
|
|
81
131
|
for (const definition of this.registry.getDefinitions()) {
|
|
@@ -84,28 +134,6 @@ export class ModuleLoader {
|
|
|
84
134
|
}
|
|
85
135
|
}
|
|
86
136
|
}
|
|
87
|
-
if (closure.size === 0)
|
|
88
|
-
return {
|
|
89
|
-
loaded: [],
|
|
90
|
-
alreadyLoaded: [],
|
|
91
|
-
skipped: Object.freeze([...skipped]),
|
|
92
|
-
order: [],
|
|
93
|
-
};
|
|
94
|
-
const graph = this.createGraph([...closure.values()]);
|
|
95
|
-
const order = resolveModuleStartupOrder(graph);
|
|
96
|
-
const loaded = [];
|
|
97
|
-
const alreadyLoaded = [];
|
|
98
|
-
for (const moduleId of order) {
|
|
99
|
-
const registration = this.registry.get(moduleId);
|
|
100
|
-
if (!registration)
|
|
101
|
-
throw new ModuleLoadError(moduleId, new Error(`Module "${moduleId}" disappeared from the registry during loading.`));
|
|
102
|
-
if (registration.state === "loaded" && registration.instance) {
|
|
103
|
-
alreadyLoaded.push(registration.instance);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const instance = await this.instantiate(registration.definition);
|
|
107
|
-
loaded.push(instance);
|
|
108
|
-
}
|
|
109
137
|
return {
|
|
110
138
|
loaded: Object.freeze([...loaded]),
|
|
111
139
|
alreadyLoaded: Object.freeze([...alreadyLoaded]),
|
|
@@ -174,9 +202,17 @@ export class ModuleLoader {
|
|
|
174
202
|
* they cannot enumerate or reach undeclared modules.
|
|
175
203
|
*/
|
|
176
204
|
moduleContexts: (dependencyId) => this.contexts.get(dependencyId),
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
/*
|
|
206
|
+
* Dependencies declared by the instance itself
|
|
207
|
+
* (Module.dependencies) are honoured alongside the
|
|
208
|
+
* definition's, matching the lifecycle ordering.
|
|
209
|
+
*/
|
|
210
|
+
declaredDependencies: [
|
|
211
|
+
...this.registry
|
|
212
|
+
.getDependencies(moduleId)
|
|
213
|
+
.map((dependency) => dependency.id),
|
|
214
|
+
...(module.dependencies ?? []),
|
|
215
|
+
],
|
|
180
216
|
});
|
|
181
217
|
this.contexts.set(moduleId, context);
|
|
182
218
|
this.registry.setState(moduleId, "loaded", { instance: module });
|
|
@@ -30,12 +30,12 @@ export async function executeBootstrapPipeline(options, services, state, signal,
|
|
|
30
30
|
await loadModules(services.moduleLoader, identity, state, publish, log);
|
|
31
31
|
}
|
|
32
32
|
if (options.initializeModules) {
|
|
33
|
-
await runLifecyclePhase("initializing", "initialized", () => services.moduleLifecycle.initialize(), services.moduleLifecycle, options.continueOnInitializeError, (count) => {
|
|
33
|
+
await runLifecyclePhase("initializing", "initialized", () => services.moduleLifecycle.initialize(phaseOptions(options.continueOnInitializeError)), services.moduleLifecycle, options.continueOnInitializeError, (count) => {
|
|
34
34
|
state.counters.initializedModules = count;
|
|
35
35
|
}, (message, opts) => new RuntimeInitializationError(message, { ...identity, ...opts }), state, publish, log);
|
|
36
36
|
}
|
|
37
37
|
if (options.startModules) {
|
|
38
|
-
await runLifecyclePhase("starting", "started", () => services.moduleLifecycle.start(), services.moduleLifecycle, options.continueOnStartError, (count) => {
|
|
38
|
+
await runLifecyclePhase("starting", "started", () => services.moduleLifecycle.start(phaseOptions(options.continueOnStartError)), services.moduleLifecycle, options.continueOnStartError, (count) => {
|
|
39
39
|
state.counters.startedModules = count;
|
|
40
40
|
}, (message, opts) => new RuntimeStartError(message, {
|
|
41
41
|
...identity,
|
|
@@ -44,6 +44,19 @@ export async function executeBootstrapPipeline(options, services, state, signal,
|
|
|
44
44
|
}), state, publish, log);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Per-phase options handed to the ModuleLifecycleManager.
|
|
49
|
+
*
|
|
50
|
+
* The runtime's continueOn*Error flag can only relax the manager: when
|
|
51
|
+
* it is on, the manager must keep going too (otherwise it would roll
|
|
52
|
+
* every module back and throw, and the runtime would report READY
|
|
53
|
+
* with nothing running). When it is off the manager keeps its own
|
|
54
|
+
* setting; a permissive manager then returns the failures and the
|
|
55
|
+
* runtime throws and unwinds.
|
|
56
|
+
*/
|
|
57
|
+
function phaseOptions(continueOnError) {
|
|
58
|
+
return continueOnError ? { continueOnError: true } : {};
|
|
59
|
+
}
|
|
47
60
|
async function loadModules(moduleLoader, identity, state, publish, log) {
|
|
48
61
|
publish("loading");
|
|
49
62
|
log("debug", "Loading runtime modules.");
|
|
@@ -8,6 +8,14 @@ export function resolveRuntimeOptions(options = {}) {
|
|
|
8
8
|
const shutdown = options.shutdown ?? {};
|
|
9
9
|
const signals = options.signals ?? {};
|
|
10
10
|
const diagnostics = options.diagnostics ?? {};
|
|
11
|
+
/*
|
|
12
|
+
* Mode and role are validated here, not only in
|
|
13
|
+
* validateRuntimeOptions(): an unknown mode would otherwise be
|
|
14
|
+
* accepted silently and make the environment report neither
|
|
15
|
+
* production, development, nor test.
|
|
16
|
+
*/
|
|
17
|
+
assertRuntimeMode(options.mode ?? DEFAULT_RUNTIME_OPTIONS.mode);
|
|
18
|
+
assertRuntimeRole(options.role ?? DEFAULT_RUNTIME_OPTIONS.role);
|
|
11
19
|
validateRuntimeName(options.name);
|
|
12
20
|
validateRuntimeTimeout(startup.timeoutMs, "startup");
|
|
13
21
|
validateRuntimeTimeout(shutdown.timeoutMs, "shutdown");
|
|
@@ -17,16 +17,24 @@ export async function executeShutdownPipeline(options, services, state, signal,
|
|
|
17
17
|
runtimeName: services.runtimeName,
|
|
18
18
|
};
|
|
19
19
|
if (options.stopModules) {
|
|
20
|
-
await runShutdownPhase("stopping", "stopped", () => moduleLifecycle.stop(), moduleLifecycle, options.continueOnStopError, (count) => {
|
|
20
|
+
await runShutdownPhase("stopping", "stopped", () => moduleLifecycle.stop(phaseOptions(options.continueOnStopError)), moduleLifecycle, options.continueOnStopError, (count) => {
|
|
21
21
|
state.counters.stoppedModules = count;
|
|
22
22
|
}, RuntimeErrorCode.MODULE_STOP_FAILED, identity, state, publish, log);
|
|
23
23
|
}
|
|
24
24
|
if (options.destroyModules) {
|
|
25
|
-
await runShutdownPhase("destroying", "destroyed", () => moduleLifecycle.destroy(), moduleLifecycle, options.continueOnDestroyError, (count) => {
|
|
25
|
+
await runShutdownPhase("destroying", "destroyed", () => moduleLifecycle.destroy(phaseOptions(options.continueOnDestroyError)), moduleLifecycle, options.continueOnDestroyError, (count) => {
|
|
26
26
|
state.counters.destroyedModules = count;
|
|
27
27
|
}, RuntimeErrorCode.MODULE_DESTROY_FAILED, identity, state, publish, log);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Per-phase options for the ModuleLifecycleManager: the runtime flag
|
|
32
|
+
* relaxes the manager when on and leaves its own setting when off
|
|
33
|
+
* (see the bootstrap pipeline for the rationale).
|
|
34
|
+
*/
|
|
35
|
+
function phaseOptions(continueOnError) {
|
|
36
|
+
return continueOnError ? { continueOnError: true } : {};
|
|
37
|
+
}
|
|
30
38
|
async function runShutdownPhase(phase, donePhase, run, moduleLifecycle, continueOnError, setCount, code, identity, state, publish, log) {
|
|
31
39
|
publish(phase);
|
|
32
40
|
log("debug", `Runtime modules ${phase}.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Application lifecycle management, execution context propagation, and runtime orchestration for Zudojs applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,10 +55,14 @@
|
|
|
55
55
|
"vitest": "^4.1.11"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@zudojs/errors": "1.0.
|
|
59
|
-
"@zudojs/constants": "1.0.
|
|
58
|
+
"@zudojs/errors": "1.0.1",
|
|
59
|
+
"@zudojs/constants": "1.0.1"
|
|
60
60
|
},
|
|
61
61
|
"license": "MIT",
|
|
62
|
+
"author": {
|
|
63
|
+
"name": "Oluwayemi Oyinlola",
|
|
64
|
+
"url": "https://github.com/oyinlola-tech"
|
|
65
|
+
},
|
|
62
66
|
"publishConfig": {
|
|
63
67
|
"access": "public"
|
|
64
68
|
},
|