@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.
- 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 +66 -1
- package/dist/helpers/config-file-loader.mjs +82 -6
- 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 +178 -17
- package/dist/helpers/logger.mjs +41 -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 +29 -1
- package/dist/services/wiring.service.mjs +153 -20
- 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 +107 -27
- package/dist/testing/test-module.mjs +58 -1
- package/dist/testing/test-module.mjs.map +1 -1
- 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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
310
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|