@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
|
@@ -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, options.signal);
|
|
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, options.signal);
|
|
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);
|
|
@@ -38,7 +38,7 @@ export declare function isModuleDestroyed(moduleId: ModuleId, states: LifecycleS
|
|
|
38
38
|
export declare function invokeLifecycleHook(module: Module, step: ModuleLifecycleStep, context: ModuleContext, contextStorage?: ContextStorage, phase?: ModuleLifecyclePhase): Promise<void>;
|
|
39
39
|
export declare function canModuleEnterPhase(moduleId: ModuleId, hook: ModuleLifecycleStep, states: LifecycleStateMap): boolean;
|
|
40
40
|
export declare function setLifecycleState(moduleId: ModuleId, phase: ModuleLifecyclePhase, states: LifecycleStateMap, error?: unknown): void;
|
|
41
|
-
export declare function executeLifecyclePhase(order: readonly ModuleId[], hook: ModuleLifecycleStep, activePhase: ModuleLifecyclePhase, completedPhase: ModuleLifecyclePhase, continueOnError: boolean, registry: ModuleRegistry, loader: ModuleLoader, states: LifecycleStateMap, contextStorage?: ContextStorage): Promise<{
|
|
41
|
+
export declare function executeLifecyclePhase(order: readonly ModuleId[], hook: ModuleLifecycleStep, activePhase: ModuleLifecyclePhase, completedPhase: ModuleLifecyclePhase, continueOnError: boolean, registry: ModuleRegistry, loader: ModuleLoader, states: LifecycleStateMap, contextStorage?: ContextStorage, signal?: AbortSignal): Promise<{
|
|
42
42
|
readonly completed: readonly ModuleId[];
|
|
43
43
|
readonly failed: readonly ModuleId[];
|
|
44
44
|
readonly skipped: readonly ModuleLifecycleSkip[];
|
|
@@ -146,13 +146,15 @@ const REQUIRED_DEPENDENCY_PHASES = {
|
|
|
146
146
|
initialize: ["initialized", "starting", "started"],
|
|
147
147
|
start: ["started"],
|
|
148
148
|
};
|
|
149
|
-
export async function executeLifecyclePhase(order, hook, activePhase, completedPhase, continueOnError, registry, loader, states, contextStorage) {
|
|
149
|
+
export async function executeLifecyclePhase(order, hook, activePhase, completedPhase, continueOnError, registry, loader, states, contextStorage, signal) {
|
|
150
150
|
const completed = [];
|
|
151
151
|
const failed = [];
|
|
152
152
|
const skipped = [];
|
|
153
153
|
const blocked = new Set();
|
|
154
154
|
const requiredDependencyPhases = REQUIRED_DEPENDENCY_PHASES[hook];
|
|
155
155
|
for (const moduleId of order) {
|
|
156
|
+
if (signal?.aborted)
|
|
157
|
+
break;
|
|
156
158
|
const registration = registry.get(moduleId);
|
|
157
159
|
if (!registration?.instance)
|
|
158
160
|
continue;
|
|
@@ -86,6 +86,26 @@ 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
|
+
/**
|
|
102
|
+
* Abandons the phase once aborted: no further module hook is started.
|
|
103
|
+
* A hook already running completes and its module keeps the phase it
|
|
104
|
+
* reached, so a later stop()/destroy() tears it down. The runtime
|
|
105
|
+
* passes its startup-timeout signal here.
|
|
106
|
+
*/
|
|
107
|
+
readonly signal?: AbortSignal;
|
|
108
|
+
}
|
|
89
109
|
import { ModuleOperationError } from "../moduleError/moduleError.lifecycle.js";
|
|
90
110
|
/**
|
|
91
111
|
* 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 });
|
|
@@ -135,6 +135,7 @@ export declare class DefaultRuntime implements Runtime {
|
|
|
135
135
|
private _startPromise;
|
|
136
136
|
private _stopPromise;
|
|
137
137
|
private _unwound;
|
|
138
|
+
private _unwindPromise;
|
|
138
139
|
constructor(dependencies: RuntimeDependencies, options?: RuntimeOptions);
|
|
139
140
|
get state(): RuntimeState;
|
|
140
141
|
get context(): RuntimeExecutionContext;
|
|
@@ -158,6 +159,7 @@ export declare class DefaultRuntime implements Runtime {
|
|
|
158
159
|
* logged and swallowed; the runtime stays FAILED.
|
|
159
160
|
*/
|
|
160
161
|
private unwind;
|
|
162
|
+
private performUnwind;
|
|
161
163
|
/**
|
|
162
164
|
* Runs an operation inside the runtime's execution context so that
|
|
163
165
|
* module hooks, container factories, and loggers reached from it
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -37,6 +37,7 @@ export class DefaultRuntime {
|
|
|
37
37
|
_startPromise;
|
|
38
38
|
_stopPromise;
|
|
39
39
|
_unwound = false;
|
|
40
|
+
_unwindPromise;
|
|
40
41
|
constructor(dependencies, options = {}) {
|
|
41
42
|
this._options = resolveRuntimeOptions(options);
|
|
42
43
|
this._application = dependencies.application;
|
|
@@ -203,9 +204,15 @@ export class DefaultRuntime {
|
|
|
203
204
|
this.fail(error);
|
|
204
205
|
this._logger.error("Runtime failed to start.", error, this.logContext());
|
|
205
206
|
// The module subsystem has already rolled back when the module
|
|
206
|
-
// lifecycle manager threw
|
|
207
|
-
// running
|
|
208
|
-
|
|
207
|
+
// lifecycle manager threw. A timed-out bootstrap is still
|
|
208
|
+
// running: its teardown is queued behind the lifecycle manager's
|
|
209
|
+
// lock, so it runs once the in-flight hook settles and reaches
|
|
210
|
+
// every module that came up late. start() rejects now; stop()
|
|
211
|
+
// waits for that teardown.
|
|
212
|
+
if (error instanceof RuntimeTimeoutError) {
|
|
213
|
+
void this.unwind();
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
209
216
|
await this.unwind();
|
|
210
217
|
}
|
|
211
218
|
throw error;
|
|
@@ -236,7 +243,11 @@ export class DefaultRuntime {
|
|
|
236
243
|
* Best-effort module unwinding used after a failure. Errors are
|
|
237
244
|
* logged and swallowed; the runtime stays FAILED.
|
|
238
245
|
*/
|
|
239
|
-
|
|
246
|
+
unwind() {
|
|
247
|
+
this._unwindPromise ??= this.performUnwind();
|
|
248
|
+
return this._unwindPromise;
|
|
249
|
+
}
|
|
250
|
+
async performUnwind() {
|
|
240
251
|
if (this._unwound)
|
|
241
252
|
return;
|
|
242
253
|
try {
|
|
@@ -38,7 +38,9 @@ export declare function createBootstrapPipelineState(): BootstrapPipelineState;
|
|
|
38
38
|
* throws; the thrown error is NOT pushed to `errors` here — the
|
|
39
39
|
* caller records it exactly once.
|
|
40
40
|
* - Once `signal` is aborted (timeout) the pipeline stops publishing
|
|
41
|
-
* phase changes so an abandoned run cannot mutate the owner
|
|
41
|
+
* phase changes so an abandoned run cannot mutate the owner, starts
|
|
42
|
+
* no further phase, and the lifecycle manager starts no further
|
|
43
|
+
* module hook.
|
|
42
44
|
*/
|
|
43
45
|
export declare function executeBootstrapPipeline(options: ResolvedBootstrapOptions, services: BootstrapPipelineServices, state: BootstrapPipelineState, signal: AbortSignal, setPhase: (phase: RuntimeBootstrapPhase) => void, log: BootstrapLogFn): Promise<void>;
|
|
44
46
|
export declare function createBootstrapResult(success: boolean, phase: RuntimeBootstrapPhase, counters: BootstrapCounters, errors: readonly RuntimeBootstrapErrorInfo[], startedAt: Date, completedAt: Date): RuntimeBootstrapResult;
|
|
@@ -15,7 +15,9 @@ export function createBootstrapPipelineState() {
|
|
|
15
15
|
* throws; the thrown error is NOT pushed to `errors` here — the
|
|
16
16
|
* caller records it exactly once.
|
|
17
17
|
* - Once `signal` is aborted (timeout) the pipeline stops publishing
|
|
18
|
-
* phase changes so an abandoned run cannot mutate the owner
|
|
18
|
+
* phase changes so an abandoned run cannot mutate the owner, starts
|
|
19
|
+
* no further phase, and the lifecycle manager starts no further
|
|
20
|
+
* module hook.
|
|
19
21
|
*/
|
|
20
22
|
export async function executeBootstrapPipeline(options, services, state, signal, setPhase, log) {
|
|
21
23
|
const publish = (phase) => {
|
|
@@ -29,13 +31,22 @@ export async function executeBootstrapPipeline(options, services, state, signal,
|
|
|
29
31
|
if (options.loadModules) {
|
|
30
32
|
await loadModules(services.moduleLoader, identity, state, publish, log);
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// Every phase and every module hook checks the signal: a timed-out
|
|
35
|
+
// bootstrap must not bring modules up after the runtime has already
|
|
36
|
+
// reported a failed start.
|
|
37
|
+
if (options.initializeModules && !signal.aborted) {
|
|
38
|
+
await runLifecyclePhase("initializing", "initialized", () => services.moduleLifecycle.initialize({
|
|
39
|
+
...phaseOptions(options.continueOnInitializeError),
|
|
40
|
+
signal,
|
|
41
|
+
}), services.moduleLifecycle, options.continueOnInitializeError, (count) => {
|
|
34
42
|
state.counters.initializedModules = count;
|
|
35
43
|
}, (message, opts) => new RuntimeInitializationError(message, { ...identity, ...opts }), state, publish, log);
|
|
36
44
|
}
|
|
37
|
-
if (options.startModules) {
|
|
38
|
-
await runLifecyclePhase("starting", "started", () => services.moduleLifecycle.start(
|
|
45
|
+
if (options.startModules && !signal.aborted) {
|
|
46
|
+
await runLifecyclePhase("starting", "started", () => services.moduleLifecycle.start({
|
|
47
|
+
...phaseOptions(options.continueOnStartError),
|
|
48
|
+
signal,
|
|
49
|
+
}), services.moduleLifecycle, options.continueOnStartError, (count) => {
|
|
39
50
|
state.counters.startedModules = count;
|
|
40
51
|
}, (message, opts) => new RuntimeStartError(message, {
|
|
41
52
|
...identity,
|
|
@@ -44,6 +55,19 @@ export async function executeBootstrapPipeline(options, services, state, signal,
|
|
|
44
55
|
}), state, publish, log);
|
|
45
56
|
}
|
|
46
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Per-phase options handed to the ModuleLifecycleManager.
|
|
60
|
+
*
|
|
61
|
+
* The runtime's continueOn*Error flag can only relax the manager: when
|
|
62
|
+
* it is on, the manager must keep going too (otherwise it would roll
|
|
63
|
+
* every module back and throw, and the runtime would report READY
|
|
64
|
+
* with nothing running). When it is off the manager keeps its own
|
|
65
|
+
* setting; a permissive manager then returns the failures and the
|
|
66
|
+
* runtime throws and unwinds.
|
|
67
|
+
*/
|
|
68
|
+
function phaseOptions(continueOnError) {
|
|
69
|
+
return continueOnError ? { continueOnError: true } : {};
|
|
70
|
+
}
|
|
47
71
|
async function loadModules(moduleLoader, identity, state, publish, log) {
|
|
48
72
|
publish("loading");
|
|
49
73
|
log("debug", "Loading runtime modules.");
|
|
@@ -26,8 +26,10 @@ export const DEFAULT_RUNTIME_OPTIONS = Object.freeze({
|
|
|
26
26
|
handleSighup: false,
|
|
27
27
|
handleUncaughtException: true,
|
|
28
28
|
handleUnhandledRejection: true,
|
|
29
|
-
forceExitOnSecondSignal:
|
|
29
|
+
forceExitOnSecondSignal: true,
|
|
30
30
|
forceExitCode: 1,
|
|
31
|
+
exitOnFatalError: true,
|
|
32
|
+
fatalExitTimeout: 10_000,
|
|
31
33
|
},
|
|
32
34
|
diagnostics: {
|
|
33
35
|
startupLogging: true,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RuntimeEnvironmentVariables } from "../runtimeEnvironment/runtimeEnvironment.type.js";
|
|
2
|
+
import type { RuntimeMode } from "./runtimeOptions.type.js";
|
|
3
|
+
/**
|
|
4
|
+
* Derives the runtime mode used when `RuntimeOptions.mode` is not set.
|
|
5
|
+
*
|
|
6
|
+
* `NODE_ENV` is read through `resolveEnvironment()` from `@zudojs/constants`,
|
|
7
|
+
* so every layer maps the same value to the same environment (`prod` and
|
|
8
|
+
* `Production` are production, unset is development). `staging` has no
|
|
9
|
+
* runtime mode of its own and runs as `production`.
|
|
10
|
+
*
|
|
11
|
+
* @param variables - Environment variables to read instead of `process.env`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveDefaultRuntimeMode(variables?: RuntimeEnvironmentVariables): RuntimeMode;
|
|
14
|
+
//# sourceMappingURL=runtimeOptions.mode.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { resolveEnvironment } from "@zudojs/constants";
|
|
2
|
+
/**
|
|
3
|
+
* Derives the runtime mode used when `RuntimeOptions.mode` is not set.
|
|
4
|
+
*
|
|
5
|
+
* `NODE_ENV` is read through `resolveEnvironment()` from `@zudojs/constants`,
|
|
6
|
+
* so every layer maps the same value to the same environment (`prod` and
|
|
7
|
+
* `Production` are production, unset is development). `staging` has no
|
|
8
|
+
* runtime mode of its own and runs as `production`.
|
|
9
|
+
*
|
|
10
|
+
* @param variables - Environment variables to read instead of `process.env`.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveDefaultRuntimeMode(variables) {
|
|
13
|
+
const environment = resolveEnvironment(variables === undefined ? undefined : { ...variables });
|
|
14
|
+
return environment === "staging" ? "production" : environment;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=runtimeOptions.mode.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_RUNTIME_OPTIONS } from "./runtimeOptions.defaults.js";
|
|
2
|
+
import { resolveDefaultRuntimeMode } from "./runtimeOptions.mode.js";
|
|
2
3
|
import { validateRuntimeName, validateRuntimeTimeout, assertRuntimeMode, assertRuntimeRole, } from "./runtimeOptions.validation.js";
|
|
3
4
|
/**
|
|
4
5
|
* Resolves partial runtime options into a complete immutable runtime configuration.
|
|
@@ -8,12 +9,22 @@ export function resolveRuntimeOptions(options = {}) {
|
|
|
8
9
|
const shutdown = options.shutdown ?? {};
|
|
9
10
|
const signals = options.signals ?? {};
|
|
10
11
|
const diagnostics = options.diagnostics ?? {};
|
|
12
|
+
/*
|
|
13
|
+
* Mode and role are validated here, not only in
|
|
14
|
+
* validateRuntimeOptions(): an unknown mode would otherwise be
|
|
15
|
+
* accepted silently and make the environment report neither
|
|
16
|
+
* production, development, nor test.
|
|
17
|
+
*/
|
|
18
|
+
const mode = options.mode ?? resolveDefaultRuntimeMode(options.environment?.variables);
|
|
19
|
+
assertRuntimeMode(mode);
|
|
20
|
+
assertRuntimeRole(options.role ?? DEFAULT_RUNTIME_OPTIONS.role);
|
|
11
21
|
validateRuntimeName(options.name);
|
|
12
22
|
validateRuntimeTimeout(startup.timeoutMs, "startup");
|
|
13
23
|
validateRuntimeTimeout(shutdown.timeoutMs, "shutdown");
|
|
24
|
+
validateRuntimeTimeout(signals.fatalExitTimeout, "fatal exit");
|
|
14
25
|
const resolved = {
|
|
15
26
|
name: options.name ?? DEFAULT_RUNTIME_OPTIONS.name,
|
|
16
|
-
mode
|
|
27
|
+
mode,
|
|
17
28
|
role: options.role ?? DEFAULT_RUNTIME_OPTIONS.role,
|
|
18
29
|
startup: {
|
|
19
30
|
autoLoadModules: startup.autoLoadModules ??
|
|
@@ -50,6 +61,10 @@ export function resolveRuntimeOptions(options = {}) {
|
|
|
50
61
|
forceExitOnSecondSignal: signals.forceExitOnSecondSignal ??
|
|
51
62
|
DEFAULT_RUNTIME_OPTIONS.signals.forceExitOnSecondSignal,
|
|
52
63
|
forceExitCode: signals.forceExitCode ?? DEFAULT_RUNTIME_OPTIONS.signals.forceExitCode,
|
|
64
|
+
exitOnFatalError: signals.exitOnFatalError ??
|
|
65
|
+
DEFAULT_RUNTIME_OPTIONS.signals.exitOnFatalError,
|
|
66
|
+
fatalExitTimeout: signals.fatalExitTimeout ??
|
|
67
|
+
DEFAULT_RUNTIME_OPTIONS.signals.fatalExitTimeout,
|
|
53
68
|
},
|
|
54
69
|
diagnostics: {
|
|
55
70
|
startupLogging: diagnostics.startupLogging ??
|
|
@@ -79,5 +94,6 @@ export function validateRuntimeOptions(options) {
|
|
|
79
94
|
validateRuntimeName(options.name);
|
|
80
95
|
validateRuntimeTimeout(options.startup?.timeoutMs, "startup");
|
|
81
96
|
validateRuntimeTimeout(options.shutdown?.timeoutMs, "shutdown");
|
|
97
|
+
validateRuntimeTimeout(options.signals?.fatalExitTimeout, "fatal exit");
|
|
82
98
|
}
|
|
83
99
|
//# sourceMappingURL=runtimeOptions.resolver.js.map
|
|
@@ -63,12 +63,27 @@ export interface RuntimeSignalOptions {
|
|
|
63
63
|
/**
|
|
64
64
|
* When a second termination signal arrives while a graceful stop is
|
|
65
65
|
* already in progress, exit the process immediately with
|
|
66
|
-
* `forceExitCode`.
|
|
67
|
-
*
|
|
66
|
+
* `forceExitCode`. On by default, matching `@zudojs/runtime`: an
|
|
67
|
+
* operator pressing Ctrl-C again on a stuck shutdown is asking for
|
|
68
|
+
* exactly that. Set to `false` to log and ignore the second signal.
|
|
68
69
|
*/
|
|
69
70
|
readonly forceExitOnSecondSignal?: boolean;
|
|
70
71
|
/** Exit code used by `forceExitOnSecondSignal`. Defaults to 1. */
|
|
71
72
|
readonly forceExitCode?: number;
|
|
73
|
+
/**
|
|
74
|
+
* After an uncaught exception or unhandled rejection has stopped the
|
|
75
|
+
* runtime, exit the process with code 1. On by default: the
|
|
76
|
+
* installed handler suppresses Node's own crash, so without this the
|
|
77
|
+
* process exited 0 and supervisors never restarted or alerted. Set to
|
|
78
|
+
* `false` to stop the runtime and leave the process running.
|
|
79
|
+
*/
|
|
80
|
+
readonly exitOnFatalError?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* How long that fatal-error shutdown may take before the process
|
|
83
|
+
* exits anyway, in milliseconds. `0` waits for the shutdown however
|
|
84
|
+
* long it takes. Defaults to 10000.
|
|
85
|
+
*/
|
|
86
|
+
readonly fatalExitTimeout?: number;
|
|
72
87
|
}
|
|
73
88
|
/**
|
|
74
89
|
* Options controlling runtime diagnostics.
|
|
@@ -97,6 +112,7 @@ export interface RuntimeEnvironmentOverrides {
|
|
|
97
112
|
*/
|
|
98
113
|
export interface RuntimeOptions {
|
|
99
114
|
readonly name?: string;
|
|
115
|
+
/** Omitted: derived from NODE_ENV by `resolveEnvironment()` (@zudojs/constants); staging runs as production. */
|
|
100
116
|
readonly mode?: RuntimeMode;
|
|
101
117
|
readonly role?: RuntimeRole;
|
|
102
118
|
readonly startup?: RuntimeStartupOptions;
|
|
@@ -22,5 +22,5 @@ export declare function validateRuntimeName(name: string | undefined): void;
|
|
|
22
22
|
/**
|
|
23
23
|
* Validates a runtime timeout.
|
|
24
24
|
*/
|
|
25
|
-
export declare function validateRuntimeTimeout(timeout: number | undefined, field: "startup" | "shutdown"): void;
|
|
25
|
+
export declare function validateRuntimeTimeout(timeout: number | undefined, field: "startup" | "shutdown" | "fatal exit"): void;
|
|
26
26
|
//# sourceMappingURL=runtimeOptions.validation.d.ts.map
|