@digital-alchemy/core 26.2.17 → 26.5.28

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.
Files changed (85) hide show
  1. package/CLAUDE.md +302 -0
  2. package/README.md +19 -3
  3. package/dist/helpers/async.d.mts +37 -0
  4. package/dist/helpers/async.mjs +50 -15
  5. package/dist/helpers/async.mjs.map +1 -1
  6. package/dist/helpers/config-environment-loader.d.mts +39 -0
  7. package/dist/helpers/config-environment-loader.mjs +51 -11
  8. package/dist/helpers/config-environment-loader.mjs.map +1 -1
  9. package/dist/helpers/config-file-loader.d.mts +65 -0
  10. package/dist/helpers/config-file-loader.mjs +80 -4
  11. package/dist/helpers/config-file-loader.mjs.map +1 -1
  12. package/dist/helpers/config.d.mts +202 -5
  13. package/dist/helpers/config.mjs +60 -0
  14. package/dist/helpers/config.mjs.map +1 -1
  15. package/dist/helpers/context.d.mts +12 -1
  16. package/dist/helpers/cron.d.mts +154 -7
  17. package/dist/helpers/cron.mjs +47 -4
  18. package/dist/helpers/cron.mjs.map +1 -1
  19. package/dist/helpers/errors.d.mts +45 -0
  20. package/dist/helpers/errors.mjs +45 -0
  21. package/dist/helpers/errors.mjs.map +1 -1
  22. package/dist/helpers/events.d.mts +23 -0
  23. package/dist/helpers/events.mjs +23 -0
  24. package/dist/helpers/events.mjs.map +1 -1
  25. package/dist/helpers/extend.d.mts +50 -0
  26. package/dist/helpers/extend.mjs +63 -0
  27. package/dist/helpers/extend.mjs.map +1 -1
  28. package/dist/helpers/index.d.mts +9 -0
  29. package/dist/helpers/index.mjs +9 -0
  30. package/dist/helpers/index.mjs.map +1 -1
  31. package/dist/helpers/lifecycle.d.mts +102 -16
  32. package/dist/helpers/lifecycle.mjs +19 -1
  33. package/dist/helpers/lifecycle.mjs.map +1 -1
  34. package/dist/helpers/logger.d.mts +182 -17
  35. package/dist/helpers/logger.mjs +45 -1
  36. package/dist/helpers/logger.mjs.map +1 -1
  37. package/dist/helpers/module.d.mts +110 -0
  38. package/dist/helpers/module.mjs +55 -6
  39. package/dist/helpers/module.mjs.map +1 -1
  40. package/dist/helpers/service-runner.d.mts +27 -1
  41. package/dist/helpers/service-runner.mjs +27 -1
  42. package/dist/helpers/service-runner.mjs.map +1 -1
  43. package/dist/helpers/utilities.d.mts +123 -3
  44. package/dist/helpers/utilities.mjs +110 -3
  45. package/dist/helpers/utilities.mjs.map +1 -1
  46. package/dist/helpers/wiring.d.mts +385 -0
  47. package/dist/helpers/wiring.mjs +120 -0
  48. package/dist/helpers/wiring.mjs.map +1 -1
  49. package/dist/services/als.service.d.mts +10 -0
  50. package/dist/services/als.service.mjs +49 -0
  51. package/dist/services/als.service.mjs.map +1 -1
  52. package/dist/services/configuration.service.d.mts +22 -0
  53. package/dist/services/configuration.service.mjs +140 -12
  54. package/dist/services/configuration.service.mjs.map +1 -1
  55. package/dist/services/index.d.mts +8 -0
  56. package/dist/services/index.mjs +8 -0
  57. package/dist/services/index.mjs.map +1 -1
  58. package/dist/services/internal.service.d.mts +98 -19
  59. package/dist/services/internal.service.mjs +91 -9
  60. package/dist/services/internal.service.mjs.map +1 -1
  61. package/dist/services/is.service.d.mts +64 -4
  62. package/dist/services/is.service.mjs +67 -4
  63. package/dist/services/is.service.mjs.map +1 -1
  64. package/dist/services/lifecycle.service.d.mts +26 -0
  65. package/dist/services/lifecycle.service.mjs +67 -9
  66. package/dist/services/lifecycle.service.mjs.map +1 -1
  67. package/dist/services/logger.service.d.mts +27 -0
  68. package/dist/services/logger.service.mjs +133 -9
  69. package/dist/services/logger.service.mjs.map +1 -1
  70. package/dist/services/scheduler.service.d.mts +19 -0
  71. package/dist/services/scheduler.service.mjs +87 -4
  72. package/dist/services/scheduler.service.mjs.map +1 -1
  73. package/dist/services/wiring.service.d.mts +28 -0
  74. package/dist/services/wiring.service.mjs +173 -19
  75. package/dist/services/wiring.service.mjs.map +1 -1
  76. package/dist/testing/index.d.mts +4 -0
  77. package/dist/testing/index.mjs +4 -0
  78. package/dist/testing/index.mjs.map +1 -1
  79. package/dist/testing/mock-logger.d.mts +8 -0
  80. package/dist/testing/mock-logger.mjs +9 -0
  81. package/dist/testing/mock-logger.mjs.map +1 -1
  82. package/dist/testing/test-module.d.mts +112 -27
  83. package/dist/testing/test-module.mjs +64 -2
  84. package/dist/testing/test-module.mjs.map +1 -1
  85. package/package.json +36 -34
