@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.
- package/CLAUDE.md +302 -0
- package/README.md +19 -3
- package/dist/helpers/async.d.mts +37 -0
- package/dist/helpers/async.mjs +50 -15
- package/dist/helpers/async.mjs.map +1 -1
- package/dist/helpers/config-environment-loader.d.mts +39 -0
- package/dist/helpers/config-environment-loader.mjs +51 -11
- package/dist/helpers/config-environment-loader.mjs.map +1 -1
- package/dist/helpers/config-file-loader.d.mts +65 -0
- package/dist/helpers/config-file-loader.mjs +80 -4
- package/dist/helpers/config-file-loader.mjs.map +1 -1
- package/dist/helpers/config.d.mts +202 -5
- package/dist/helpers/config.mjs +60 -0
- package/dist/helpers/config.mjs.map +1 -1
- package/dist/helpers/context.d.mts +12 -1
- package/dist/helpers/cron.d.mts +154 -7
- package/dist/helpers/cron.mjs +47 -4
- package/dist/helpers/cron.mjs.map +1 -1
- package/dist/helpers/errors.d.mts +45 -0
- package/dist/helpers/errors.mjs +45 -0
- package/dist/helpers/errors.mjs.map +1 -1
- package/dist/helpers/events.d.mts +23 -0
- package/dist/helpers/events.mjs +23 -0
- package/dist/helpers/events.mjs.map +1 -1
- package/dist/helpers/extend.d.mts +50 -0
- package/dist/helpers/extend.mjs +63 -0
- package/dist/helpers/extend.mjs.map +1 -1
- package/dist/helpers/index.d.mts +9 -0
- package/dist/helpers/index.mjs +9 -0
- package/dist/helpers/index.mjs.map +1 -1
- package/dist/helpers/lifecycle.d.mts +102 -16
- package/dist/helpers/lifecycle.mjs +19 -1
- package/dist/helpers/lifecycle.mjs.map +1 -1
- package/dist/helpers/logger.d.mts +182 -17
- package/dist/helpers/logger.mjs +45 -1
- package/dist/helpers/logger.mjs.map +1 -1
- package/dist/helpers/module.d.mts +110 -0
- package/dist/helpers/module.mjs +55 -6
- package/dist/helpers/module.mjs.map +1 -1
- package/dist/helpers/service-runner.d.mts +27 -1
- package/dist/helpers/service-runner.mjs +27 -1
- package/dist/helpers/service-runner.mjs.map +1 -1
- package/dist/helpers/utilities.d.mts +123 -3
- package/dist/helpers/utilities.mjs +110 -3
- package/dist/helpers/utilities.mjs.map +1 -1
- package/dist/helpers/wiring.d.mts +385 -0
- package/dist/helpers/wiring.mjs +120 -0
- package/dist/helpers/wiring.mjs.map +1 -1
- package/dist/services/als.service.d.mts +10 -0
- package/dist/services/als.service.mjs +49 -0
- package/dist/services/als.service.mjs.map +1 -1
- package/dist/services/configuration.service.d.mts +22 -0
- package/dist/services/configuration.service.mjs +140 -12
- package/dist/services/configuration.service.mjs.map +1 -1
- package/dist/services/index.d.mts +8 -0
- package/dist/services/index.mjs +8 -0
- package/dist/services/index.mjs.map +1 -1
- package/dist/services/internal.service.d.mts +98 -19
- package/dist/services/internal.service.mjs +91 -9
- package/dist/services/internal.service.mjs.map +1 -1
- package/dist/services/is.service.d.mts +64 -4
- package/dist/services/is.service.mjs +67 -4
- package/dist/services/is.service.mjs.map +1 -1
- package/dist/services/lifecycle.service.d.mts +26 -0
- package/dist/services/lifecycle.service.mjs +67 -9
- package/dist/services/lifecycle.service.mjs.map +1 -1
- package/dist/services/logger.service.d.mts +27 -0
- package/dist/services/logger.service.mjs +133 -9
- package/dist/services/logger.service.mjs.map +1 -1
- package/dist/services/scheduler.service.d.mts +19 -0
- package/dist/services/scheduler.service.mjs +87 -4
- package/dist/services/scheduler.service.mjs.map +1 -1
- package/dist/services/wiring.service.d.mts +28 -0
- package/dist/services/wiring.service.mjs +173 -19
- package/dist/services/wiring.service.mjs.map +1 -1
- package/dist/testing/index.d.mts +4 -0
- package/dist/testing/index.mjs +4 -0
- package/dist/testing/index.mjs.map +1 -1
- package/dist/testing/mock-logger.d.mts +8 -0
- package/dist/testing/mock-logger.mjs +9 -0
- package/dist/testing/mock-logger.mjs.map +1 -1
- package/dist/testing/test-module.d.mts +112 -27
- package/dist/testing/test-module.mjs +64 -2
- package/dist/testing/test-module.mjs.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
310
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|