@digital-alchemy/core 26.1.9 → 26.5.1

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 +66 -1
  10. package/dist/helpers/config-file-loader.mjs +82 -6
  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 +178 -17
  35. package/dist/helpers/logger.mjs +41 -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 +29 -1
  74. package/dist/services/wiring.service.mjs +153 -20
  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 +107 -27
  83. package/dist/testing/test-module.mjs +58 -1
  84. package/dist/testing/test-module.mjs.map +1 -1
  85. package/package.json +33 -31
@@ -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
@@ -39,7 +50,7 @@ declare function createBoilerplate(): import("../index.mts").LibraryDefinition<{
39
50
  scheduler: typeof Scheduler;
40
51
  }, {
41
52
  /**
42
- * Only usable by **cli switch**.
53
+ * Only usable by **cli switch** / application bootstrap.
43
54
  * Pass path to a config file for loader
44
55
  *
45
56
  * ```bash
@@ -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,13 +13,24 @@ 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.
17
30
  return CreateLibrary({
18
31
  configuration: {
19
32
  /**
20
- * Only usable by **cli switch**.
33
+ * Only usable by **cli switch** / application bootstrap.
21
34
  * Pass path to a config file for loader
22
35
  *
23
36
  * ```bash
@@ -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,68 @@ 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
+ // constructor errors are blocking a partially-initialized service graph
336
+ // cannot be safely recovered, so exit immediately
265
337
  fatalLog("initialization error", error);
266
338
  process.exit(EXIT_ERROR);
267
339
  }
268
340
  }
341
+ /**
342
+ * Execute the `PreInit` lifecycle stage and mark it complete.
343
+ * @internal
344
+ */
269
345
  const runPreInit = async (internal) => {
270
346
  const duration = await internal.boot.lifecycle.exec("PreInit");
271
347
  internal.boot.completedLifecycleEvents.add("PreInit");
272
348
  return duration;
273
349
  };
350
+ /**
351
+ * Execute the `PostConfig` lifecycle stage and mark it complete.
352
+ * @internal
353
+ */
274
354
  const runPostConfig = async (internal) => {
275
355
  const duration = await internal.boot.lifecycle.exec("PostConfig");
276
356
  internal.boot.completedLifecycleEvents.add("PostConfig");
277
357
  return duration;
278
358
  };
359
+ /**
360
+ * Execute the `Bootstrap` lifecycle stage and mark it complete.
361
+ * @internal
362
+ */
279
363
  const runBootstrap = async (internal) => {
280
364
  const duration = await internal.boot.lifecycle.exec("Bootstrap");
281
365
  internal.boot.completedLifecycleEvents.add("Bootstrap");
282
366
  return duration;
283
367
  };
368
+ /**
369
+ * Execute the `Ready` lifecycle stage and mark it complete.
370
+ * @internal
371
+ */
284
372
  const runReady = async (internal) => {
285
373
  const duration = await internal.boot.lifecycle.exec("Ready");
286
374
  internal.boot.completedLifecycleEvents.add("Ready");
287
375
  return duration;
288
376
  };
289
377
  // #MARK: Bootstrap
378
+ /**
379
+ * Wire all services for an application, run the full lifecycle, and return
380
+ * the `TServiceParams` for the bootstrap service.
381
+ *
382
+ * @remarks
383
+ * Sequence:
384
+ * 1. Initialize a fresh `InternalDefinition.boot` record.
385
+ * 2. Wire boilerplate (als, configuration, logger, scheduler).
386
+ * 3. Register SIGINT/SIGTERM process event handlers for graceful shutdown.
387
+ * 4. Wire declared libraries in dependency-sorted order.
388
+ * 5. Wire the application services (or defer to after Bootstrap if
389
+ * `bootLibrariesFirst` is set).
390
+ * 6. Apply any inline `options.configuration` overrides.
391
+ * 7. Run `PreInit → Configure (loaders) → PostConfig → Bootstrap → Ready`.
392
+ * 8. Resolve the returned `TServiceParams` promise via a synthetic
393
+ * "bootstrap" service wire call so the caller can access the wired graph.
394
+ *
395
+ * @internal
396
+ */
290
397
  async function bootstrap(application, options, internal) {
291
398
  const initTime = performance.now();
292
399
  internal.boot = {
@@ -306,14 +413,16 @@ async function bootstrap(application, options, internal) {
306
413
  const STATS = {};
307
414
  const CONSTRUCT = {};
308
415
  // 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
416
+ // before the boilerplate services themselves are wired; without this pre-seeding
417
+ // the boilerplate services would not find each other in the module map
311
418
  const api = {};
312
419
  internal.boilerplate = api;
313
420
  internal.boot.loadedModules.set("boilerplate", api);
314
421
  STATS.Construct = CONSTRUCT;
315
422
  // * Recreate base eventemitter
316
423
  internal.utils.event = new EventEmitter();
424
+ // unlimited listeners: every service can register lifecycle hooks, and the
425
+ // default cap of 10 would produce spurious MaxListenersExceededWarnings
317
426
  internal.utils.event.setMaxListeners(NONE);
318
427
  // * Generate a new boilerplate module
319
428
  LIB_BOILERPLATE = createBoilerplate();
@@ -321,20 +430,23 @@ async function bootstrap(application, options, internal) {
321
430
  let start = performance.now();
322
431
  await LIB_BOILERPLATE[WIRE_PROJECT](internal, wireService);
323
432
  CONSTRUCT.boilerplate = `${(performance.now() - start).toFixed(DECIMALS)}ms`;
324
- // sync properties for convenience
433
+ // sync convenience aliases so downstream code reaches config via either path
325
434
  internal.config = api.configuration;
326
435
  // ~ configuration
327
436
  api.configuration?.[LOAD_PROJECT](LIB_BOILERPLATE.name, LIB_BOILERPLATE.configuration);
328
437
  const logger = api.logger.context(WIRING_CONTEXT);
329
438
  application.logger = logger;
330
439
  logger.debug({ name: bootstrap }, `[boilerplate] wiring complete`);
331
- // * Wire in various shutdown events
440
+ // register signal handlers now that the logger is available so teardown
441
+ // log lines are visible; handlers are removed on teardown/un-booted teardown
332
442
  processEvents.forEach((callback, event) => {
333
443
  process.on(event, callback);
334
444
  logger.trace({ event, name: bootstrap }, "register shutdown event");
335
445
  });
336
446
  // * Add in libraries
337
447
  application.libraries ??= [];
448
+ // appendLibrary allows replacing a library at bootstrap time, which is
449
+ // the primary mechanism for injecting test doubles at the library level
338
450
  if (!is.undefined(options?.appendLibrary)) {
339
451
  const list = is.array(options.appendLibrary)
340
452
  ? options.appendLibrary
@@ -342,7 +454,7 @@ async function bootstrap(application, options, internal) {
342
454
  list.forEach(append => {
343
455
  application.libraries.some((library, index) => {
344
456
  if (append.name === library.name) {
345
- // remove existing
457
+ // remove existing entry so the appended version takes its slot
346
458
  logger.warn({ name: append.name }, `replacing library`);
347
459
  application.libraries.splice(index, SINGLE);
348
460
  return true;
@@ -354,6 +466,7 @@ async function bootstrap(application, options, internal) {
354
466
  application.libraries.push(append);
355
467
  });
356
468
  }
469
+ // sort libraries so each one is wired after its declared dependencies
357
470
  const order = buildSortOrder(application, logger);
358
471
  await eachSeries(order, async (i) => {
359
472
  start = performance.now();
@@ -365,7 +478,8 @@ async function bootstrap(application, options, internal) {
365
478
  // * Finally the application
366
479
  if (options?.bootLibrariesFirst) {
367
480
  logger.debug({ name: bootstrap }, `bootLibrariesFirst`);
368
- // * preload config
481
+ // bootLibrariesFirst: skip application wiring here and run it later,
482
+ // between Bootstrap and Ready, so the app sees fully-initialized library services
369
483
  api.configuration[LOAD_PROJECT](application.name, application.configuration);
370
484
  }
371
485
  else {
@@ -374,7 +488,7 @@ async function bootstrap(application, options, internal) {
374
488
  await application[WIRE_PROJECT](internal, wireService);
375
489
  CONSTRUCT[application.name] = `${(performance.now() - start).toFixed(DECIMALS)}ms`;
376
490
  }
377
- // ? Configuration values provided bootstrap take priority over module level
491
+ // ? Configuration values provided at bootstrap take priority over module-level defaults
378
492
  if (!is.empty(options?.configuration)) {
379
493
  api.configuration.merge(options?.configuration);
380
494
  }
@@ -391,12 +505,9 @@ async function bootstrap(application, options, internal) {
391
505
  logger.debug({ name: bootstrap }, `[Bootstrap] running lifecycle callbacks`);
392
506
  STATS.Bootstrap = await runBootstrap(internal);
393
507
  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
508
+ // wire the application between Bootstrap and Ready so library resources are
509
+ // fully initialized (e.g. hass socket open, fastify bindings registered)
510
+ // but the app itself does not start accepting work until Ready fires
400
511
  logger.debug({ name: bootstrap }, `late init application`);
401
512
  start = performance.now();
402
513
  await application[WIRE_PROJECT](internal, wireService);
@@ -421,6 +532,8 @@ async function bootstrap(application, options, internal) {
421
532
  }
422
533
  : { Total: STATS.Total, name: bootstrap }, `[%s] application bootstrapped`, application.name);
423
534
  internal.boot.phase = "running";
535
+ // resolve the bootstrap promise by wiring a synthetic "bootstrap" service
536
+ // whose params object is the fully-wired TServiceParams the caller receives
424
537
  return new Promise(done => wireService(application.name, "bootstrap", i => done(i), internal.boot.lifecycle.events, internal));
425
538
  }
426
539
  catch (error) {
@@ -431,7 +544,23 @@ async function bootstrap(application, options, internal) {
431
544
  }
432
545
  }
433
546
  // #MARK: Teardown
547
+ /**
548
+ * Run shutdown lifecycle stages and clean up process-level resources.
549
+ *
550
+ * @remarks
551
+ * Called by `.teardown()` on the application handle and indirectly by the
552
+ * SIGINT/SIGTERM handlers via `quickShutdown`. Idempotent at the phase check
553
+ * — only executes if the application is in the `"running"` phase.
554
+ *
555
+ * Sequence: `PreShutdown → ShutdownStart → (cancel active sleeps) →
556
+ * ShutdownComplete`. Any error during teardown is logged to stderr via
557
+ * `globalThis.console.error` because the logger itself may be in an
558
+ * indeterminate state at that point.
559
+ *
560
+ * @internal
561
+ */
434
562
  async function teardown(internal, logger) {
563
+ // skip if the app never fully started or has already been torn down
435
564
  if (internal.boot.phase !== "running") {
436
565
  return;
437
566
  }
@@ -448,14 +577,16 @@ async function teardown(internal, logger) {
448
577
  logger.debug({ name: teardown }, `[ShutdownStart] running lifecycle callbacks`);
449
578
  await internal.boot.lifecycle.exec("ShutdownStart");
450
579
  internal.boot.completedLifecycleEvents.add("ShutdownStart");
451
- // - clean up active `sleep` calls (can keep tests open and stuff)
580
+ // cancel any in-flight sleep() calls; without this they keep the event loop
581
+ // alive and hang test runners / process exits
452
582
  ACTIVE_SLEEPS.forEach(i => i.kill("stop"));
453
583
  logger.debug({ name: teardown }, `[ShutdownComplete] running lifecycle callbacks`);
454
584
  await internal.boot.lifecycle.exec("ShutdownComplete");
455
585
  internal.boot.completedLifecycleEvents.add("ShutdownComplete");
456
586
  }
457
587
  catch (error) {
458
- // ! oof
588
+ // teardown errors are logged via globalThis.console rather than logger because
589
+ // the logger service itself may have been partially torn down by this point
459
590
  globalThis.console.error({ error }, "error occurred during teardown, some lifecycle events may be incomplete");
460
591
  }
461
592
  // * Final resource cleanup, attempt to reset everything possible
@@ -463,6 +594,8 @@ async function teardown(internal, logger) {
463
594
  name: teardown,
464
595
  started_at: internal.utils.relativeDate(internal.boot.startup),
465
596
  }, `application terminated`);
597
+ // deregister signal handlers so they don't fire again if the process receives
598
+ // another signal after teardown completes
466
599
  processEvents.forEach((callback, event) => process.removeListener(event, callback));
467
600
  }
468
601
  //# sourceMappingURL=wiring.service.mjs.map