@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 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 telling the runtime
116
- to stop accepting — endpoint removal is eventually consistent — then
117
- in-flight work gets `drainTimeoutMs` (default `20_000`); whatever is still
118
- open is aborted and reported `abandoned`. A second signal skips the drain.
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 preDrainDelayMs = options.preDrainDelayMs ?? 5e3;
386
- const drainTimeoutMs = options.drainTimeoutMs ?? 2e4;
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 = options.probes === void 0 ? _btravstack_config.Config.port("PROBE_PORT", { default: 9e3 }).parse(env["PROBE_PORT"]).map((port) => ({ port })).mapErrCases((matcher) => matcher.with(unthrown.P.tag("ConfigFieldInvalid"), ({ reason }) => new RuntimeStartFailed({
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 preDrainDelayMs = options.preDrainDelayMs ?? 5e3;
385
- const drainTimeoutMs = options.drainTimeoutMs ?? 2e4;
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 = options.probes === void 0 ? Config.port("PROBE_PORT", { default: 9e3 }).parse(env["PROBE_PORT"]).map((port) => ({ port })).mapErrCases((matcher) => matcher.with(P.tag("ConfigFieldInvalid"), ({ reason }) => new RuntimeStartFailed({
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.6.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.6.0",
57
- "@btravstack/di": "0.6.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.6.0",
62
- "@btravstack/di": "^0.6.0"
61
+ "@btravstack/config": "^0.7.0",
62
+ "@btravstack/di": "^0.7.0"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=22"