@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 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(event, listener)` / `manager.off(...)` receive `initializing`, `loaded`, `validated`, `ready`, `failed`, `reloading`, and `reloaded` events.
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)` adds request-scoped values, 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.
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
- readonly configuration: Configuration;
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
- this.configuration = options.configuration;
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
- configuration: configuration.getConfiguration(),
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;
@@ -26,7 +26,6 @@ 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;
@@ -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
- const cache = this.scopedInstances.get(scopeKey);
85
- if (cache?.has(token)) {
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
- let cache = this.scopedInstances.get(scopeKey);
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
- result[key] = sanitizeLogValue(item, seen, depth + 1);
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.registry
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.getAll().map((r) => ({
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.registry.getDependencies(r.definition.id),
246
+ dependencies: this.resolveDependencies(r),
227
247
  version: r.definition.version,
228
248
  }));
229
249
  return createModuleDependencyGraph(nodes);
230
250
  }
231
251
  getStartupOrder() {
232
- const order = resolveModuleStartupOrder(this.createGraph());
233
- return Object.freeze(order.filter((id) => this.registry.get(id)?.state === "loaded"));
252
+ return resolveModuleStartupOrder(this.createGraph());
234
253
  }
235
254
  getShutdownOrder() {
236
- const order = resolveModuleShutdownOrder(this.createGraph());
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
- declaredDependencies: this.registry
178
- .getDependencies(moduleId)
179
- .map((dependency) => dependency.id),
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.0.0",
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.0",
59
- "@zudojs/constants": "1.0.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
  },