@btravstack/core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/index.cjs +34 -12
- package/dist/index.d.cts +14 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +34 -12
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -112,10 +112,13 @@ is a module that exports no runtime port at all.
|
|
|
112
112
|
## What you get
|
|
113
113
|
|
|
114
114
|
- **A drain that survives Kubernetes.** On SIGTERM: readiness flips false, the
|
|
115
|
-
kernel waits `preDrainDelayMs` (default `5_000`) before
|
|
116
|
-
to stop accepting — endpoint removal is eventually
|
|
117
|
-
in-flight work gets `drainTimeoutMs` (
|
|
118
|
-
open is aborted and reported
|
|
115
|
+
kernel waits `preDrainDelayMs` (`PRE_DRAIN_DELAY_MS`, default `5_000`) before
|
|
116
|
+
telling the runtime to stop accepting — endpoint removal is eventually
|
|
117
|
+
consistent — then in-flight work gets `drainTimeoutMs` (`DRAIN_TIMEOUT_MS`,
|
|
118
|
+
default `20_000`); whatever is still open is aborted and reported
|
|
119
|
+
`abandoned`. A second signal skips the drain. Both timings have to agree with
|
|
120
|
+
the pod's `terminationGracePeriodSeconds`, which is why the deployment can
|
|
121
|
+
set them and the option only pins them.
|
|
119
122
|
- **Probes from the state machine.** `GET /livez` and `GET /readyz` on
|
|
120
123
|
`PROBE_PORT` (default `9000`), up before the graph is built.
|
|
121
124
|
- **Configuration from the environment, typed.** The kernel provides
|
package/dist/index.cjs
CHANGED
|
@@ -361,6 +361,37 @@ const createUnitRegistry = () => {
|
|
|
361
361
|
//#endregion
|
|
362
362
|
//#region src/start.ts
|
|
363
363
|
const providesEnv = (module) => module.provides.some((provider) => provider.port.portId === _btravstack_config.Env.portId) || module.imports.some(providesEnv);
|
|
364
|
+
/**
|
|
365
|
+
* What the kernel reads from the environment for itself. Each field is pinned
|
|
366
|
+
* by the matching `StartOptions` field — explicit beats environment beats
|
|
367
|
+
* default, per field, exactly as a starter's own configuration is.
|
|
368
|
+
*/
|
|
369
|
+
const KERNEL_DEFAULTS = {
|
|
370
|
+
probePort: 9e3,
|
|
371
|
+
preDrainDelayMs: 5e3,
|
|
372
|
+
drainTimeoutMs: 2e4
|
|
373
|
+
};
|
|
374
|
+
const readKernelConfig = (options, env) => {
|
|
375
|
+
const probePort = options.probes === void 0 ? void 0 : options.probes === false ? KERNEL_DEFAULTS.probePort : options.probes.port;
|
|
376
|
+
const result = _btravstack_config.Config.object({
|
|
377
|
+
probePort: _btravstack_config.Config.pinned(probePort, _btravstack_config.Config.port("PROBE_PORT", { default: KERNEL_DEFAULTS.probePort })),
|
|
378
|
+
preDrainDelayMs: _btravstack_config.Config.pinned(options.preDrainDelayMs, _btravstack_config.Config.integer("PRE_DRAIN_DELAY_MS", {
|
|
379
|
+
default: KERNEL_DEFAULTS.preDrainDelayMs,
|
|
380
|
+
min: 0
|
|
381
|
+
})),
|
|
382
|
+
drainTimeoutMs: _btravstack_config.Config.pinned(options.drainTimeoutMs, _btravstack_config.Config.integer("DRAIN_TIMEOUT_MS", {
|
|
383
|
+
default: KERNEL_DEFAULTS.drainTimeoutMs,
|
|
384
|
+
min: 0
|
|
385
|
+
}))
|
|
386
|
+
})["~standard"].validate(env);
|
|
387
|
+
return result.issues === void 0 ? (0, unthrown.Ok)(result.value) : (0, unthrown.Err)(new RuntimeStartFailed({
|
|
388
|
+
runtime: "kernel",
|
|
389
|
+
cause: new _btravstack_config.ConfigInvalid({
|
|
390
|
+
port: "kernel",
|
|
391
|
+
issues: result.issues
|
|
392
|
+
})
|
|
393
|
+
}));
|
|
394
|
+
};
|
|
364
395
|
const start = (module, options = {}) => {
|
|
365
396
|
const clock = options.clock ?? systemClock;
|
|
366
397
|
const env = options.env ?? process.env;
|
|
@@ -382,8 +413,8 @@ const start = (module, options = {}) => {
|
|
|
382
413
|
const shutdown = Promise.withResolvers();
|
|
383
414
|
const teardownErrors = [];
|
|
384
415
|
const startedAt = clock.now();
|
|
385
|
-
const
|
|
386
|
-
const drainTimeoutMs =
|
|
416
|
+
const kernelConfig = readKernelConfig(options, env);
|
|
417
|
+
const { preDrainDelayMs, drainTimeoutMs } = kernelConfig.getOrElse(() => KERNEL_DEFAULTS);
|
|
387
418
|
const skipDrain = new AbortController();
|
|
388
419
|
let shutdownRequestedAt = startedAt;
|
|
389
420
|
let shutdownRequested = false;
|
|
@@ -420,16 +451,7 @@ const start = (module, options = {}) => {
|
|
|
420
451
|
const probeBound = Promise.withResolvers();
|
|
421
452
|
const runtimePublished = Promise.withResolvers();
|
|
422
453
|
emit({ type: "building" });
|
|
423
|
-
const probesOptions =
|
|
424
|
-
runtime: "probes",
|
|
425
|
-
cause: new _btravstack_config.ConfigInvalid({
|
|
426
|
-
port: "probes",
|
|
427
|
-
issues: [{
|
|
428
|
-
message: reason,
|
|
429
|
-
path: ["PROBE_PORT"]
|
|
430
|
-
}]
|
|
431
|
-
})
|
|
432
|
-
}))) : (0, unthrown.Ok)(options.probes);
|
|
454
|
+
const probesOptions = kernelConfig.map(({ probePort }) => options.probes === false ? false : { port: probePort });
|
|
433
455
|
if (options.probes === false) probeBound.resolve(void 0);
|
|
434
456
|
const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? (0, unthrown.OkAsync)() : startProbeServer({
|
|
435
457
|
port: probes.port,
|
package/dist/index.d.cts
CHANGED
|
@@ -458,7 +458,21 @@ type StartOptions<UnitX = never, UnitNeeds = never> = {
|
|
|
458
458
|
readonly probes?: {
|
|
459
459
|
readonly port: number;
|
|
460
460
|
} | false;
|
|
461
|
+
/**
|
|
462
|
+
* How long readiness stays false before the runtime is told to stop
|
|
463
|
+
* accepting. Unset, it is bound from `PRE_DRAIN_DELAY_MS` in `env` (default
|
|
464
|
+
* `5_000`) — the window that covers Kubernetes' eventually-consistent
|
|
465
|
+
* endpoint removal, which is a property of the cluster rather than of the
|
|
466
|
+
* code, so a deployment must be able to set it.
|
|
467
|
+
*/
|
|
461
468
|
readonly preDrainDelayMs?: number;
|
|
469
|
+
/**
|
|
470
|
+
* How long in-flight work gets before it is aborted and reported
|
|
471
|
+
* `abandoned`. Unset, it is bound from `DRAIN_TIMEOUT_MS` in `env` (default
|
|
472
|
+
* `20_000`). Keep it under the pod's `terminationGracePeriodSeconds`, which
|
|
473
|
+
* is the reason this one belongs in the environment: the two are set
|
|
474
|
+
* together, in the same manifest.
|
|
475
|
+
*/
|
|
462
476
|
readonly drainTimeoutMs?: number;
|
|
463
477
|
readonly onEvent?: EventSink;
|
|
464
478
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -458,7 +458,21 @@ type StartOptions<UnitX = never, UnitNeeds = never> = {
|
|
|
458
458
|
readonly probes?: {
|
|
459
459
|
readonly port: number;
|
|
460
460
|
} | false;
|
|
461
|
+
/**
|
|
462
|
+
* How long readiness stays false before the runtime is told to stop
|
|
463
|
+
* accepting. Unset, it is bound from `PRE_DRAIN_DELAY_MS` in `env` (default
|
|
464
|
+
* `5_000`) — the window that covers Kubernetes' eventually-consistent
|
|
465
|
+
* endpoint removal, which is a property of the cluster rather than of the
|
|
466
|
+
* code, so a deployment must be able to set it.
|
|
467
|
+
*/
|
|
461
468
|
readonly preDrainDelayMs?: number;
|
|
469
|
+
/**
|
|
470
|
+
* How long in-flight work gets before it is aborted and reported
|
|
471
|
+
* `abandoned`. Unset, it is bound from `DRAIN_TIMEOUT_MS` in `env` (default
|
|
472
|
+
* `20_000`). Keep it under the pod's `terminationGracePeriodSeconds`, which
|
|
473
|
+
* is the reason this one belongs in the environment: the two are set
|
|
474
|
+
* together, in the same manifest.
|
|
475
|
+
*/
|
|
462
476
|
readonly drainTimeoutMs?: number;
|
|
463
477
|
readonly onEvent?: EventSink;
|
|
464
478
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -360,6 +360,37 @@ const createUnitRegistry = () => {
|
|
|
360
360
|
//#endregion
|
|
361
361
|
//#region src/start.ts
|
|
362
362
|
const providesEnv = (module) => module.provides.some((provider) => provider.port.portId === Env.portId) || module.imports.some(providesEnv);
|
|
363
|
+
/**
|
|
364
|
+
* What the kernel reads from the environment for itself. Each field is pinned
|
|
365
|
+
* by the matching `StartOptions` field — explicit beats environment beats
|
|
366
|
+
* default, per field, exactly as a starter's own configuration is.
|
|
367
|
+
*/
|
|
368
|
+
const KERNEL_DEFAULTS = {
|
|
369
|
+
probePort: 9e3,
|
|
370
|
+
preDrainDelayMs: 5e3,
|
|
371
|
+
drainTimeoutMs: 2e4
|
|
372
|
+
};
|
|
373
|
+
const readKernelConfig = (options, env) => {
|
|
374
|
+
const probePort = options.probes === void 0 ? void 0 : options.probes === false ? KERNEL_DEFAULTS.probePort : options.probes.port;
|
|
375
|
+
const result = Config.object({
|
|
376
|
+
probePort: Config.pinned(probePort, Config.port("PROBE_PORT", { default: KERNEL_DEFAULTS.probePort })),
|
|
377
|
+
preDrainDelayMs: Config.pinned(options.preDrainDelayMs, Config.integer("PRE_DRAIN_DELAY_MS", {
|
|
378
|
+
default: KERNEL_DEFAULTS.preDrainDelayMs,
|
|
379
|
+
min: 0
|
|
380
|
+
})),
|
|
381
|
+
drainTimeoutMs: Config.pinned(options.drainTimeoutMs, Config.integer("DRAIN_TIMEOUT_MS", {
|
|
382
|
+
default: KERNEL_DEFAULTS.drainTimeoutMs,
|
|
383
|
+
min: 0
|
|
384
|
+
}))
|
|
385
|
+
})["~standard"].validate(env);
|
|
386
|
+
return result.issues === void 0 ? Ok(result.value) : Err(new RuntimeStartFailed({
|
|
387
|
+
runtime: "kernel",
|
|
388
|
+
cause: new ConfigInvalid({
|
|
389
|
+
port: "kernel",
|
|
390
|
+
issues: result.issues
|
|
391
|
+
})
|
|
392
|
+
}));
|
|
393
|
+
};
|
|
363
394
|
const start = (module, options = {}) => {
|
|
364
395
|
const clock = options.clock ?? systemClock;
|
|
365
396
|
const env = options.env ?? process.env;
|
|
@@ -381,8 +412,8 @@ const start = (module, options = {}) => {
|
|
|
381
412
|
const shutdown = Promise.withResolvers();
|
|
382
413
|
const teardownErrors = [];
|
|
383
414
|
const startedAt = clock.now();
|
|
384
|
-
const
|
|
385
|
-
const drainTimeoutMs =
|
|
415
|
+
const kernelConfig = readKernelConfig(options, env);
|
|
416
|
+
const { preDrainDelayMs, drainTimeoutMs } = kernelConfig.getOrElse(() => KERNEL_DEFAULTS);
|
|
386
417
|
const skipDrain = new AbortController();
|
|
387
418
|
let shutdownRequestedAt = startedAt;
|
|
388
419
|
let shutdownRequested = false;
|
|
@@ -419,16 +450,7 @@ const start = (module, options = {}) => {
|
|
|
419
450
|
const probeBound = Promise.withResolvers();
|
|
420
451
|
const runtimePublished = Promise.withResolvers();
|
|
421
452
|
emit({ type: "building" });
|
|
422
|
-
const probesOptions =
|
|
423
|
-
runtime: "probes",
|
|
424
|
-
cause: new ConfigInvalid({
|
|
425
|
-
port: "probes",
|
|
426
|
-
issues: [{
|
|
427
|
-
message: reason,
|
|
428
|
-
path: ["PROBE_PORT"]
|
|
429
|
-
}]
|
|
430
|
-
})
|
|
431
|
-
}))) : Ok(options.probes);
|
|
453
|
+
const probesOptions = kernelConfig.map(({ probePort }) => options.probes === false ? false : { port: probePort });
|
|
432
454
|
if (options.probes === false) probeBound.resolve(void 0);
|
|
433
455
|
const probesStarted = probesOptions.toAsync().flatMap((probes) => probes === false ? OkAsync() : startProbeServer({
|
|
434
456
|
port: probes.port,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The application kernel: boot a di module into a running process, and stop it without losing work",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"application",
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
"typescript": "7.0.2",
|
|
54
54
|
"unthrown": "5.7.0",
|
|
55
55
|
"vitest": "4.1.11",
|
|
56
|
-
"@btravstack/config": "0.
|
|
57
|
-
"@btravstack/di": "0.
|
|
56
|
+
"@btravstack/config": "0.7.0",
|
|
57
|
+
"@btravstack/di": "0.7.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"unthrown": "^5.0.0",
|
|
61
|
-
"@btravstack/config": "^0.
|
|
62
|
-
"@btravstack/di": "^0.
|
|
61
|
+
"@btravstack/config": "^0.7.0",
|
|
62
|
+
"@btravstack/di": "^0.7.0"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22"
|