@@ -1,11 +1,31 @@
1
1
  import { CronJob } from "cron";
2
2
  import dayjs from "dayjs";
3
3
  import { BootstrapException, sleep } from "../index.mjs";
4
+ /**
5
+ * Builder-style scheduler factory injected into every service via `TServiceParams`.
6
+ *
7
+ * @remarks
8
+ * Returns a function that accepts a `TContext` and produces the per-caller scheduler
9
+ * API (`cron`, `interval`, `sliding`, `setTimeout`, `setInterval`, `sleep`).
10
+ * This two-level shape is deliberate: the outer factory sets up lifecycle hooks
11
+ * shared across all callers; the inner function binds the per-caller context so
12
+ * every scheduled callback is associated with the service that created it.
13
+ *
14
+ * All schedules are registered against a shared `stop` set so a single
15
+ * `onPreShutdown` hook can cleanly cancel every outstanding timer in one pass.
16
+ * Schedules only start firing after the `onReady` lifecycle event; creating a
17
+ * scheduler before `onReady` is safe — the job is registered but does not run
18
+ * until the application is ready.
19
+ *
20
+ * Remove callbacks returned from each method are dual-arity:
21
+ * call them directly (`remove()`) or destructure `{ remove }` — both work.
22
+ */
4
23
  export function Scheduler({ logger, lifecycle, internal }) {
5
24
  const { is } = internal.utils;
6
25
  const stop = new Set();
7
26
  // #MARK: lifecycle events
8
27
  lifecycle.onPreShutdown(function onPreShutdown() {
28
+ // skip teardown work if nothing was ever scheduled for this instance
9
29
  if (is.empty(stop)) {
10
30
  return;
11
31
  }
@@ -17,6 +37,15 @@ export function Scheduler({ logger, lifecycle, internal }) {
17
37
  });
18
38
  return (context) => {
19
39
  // #MARK: cron
40
+ /**
41
+ * Run `exec` on one or more cron schedules.
42
+ *
43
+ * @remarks
44
+ * Accepts a single schedule string or an array; each schedule creates an
45
+ * independent `CronJob`. Jobs start on `onReady` and are registered in the
46
+ * shared `stop` set so they are cancelled during `onPreShutdown`.
47
+ * Returns a combined remove callback that cancels all jobs created by this call.
48
+ */
20
49
  function cron({ exec, schedule: scheduleList }) {
21
50
  const stopFunctions = [];
22
51
  [scheduleList].flat().forEach(cronSchedule => {
@@ -34,9 +63,18 @@ export function Scheduler({ logger, lifecycle, internal }) {
34
63
  stopFunctions.push(stopFunction);
35
64
  return stopFunction;
36
65
  });
66
+ // wrap all individual stop functions so a single call cancels every schedule
37
67
  return internal.removeFn(() => stopFunctions.forEach(stop => stop()));
38
68
  }
39
69
  // #MARK: interval
70
+ /**
71
+ * Run `exec` repeatedly at a fixed `interval` millisecond cadence.
72
+ *
73
+ * @remarks
74
+ * The underlying `setInterval` does not start until `onReady`. If the
75
+ * application is torn down before `onReady` fires the `stopped` guard
76
+ * prevents the timer from being created at all.
77
+ */
40
78
  function interval({ exec, interval }) {
41
79
  let runningInterval;
42
80
  lifecycle.onReady(() => {
@@ -52,6 +90,28 @@ export function Scheduler({ logger, lifecycle, internal }) {
52
90
  return stopFunction;
53
91
  }
54
92
  // #MARK: sliding
93
+ /**
94
+ * Schedule an execution at a time determined dynamically by a `next` callback.
95
+ *
96
+ * @remarks
97
+ * Unlike cron (fixed periods) or interval (fixed gaps), sliding lets the
98
+ * caller compute the *exact* next run time. `reset` is a cron expression
99
+ * that controls how often `next` is re-evaluated; `next` returns the target
100
+ * `Dayjs` moment for the actual `exec` call.
101
+ *
102
+ * Decision points:
103
+ * - If `next()` returns falsy the window is skipped — caller can signal
104
+ * "nothing to do right now" without throwing.
105
+ * - If the computed time is already in the past at evaluation time, the
106
+ * execution is skipped. This most commonly happens on first boot when the
107
+ * slot has already passed for today; treating it as a no-op avoids an
108
+ * immediate double-fire.
109
+ * - If `waitForNext` is called while a previous timeout is still pending,
110
+ * it cancels the old one and schedules fresh — ensures the schedule stays
111
+ * coherent if the reset cron fires more aggressively than expected.
112
+ *
113
+ * @throws {BootstrapException} `BAD_NEXT` if `next` or `exec` is not a function.
114
+ */
55
115
  function sliding({ exec, reset, next }) {
56
116
  if (!is.function(next)) {
57
117
  throw new BootstrapException(context, "BAD_NEXT", "Did not provide next function to schedule.sliding");
@@ -61,20 +121,23 @@ export function Scheduler({ logger, lifecycle, internal }) {
61
121
  }
62
122
  let timeout;
63
123
  const waitForNext = () => {
124
+ // a pending timeout means the reset cron fired before the scheduled exec ran;
125
+ // cancel and reschedule so the next time is always freshly computed
64
126
  if (timeout) {
65
127
  logger.warn({ context, name: sliding }, `sliding schedule retrieving next execution time before previous ran`);
66
128
  clearTimeout(timeout);
67
129
  }
68
130
  let nextTime = next();
131
+ // next() returning falsy is the caller's way of saying "skip this window"
69
132
  if (!nextTime) {
70
- // nothing to do?
71
- // will try again next schedule
133
+ logger.trace({ context, name: sliding }, "next returned falsy, skipping window");
72
134
  return;
73
135
  }
74
136
  nextTime = dayjs(nextTime);
137
+ // if the target time is already past at evaluation, skip to avoid an immediate
138
+ // double-fire; most common on first boot when the slot passed earlier today
75
139
  if (dayjs().isAfter(nextTime)) {
76
- // probably a result of boot
77
- // ignore
140
+ logger.trace({ context, name: sliding }, "next time is in the past, skipping");
78
141
  return;
79
142
  }
80
143
  if (nextTime) {
@@ -98,10 +161,20 @@ export function Scheduler({ logger, lifecycle, internal }) {
98
161
  }
99
162
  });
100
163
  }
164
+ /**
165
+ * Fire `callback` once after `target` offset elapses.
166
+ *
167
+ * @remarks
168
+ * Wraps the native `setTimeout` with lifecycle awareness: the timer is not
169
+ * armed until `onReady`, and is automatically cancelled during shutdown via
170
+ * the shared `stop` set. If `remove()` is called before `onReady` the
171
+ * `stopped` flag prevents the timer from ever being created.
172
+ */
101
173
  function SetTimeout(callback, target) {
102
174
  let timer;
103
175
  let stopped = false;
104
176
  lifecycle.onReady(() => {
177
+ // guard against the case where remove() was called before onReady fired
105
178
  if (stopped) {
106
179
  return;
107
180
  }
@@ -120,10 +193,20 @@ export function Scheduler({ logger, lifecycle, internal }) {
120
193
  stop.add(remove);
121
194
  return remove;
122
195
  }
196
+ /**
197
+ * Fire `callback` repeatedly at every `target` offset interval.
198
+ *
199
+ * @remarks
200
+ * Lifecycle-aware wrapper around `setInterval`. Timer starts on `onReady`
201
+ * and is cancelled during shutdown via the shared `stop` set. If `remove()`
202
+ * is called before `onReady`, the `stopped` guard prevents the interval
203
+ * from ever starting.
204
+ */
123
205
  function SetInterval(callback, target) {
124
206
  let timer;
125
207
  let stopped = false;
126
208
  lifecycle.onReady(() => {
209
+ // guard against the case where remove() was called before onReady fired
127
210
  if (stopped) {
128
211
  return;
129
212
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.service.mjs","sourceRoot":"","sources":["../../src/services/scheduler.service.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scheduler.service.mjs","sourceRoot":"","sources":["../../src/services/scheduler.service.mts"],"names":[],"mappings":"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"}
@@ -8,6 +8,17 @@ export interface DeclaredEnvironments {
8
8
  test: true;
9
9
  local: true;
10
10
  }
11
+ /**
12
+ * Construct a fresh `LIB_BOILERPLATE` library definition.
13
+ *
14
+ * @remarks
15
+ * Must remain in this file. Moving it to another module causes code-init race
16
+ * conditions because the boilerplate services depend on types that are only
17
+ * resolved after the module graph is fully settled. The library is recreated
18
+ * on every `bootstrap()` call so tests get a clean instance.
19
+ *
20
+ * @internal
21
+ */
11
22
  declare function createBoilerplate(): import("../index.mts").LibraryDefinition<{
12
23
  /**
13
24
  * [AsyncLocalStorage](https://nodejs.org/api/async_context.html) hooks
@@ -105,5 +116,22 @@ declare function createBoilerplate(): import("../index.mts").LibraryDefinition<{
105
116
  NODE_ENV: StringConfig<keyof DeclaredEnvironments>;
106
117
  }>;
107
118
  export declare let LIB_BOILERPLATE: ReturnType<typeof createBoilerplate>;
119
+ /**
120
+ * Define an application and return a handle that can be bootstrapped.
121
+ *
122
+ * @remarks
123
+ * Does not start any services. Call `.bootstrap(options)` on the returned
124
+ * handle to wire services and run the lifecycle. Only one bootstrap per
125
+ * handle is allowed; a second call throws `DOUBLE_BOOT`.
126
+ *
127
+ * The returned handle also exposes `.teardown()` for orderly shutdown —
128
+ * this is the same path taken by the SIGINT/SIGTERM handlers.
129
+ *
130
+ * `priorityInit` controls which services are wired first within this
131
+ * application; all others are wired in declaration order after them.
132
+ *
133
+ * @throws {BootstrapException} `MISSING_PRIORITY_SERVICE` if a service listed
134
+ * in `priorityInit` is not present in `services`.
135
+ */
108
136
  export declare function CreateApplication<S extends ServiceMap, C extends OptionalModuleConfiguration>({ name, services, libraries, configuration, priorityInit, ...extra }: ApplicationConfigurationOptions<S, C>): ApplicationDefinition<S, C>;
109
137
  export {};
@@ -3,6 +3,8 @@ import { ACTIVE_SLEEPS, BootstrapException, buildSortOrder, COERCE_CONTEXT, Crea
3
3
  import { ALS } from "./als.service.mjs";
4
4
  import { Configuration, INITIALIZE, INJECTED_DEFINITIONS, LOAD_PROJECT, } from "./configuration.service.mjs";
5
5
  import { InternalDefinition } from "./internal.service.mjs";
6
+ // direct import of `is` from its source file to avoid a circular dependency:
7
+ // going through ../index.mts would force wiring.mts to resolve before is.service.mts
6
8
  import { is } from "./is.service.mjs";
7
9
  import { CreateLifecycle } from "./lifecycle.service.mjs";
8
10
  import { Logger } from "./logger.service.mjs";
@@ -11,6 +13,17 @@ const EXIT_ERROR = 1;
11
13
  const SIGINT = 130;
12
14
  const SIGTERM = 143;
13
15
  // #MARK: CreateBoilerplate
16
+ /**
17
+ * Construct a fresh `LIB_BOILERPLATE` library definition.
18
+ *
19
+ * @remarks
20
+ * Must remain in this file. Moving it to another module causes code-init race
21
+ * conditions because the boilerplate services depend on types that are only
22
+ * resolved after the module graph is fully settled. The library is recreated
23
+ * on every `bootstrap()` call so tests get a clean instance.
24
+ *
25
+ * @internal
26
+ */
14
27
  function createBoilerplate() {
15
28
  // ! DO NOT MOVE TO ANOTHER FILE !
16
29
  // While it SEEMS LIKE this can be safely moved, it causes code init race conditions.
@@ -128,11 +141,19 @@ function createBoilerplate() {
128
141
  },
129
142
  });
130
143
  }
131
- // (re)defined at bootstrap
132
- // unclear if this variable even serves a purpose beyond types
144
+ // (re)defined at bootstrap; exported so downstream code can reference its type
133
145
  export let LIB_BOILERPLATE;
134
146
  const RUNNING_APPLICATIONS = new Map();
135
147
  // #MARK: QuickShutdown
148
+ /**
149
+ * Trigger teardown on every currently-running application.
150
+ *
151
+ * @remarks
152
+ * Called by the SIGINT/SIGTERM process event handlers so all applications
153
+ * go through their full shutdown lifecycle before the process exits.
154
+ *
155
+ * @internal
156
+ */
136
157
  async function quickShutdown(reason) {
137
158
  await each([...RUNNING_APPLICATIONS.values()], async (application) => {
138
159
  application.logger.warn({ reason }, `starting shutdown`);
@@ -140,6 +161,9 @@ async function quickShutdown(reason) {
140
161
  });
141
162
  }
142
163
  // #MARK: processEvents
164
+ // SIGTERM is sent by orchestrators (e.g. Docker, Kubernetes) for graceful shutdown;
165
+ // SIGINT is sent by the terminal (Ctrl-C); both should drain the lifecycle cleanly
166
+ // before exiting rather than killing the process immediately
143
167
  const processEvents = new Map([
144
168
  [
145
169
  "SIGTERM",
@@ -161,8 +185,27 @@ const processEvents = new Map([
161
185
  const DECIMALS = 2;
162
186
  const BOILERPLATE = (internal) => internal.boot.loadedModules.get("boilerplate");
163
187
  // #MARK: CreateApplication
188
+ /**
189
+ * Define an application and return a handle that can be bootstrapped.
190
+ *
191
+ * @remarks
192
+ * Does not start any services. Call `.bootstrap(options)` on the returned
193
+ * handle to wire services and run the lifecycle. Only one bootstrap per
194
+ * handle is allowed; a second call throws `DOUBLE_BOOT`.
195
+ *
196
+ * The returned handle also exposes `.teardown()` for orderly shutdown —
197
+ * this is the same path taken by the SIGINT/SIGTERM handlers.
198
+ *
199
+ * `priorityInit` controls which services are wired first within this
200
+ * application; all others are wired in declaration order after them.
201
+ *
202
+ * @throws {BootstrapException} `MISSING_PRIORITY_SERVICE` if a service listed
203
+ * in `priorityInit` is not present in `services`.
204
+ */
164
205
  export function CreateApplication({ name, services = {}, libraries = [], configuration = {}, priorityInit = [], ...extra }) {
165
206
  let internal;
207
+ // validate priority list up front so misconfiguration fails loudly at definition
208
+ // time rather than silently during bootstrap when the service is just missing
166
209
  if (!is.empty(priorityInit)) {
167
210
  priorityInit.forEach(name => {
168
211
  if (!is.function(services[name])) {
@@ -180,6 +223,8 @@ export function CreateApplication({ name, services = {}, libraries = [], configu
180
223
  serviceApis[service] = await wireService(name, service, services[service], internal.boot.lifecycle.events, internal);
181
224
  });
182
225
  const append = internal.boot.options?.appendService;
226
+ // appendService allows tests (and advanced users) to inject extra services
227
+ // without modifying the application definition
183
228
  if (!is.empty(append)) {
184
229
  await eachSeries(Object.keys(append), async (service) => {
185
230
  await wireService(name, service, append[service], internal.boot.lifecycle.events, internal);
@@ -189,6 +234,8 @@ export function CreateApplication({ name, services = {}, libraries = [], configu
189
234
  },
190
235
  booted: false,
191
236
  bootstrap: async (options) => {
237
+ // guard against accidentally bootstrapping the same application twice;
238
+ // this is always a programming error — each app should have a single entry point
192
239
  if (application.booted) {
193
240
  throw new BootstrapException(WIRING_CONTEXT, "DOUBLE_BOOT", "Application is already booted! Cannot bootstrap again");
194
241
  }
@@ -208,6 +255,8 @@ export function CreateApplication({ name, services = {}, libraries = [], configu
208
255
  services,
209
256
  async teardown() {
210
257
  if (!application.booted) {
258
+ // remove signal handlers even for un-booted teardown so they don't
259
+ // accumulate across test re-runs
211
260
  processEvents.forEach((callback, event) => process.removeListener(event, callback));
212
261
  return;
213
262
  }
@@ -221,12 +270,29 @@ export function CreateApplication({ name, services = {}, libraries = [], configu
221
270
  return application;
222
271
  }
223
272
  // #MARK: WireService
273
+ /**
274
+ * Instantiate a single service function and register it in the module registry.
275
+ *
276
+ * @remarks
277
+ * Builds the full `TServiceParams` injection object from the currently loaded
278
+ * modules and calls the service factory. The returned value is stored in
279
+ * `internal.boot.loadedModules` so subsequent services can reference it.
280
+ *
281
+ * Construction time is recorded for bootstrap stats (`showExtraBootStats`).
282
+ *
283
+ * If the factory throws, the error is treated as fatal — the process exits
284
+ * with code 1 because a failed constructor leaves the DI graph in a
285
+ * partially-initialized state with no safe recovery path.
286
+ *
287
+ * @internal
288
+ */
224
289
  async function wireService(project, service, definition, lifecycle, internal) {
225
290
  const mappings = internal.boot.moduleMappings.get(project) ?? {};
226
291
  mappings[service] = definition;
227
292
  internal.boot.moduleMappings.set(project, mappings);
228
293
  const context = COERCE_CONTEXT(`${project}:${service}`);
229
- // logger gets defined first, so this really is only for the start of the start of bootstrapping
294
+ // logger is not yet available at the very start of bootstrap; only undefined briefly
295
+ // during boilerplate wiring before the logger service itself is initialized
230
296
  const boilerplate = BOILERPLATE(internal);
231
297
  const logger = boilerplate?.logger?.context(context);
232
298
  const loaded = internal.boot.loadedModules.get(project) ?? {};
@@ -234,6 +300,8 @@ async function wireService(project, service, definition, lifecycle, internal) {
234
300
  try {
235
301
  logger?.trace({ name: wireService }, `initializing`);
236
302
  const serviceStart = performance.now();
303
+ // snapshot all currently-loaded modules so each service sees siblings
304
+ // that were wired before it, but not ones that come after (no forward refs)
237
305
  const inject = Object.fromEntries([...internal.boot.loadedModules.keys()].map(project => [
238
306
  project,
239
307
  internal.boot.loadedModules.get(project),
@@ -248,8 +316,11 @@ async function wireService(project, service, definition, lifecycle, internal) {
248
316
  lifecycle,
249
317
  logger,
250
318
  params: undefined,
319
+ // scheduler is a builder; call it with context so the returned API
320
+ // tags every scheduled callback with this service's context string
251
321
  scheduler: boilerplate?.scheduler?.(context),
252
322
  };
323
+ // params.params is a self-reference so callers can spread the whole bundle
253
324
  serviceParams.params = serviceParams;
254
325
  loaded[service] = (await definition(serviceParams));
255
326
  const serviceDuration = performance.now() - serviceStart;
@@ -261,32 +332,81 @@ async function wireService(project, service, definition, lifecycle, internal) {
261
332
  return loaded[service];
262
333
  }
263
334
  catch (error) {
264
- // Init errors at this level are considered blocking / fatal
335
+ // Default (app) mode: a constructor throw means the service graph is partially initialized
336
+ // and cannot recover, so fatalLog + exit is correct. But test/library consumers set
337
+ // customLogger to claim output and the rejection chain — the FATAL write leaks past their
338
+ // no-op logger and process.exit mangles the original error class through the test runner's
339
+ // interceptor. Short-circuit to a plain re-throw; app-mode fallthrough is unchanged.
340
+ if (internal.boot.options?.customLogger) {
341
+ throw error;
342
+ }
343
+ // constructor errors are blocking — a partially-initialized service graph
344
+ // cannot be safely recovered, so exit immediately
265
345
  fatalLog("initialization error", error);
266
346
  process.exit(EXIT_ERROR);
267
347
  }
268
348
  }
349
+ /**
350
+ * Execute the `PreInit` lifecycle stage and mark it complete.
351
+ * @internal
352
+ */
269
353
  const runPreInit = async (internal) => {
270
354
  const duration = await internal.boot.lifecycle.exec("PreInit");
271
355
  internal.boot.completedLifecycleEvents.add("PreInit");
272
356
  return duration;
273
357
  };
358
+ /**
359
+ * Execute the `PostConfig` lifecycle stage and mark it complete.
360
+ * @internal
361
+ */
274
362
  const runPostConfig = async (internal) => {
275
363
  const duration = await internal.boot.lifecycle.exec("PostConfig");
276
364
  internal.boot.completedLifecycleEvents.add("PostConfig");
277
365
  return duration;
278
366
  };
367
+ /**
368
+ * Execute the `Bootstrap` lifecycle stage and mark it complete.
369
+ * @internal
370
+ */
279
371
  const runBootstrap = async (internal) => {
280
372
  const duration = await internal.boot.lifecycle.exec("Bootstrap");
281
373
  internal.boot.completedLifecycleEvents.add("Bootstrap");
282
374
  return duration;
283
375
  };
376
+ /**
377
+ * Execute the `Ready` lifecycle stage and mark it complete.
378
+ * @internal
379
+ */
284
380
  const runReady = async (internal) => {
285
381
  const duration = await internal.boot.lifecycle.exec("Ready");
286
382
  internal.boot.completedLifecycleEvents.add("Ready");
287
383
  return duration;
288
384
  };
289
385
  // #MARK: Bootstrap
386
+ /**
387
+ * Wire all services for an application, run the full lifecycle, and return
388
+ * the `TServiceParams` for the bootstrap service.
389
+ *
390
+ * @remarks
391
+ * Sequence:
392
+ * 1. Initialize a fresh `InternalDefinition.boot` record.
393
+ * 2. Wire boilerplate (als, configuration, logger, scheduler).
394
+ * 3. Register SIGINT/SIGTERM process event handlers for graceful shutdown.
395
+ * 4. Wire declared libraries in dependency-sorted order.
396
+ * 5. Wire the application services (or defer to after Bootstrap if
397
+ * `bootLibrariesFirst` is set).
398
+ * 6. Apply any inline `options.configuration` overrides.
399
+ * 7. Run `PreInit → Configure (loaders) → PostConfig → Bootstrap → Ready`.
400
+ * 8. Resolve the returned `TServiceParams` promise via a synthetic
401
+ * "bootstrap" service wire call so the caller can access the wired graph.
402
+ *
403
+ * When `customLogger` is supplied (library/test mode), bootstrap failures reject
404
+ * the returned promise with the original error instead of calling `fatalLog` and
405
+ * `process.exit`. App-mode callers (no `customLogger`) get the existing
406
+ * fail-fast behavior.
407
+ *
408
+ * @internal
409
+ */
290
410
  async function bootstrap(application, options, internal) {
291
411
  const initTime = performance.now();
292
412
  internal.boot = {
@@ -306,14 +426,16 @@ async function bootstrap(application, options, internal) {
306
426
  const STATS = {};
307
427
  const CONSTRUCT = {};
308
428
  // pre-create loaded module for boilerplate, so it can be attached to `internal`
309
- // this allows it to be used as part of `internal` during boilerplate construction
310
- // otherwise it'd only be there for everyone else
429
+ // before the boilerplate services themselves are wired; without this pre-seeding
430
+ // the boilerplate services would not find each other in the module map
311
431
  const api = {};
312
432
  internal.boilerplate = api;
313
433
  internal.boot.loadedModules.set("boilerplate", api);
314
434
  STATS.Construct = CONSTRUCT;
315
435
  // * Recreate base eventemitter
316
436
  internal.utils.event = new EventEmitter();
437
+ // unlimited listeners: every service can register lifecycle hooks, and the
438
+ // default cap of 10 would produce spurious MaxListenersExceededWarnings
317
439
  internal.utils.event.setMaxListeners(NONE);
318
440
  // * Generate a new boilerplate module
319
441
  LIB_BOILERPLATE = createBoilerplate();
@@ -321,20 +443,23 @@ async function bootstrap(application, options, internal) {
321
443
  let start = performance.now();
322
444
  await LIB_BOILERPLATE[WIRE_PROJECT](internal, wireService);
323
445
  CONSTRUCT.boilerplate = `${(performance.now() - start).toFixed(DECIMALS)}ms`;
324
- // sync properties for convenience
446
+ // sync convenience aliases so downstream code reaches config via either path
325
447
  internal.config = api.configuration;
326
448
  // ~ configuration
327
449
  api.configuration?.[LOAD_PROJECT](LIB_BOILERPLATE.name, LIB_BOILERPLATE.configuration);
328
450
  const logger = api.logger.context(WIRING_CONTEXT);
329
451
  application.logger = logger;
330
452
  logger.debug({ name: bootstrap }, `[boilerplate] wiring complete`);
331
- // * Wire in various shutdown events
453
+ // register signal handlers now that the logger is available so teardown
454
+ // log lines are visible; handlers are removed on teardown/un-booted teardown
332
455
  processEvents.forEach((callback, event) => {
333
456
  process.on(event, callback);
334
457
  logger.trace({ event, name: bootstrap }, "register shutdown event");
335
458
  });
336
459
  // * Add in libraries
337
460
  application.libraries ??= [];
461
+ // appendLibrary allows replacing a library at bootstrap time, which is
462
+ // the primary mechanism for injecting test doubles at the library level
338
463
  if (!is.undefined(options?.appendLibrary)) {
339
464
  const list = is.array(options.appendLibrary)
340
465
  ? options.appendLibrary
@@ -342,7 +467,7 @@ async function bootstrap(application, options, internal) {
342
467
  list.forEach(append => {
343
468
  application.libraries.some((library, index) => {
344
469
  if (append.name === library.name) {
345
- // remove existing
470
+ // remove existing entry so the appended version takes its slot
346
471
  logger.warn({ name: append.name }, `replacing library`);
347
472
  application.libraries.splice(index, SINGLE);
348
473
  return true;
@@ -354,6 +479,7 @@ async function bootstrap(application, options, internal) {
354
479
  application.libraries.push(append);
355
480
  });
356
481
  }
482
+ // sort libraries so each one is wired after its declared dependencies
357
483
  const order = buildSortOrder(application, logger);
358
484
  await eachSeries(order, async (i) => {
359
485
  start = performance.now();
@@ -365,7 +491,8 @@ async function bootstrap(application, options, internal) {
365
491
  // * Finally the application
366
492
  if (options?.bootLibrariesFirst) {
367
493
  logger.debug({ name: bootstrap }, `bootLibrariesFirst`);
368
- // * preload config
494
+ // bootLibrariesFirst: skip application wiring here and run it later,
495
+ // between Bootstrap and Ready, so the app sees fully-initialized library services
369
496
  api.configuration[LOAD_PROJECT](application.name, application.configuration);
370
497
  }
371
498
  else {
@@ -374,7 +501,7 @@ async function bootstrap(application, options, internal) {
374
501
  await application[WIRE_PROJECT](internal, wireService);
375
502
  CONSTRUCT[application.name] = `${(performance.now() - start).toFixed(DECIMALS)}ms`;
376
503
  }
377
- // ? Configuration values provided bootstrap take priority over module level
504
+ // ? Configuration values provided at bootstrap take priority over module-level defaults
378
505
  if (!is.empty(options?.configuration)) {
379
506
  api.configuration.merge(options?.configuration);
380
507
  }
@@ -391,12 +518,9 @@ async function bootstrap(application, options, internal) {
391
518
  logger.debug({ name: bootstrap }, `[Bootstrap] running lifecycle callbacks`);
392
519
  STATS.Bootstrap = await runBootstrap(internal);
393
520
  if (options?.bootLibrariesFirst) {
394
- // * mental note
395
- // running between bootstrap & ready seems most appropriate
396
- // resources are expected to *technically* be ready at this point, but not finalized
397
- // reference examples:
398
- // - hass: socket is open & resources are ready
399
- // - fastify: bindings are available but port isn't listening
521
+ // wire the application between Bootstrap and Ready so library resources are
522
+ // fully initialized (e.g. hass socket open, fastify bindings registered)
523
+ // but the app itself does not start accepting work until Ready fires
400
524
  logger.debug({ name: bootstrap }, `late init application`);
401
525
  start = performance.now();
402
526
  await application[WIRE_PROJECT](internal, wireService);
@@ -421,9 +545,19 @@ async function bootstrap(application, options, internal) {
421
545
  }
422
546
  : { Total: STATS.Total, name: bootstrap }, `[%s] application bootstrapped`, application.name);
423
547
  internal.boot.phase = "running";
548
+ // resolve the bootstrap promise by wiring a synthetic "bootstrap" service
549
+ // whose params object is the fully-wired TServiceParams the caller receives
424
550
  return new Promise(done => wireService(application.name, "bootstrap", i => done(i), internal.boot.lifecycle.events, internal));
425
551
  }
426
552
  catch (error) {
553
+ // Default (app) mode: a lifecycle-stage throw (Bootstrap/Ready/etc.) gets a FATAL line and
554
+ // exits, which is correct for production. But test/library consumers set customLogger to
555
+ // claim output and the rejection chain — the FATAL write leaks past their no-op logger and
556
+ // process.exit mangles the original error class through the test runner's interceptor.
557
+ // Short-circuit to a plain re-throw; app-mode fallthrough is unchanged.
558
+ if (options?.customLogger) {
559
+ throw error;
560
+ }
427
561
  if (options?.configuration?.boilerplate?.LOG_LEVEL !== "silent") {
428
562
  fatalLog("bootstrap failed", error);
429
563
  }
@@ -431,7 +565,23 @@ async function bootstrap(application, options, internal) {
431
565
  }
432
566
  }
433
567
  // #MARK: Teardown
568
+ /**
569
+ * Run shutdown lifecycle stages and clean up process-level resources.
570
+ *
571
+ * @remarks
572
+ * Called by `.teardown()` on the application handle and indirectly by the
573
+ * SIGINT/SIGTERM handlers via `quickShutdown`. Idempotent at the phase check
574
+ * — only executes if the application is in the `"running"` phase.
575
+ *
576
+ * Sequence: `PreShutdown → ShutdownStart → (cancel active sleeps) →
577
+ * ShutdownComplete`. Any error during teardown is logged to stderr via
578
+ * `globalThis.console.error` because the logger itself may be in an
579
+ * indeterminate state at that point.
580
+ *
581
+ * @internal
582
+ */
434
583
  async function teardown(internal, logger) {
584
+ // skip if the app never fully started or has already been torn down
435
585
  if (internal.boot.phase !== "running") {
436
586
  return;
437
587
  }
@@ -448,14 +598,16 @@ async function teardown(internal, logger) {
448
598
  logger.debug({ name: teardown }, `[ShutdownStart] running lifecycle callbacks`);
449
599
  await internal.boot.lifecycle.exec("ShutdownStart");
450
600
  internal.boot.completedLifecycleEvents.add("ShutdownStart");
451
- // - clean up active `sleep` calls (can keep tests open and stuff)
601
+ // cancel any in-flight sleep() calls; without this they keep the event loop
602
+ // alive and hang test runners / process exits
452
603
  ACTIVE_SLEEPS.forEach(i => i.kill("stop"));
453
604
  logger.debug({ name: teardown }, `[ShutdownComplete] running lifecycle callbacks`);
454
605
  await internal.boot.lifecycle.exec("ShutdownComplete");
455
606
  internal.boot.completedLifecycleEvents.add("ShutdownComplete");
456
607
  }
457
608
  catch (error) {
458
- // ! oof
609
+ // teardown errors are logged via globalThis.console rather than logger because
610
+ // the logger service itself may have been partially torn down by this point
459
611
  globalThis.console.error({ error }, "error occurred during teardown, some lifecycle events may be incomplete");
460
612
  }
461
613
  // * Final resource cleanup, attempt to reset everything possible
@@ -463,6 +615,8 @@ async function teardown(internal, logger) {
463
615
  name: teardown,
464
616
  started_at: internal.utils.relativeDate(internal.boot.startup),
465
617
  }, `application terminated`);
618
+ // deregister signal handlers so they don't fire again if the process receives
619
+ // another signal after teardown completes
466
620
  processEvents.forEach((callback, event) => process.removeListener(event, callback));
467
621
  }
468
622
  //# sourceMappingURL=wiring.service.mjs.map