@btravstack/core 0.32.0 → 0.33.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/dist/index.cjs CHANGED
@@ -455,6 +455,36 @@ const installUncaughtHandlers = (onUncaught) => {
455
455
  var UnitOverrides = class extends _btravstack_di.Port.many("UnitOverrides") {};
456
456
  /** Whether `module`'s tree carries a provider for `portId`. */
457
457
  const provides = (module, portId) => module.provides.some((provider) => provider.port.portId === portId) || module.imports.some((imported) => provides(imported, portId));
458
+ /** Every port `module`'s tree provides, by its class. */
459
+ const providedPorts = (module) => [...module.imports.flatMap(providedPorts), ...module.provides.map((provider) => provider.port)];
460
+ /**
461
+ * A check, per unit module, that it provides no port the application scope
462
+ * already provides. A fork builds its whole module tree per unit, so such a
463
+ * provider would shadow the application's instance with a fresh one per unit,
464
+ * released per unit — the application's configuration re-parsed, its clients
465
+ * reopened. A unit's member of the SAME set port is the one exemption, since
466
+ * the fork appends it to the application's members; a set port meeting an
467
+ * ordinary one, or two classes sharing an id, is refused like any other.
468
+ *
469
+ * Answers the refusal rather than throwing it, so the boot can throw it and a
470
+ * fork can reject with it. A module is walked once, however often it forks.
471
+ */
472
+ const rebuildGuard = (runtime, application) => {
473
+ const owned = new Map(providedPorts(application).map((port) => [port.portId, port]));
474
+ const clean = /* @__PURE__ */ new WeakSet();
475
+ return (module) => {
476
+ if (clean.has(module)) return void 0;
477
+ const rebuilt = providedPorts(module).find((port) => {
478
+ const owner = owned.get(port.portId);
479
+ return owner !== void 0 && !(owner === port && port.many === true);
480
+ });
481
+ if (rebuilt === void 0) {
482
+ clean.add(module);
483
+ return;
484
+ }
485
+ return /* @__PURE__ */ new Error(`[core] unit module "${module.name}" of runtime "${runtime}" provides port ${JSON.stringify(rebuilt.portId)}, which the application scope already provides — a unit would rebuild it per unit; remove it from the unit module and let the unit read the application's`);
486
+ };
487
+ };
458
488
  /**
459
489
  * Each overridden kind's module, mapped to the module the kernel forks in its
460
490
  * place. Keyed by module IDENTITY, because `UnitHost.fork` is handed the
@@ -804,6 +834,11 @@ const start = (module, options = {}) => {
804
834
  runtimeName = runtime.name;
805
835
  healthChecks = ctx.get(HealthChecks);
806
836
  const runtimeCtx = ctx;
837
+ const rebuilt = rebuildGuard(runtime.name, root);
838
+ for (const unit of Object.values(runtime.units ?? {})) {
839
+ const refusal = unit === void 0 ? void 0 : rebuilt(unit);
840
+ if (refusal !== void 0) throw refusal;
841
+ }
807
842
  const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
808
843
  const run = (meta, work) => registry.run(meta, (signal) => {
809
844
  const settled = Promise.withResolvers();
@@ -812,6 +847,8 @@ const start = (module, options = {}) => {
812
847
  const fork = (module, seed) => {
813
848
  if (hasSettled) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
814
849
  if (closing !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
850
+ const refusal = rebuilt(module);
851
+ if (refusal !== void 0) return (0, unthrown.fromSafePromise)(Promise.reject(refusal));
815
852
  const ready = Promise.withResolvers();
816
853
  closing = _btravstack_di.Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
817
854
  ready.resolve(forked);
@@ -948,8 +985,7 @@ const awaitExit = async (app, exit) => {
948
985
  *
949
986
  * @example
950
987
  * ```ts
951
- * // `OrderApi` imports the application next to `http()` and exports
952
- * // `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
988
+ * // `OrderApi` is an `HttpModule`, which exports `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
953
989
  * await runMain(OrderApi);
954
990
  * ```
955
991
  */
package/dist/index.d.cts CHANGED
@@ -418,20 +418,25 @@ type AnyUnitModule = Module<never, never, unknown>;
418
418
  * The needs a bound unit module still owes, or `never` when none is bound.
419
419
  * `Scope` is excluded, since nothing can ever provide it — the same exemption
420
420
  * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
421
- * fork seed discharges.
421
+ * fork seed discharges. `Env` is replaced by a refusal nothing provides, so
422
+ * `start` names it: a unit forks per unit of work, so configuration read there
423
+ * would be parsed per request rather than once at boot. Inline rather than a
424
+ * named alias, so a consumer's declaration emit never has to name it.
422
425
  */
423
- type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
426
+ type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded | Env> | ([Extract<N, Env>] extends [never] ? never : PortInstance<"UNIT READS CONFIGURATION — Env is application-scoped: parse it in a root provider and inject the result", never>) : never;
424
427
  /** What a bound unit module exports — the port instances a piece of that kind may read. */
425
428
  type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
426
429
  /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
427
430
  type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
431
+ type UnprovidedBy<Unit, Declared> = [Unit] extends [never] ? NonNullable<Declared> : Unit extends unknown ? Exclude<NonNullable<Declared>, UnitExportsOf<Unit>> : never;
428
432
  /**
429
433
  * A piece injecting a port the bound unit module does not export. `unknown`
430
434
  * when every declared port is covered — including the empty case, so a worker
431
- * whose pieces declare no `unit:` is gated on nothing.
435
+ * whose pieces declare no `unit:` is gated on nothing. A union of modules is
436
+ * checked module by module.
432
437
  */
433
- type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
434
- readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
438
+ type UnitGate<Unit, Declared> = [UnprovidedBy<Unit, Declared>] extends [never] ? unknown : {
439
+ readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": UnprovidedBy<Unit, Declared>;
435
440
  };
436
441
  /**
437
442
  * One `(helpers, input)` implementation with its piece's declared record put on
@@ -942,8 +947,7 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
942
947
  *
943
948
  * @example
944
949
  * ```ts
945
- * // `OrderApi` imports the application next to `http()` and exports
946
- * // `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
950
+ * // `OrderApi` is an `HttpModule`, which exports `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
947
951
  * await runMain(OrderApi);
948
952
  * ```
949
953
  */
package/dist/index.d.mts CHANGED
@@ -418,20 +418,25 @@ type AnyUnitModule = Module<never, never, unknown>;
418
418
  * The needs a bound unit module still owes, or `never` when none is bound.
419
419
  * `Scope` is excluded, since nothing can ever provide it — the same exemption
420
420
  * `NeedsGate` itself carries — and so is `Seeded`, the port the runtime's own
421
- * fork seed discharges.
421
+ * fork seed discharges. `Env` is replaced by a refusal nothing provides, so
422
+ * `start` names it: a unit forks per unit of work, so configuration read there
423
+ * would be parsed per request rather than once at boot. Inline rather than a
424
+ * named alias, so a consumer's declaration emit never has to name it.
422
425
  */
423
- type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded> : never;
426
+ type UnitNeedsOf<Unit, Seeded = never> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | Seeded | Env> | ([Extract<N, Env>] extends [never] ? never : PortInstance<"UNIT READS CONFIGURATION — Env is application-scoped: parse it in a root provider and inject the result", never>) : never;
424
427
  /** What a bound unit module exports — the port instances a piece of that kind may read. */
425
428
  type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
426
429
  /** A piece's declared `unit:` record, as the services it reads off `context.unit`. */
427
430
  type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
431
+ type UnprovidedBy<Unit, Declared> = [Unit] extends [never] ? NonNullable<Declared> : Unit extends unknown ? Exclude<NonNullable<Declared>, UnitExportsOf<Unit>> : never;
428
432
  /**
429
433
  * A piece injecting a port the bound unit module does not export. `unknown`
430
434
  * when every declared port is covered — including the empty case, so a worker
431
- * whose pieces declare no `unit:` is gated on nothing.
435
+ * whose pieces declare no `unit:` is gated on nothing. A union of modules is
436
+ * checked module by module.
432
437
  */
433
- type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
434
- readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
438
+ type UnitGate<Unit, Declared> = [UnprovidedBy<Unit, Declared>] extends [never] ? unknown : {
439
+ readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": UnprovidedBy<Unit, Declared>;
435
440
  };
436
441
  /**
437
442
  * One `(helpers, input)` implementation with its piece's declared record put on
@@ -942,8 +947,7 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
942
947
  *
943
948
  * @example
944
949
  * ```ts
945
- * // `OrderApi` imports the application next to `http()` and exports
946
- * // `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
950
+ * // `OrderApi` is an `HttpModule`, which exports `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
947
951
  * await runMain(OrderApi);
948
952
  * ```
949
953
  */
package/dist/index.mjs CHANGED
@@ -454,6 +454,36 @@ const installUncaughtHandlers = (onUncaught) => {
454
454
  var UnitOverrides = class extends Port.many("UnitOverrides") {};
455
455
  /** Whether `module`'s tree carries a provider for `portId`. */
456
456
  const provides = (module, portId) => module.provides.some((provider) => provider.port.portId === portId) || module.imports.some((imported) => provides(imported, portId));
457
+ /** Every port `module`'s tree provides, by its class. */
458
+ const providedPorts = (module) => [...module.imports.flatMap(providedPorts), ...module.provides.map((provider) => provider.port)];
459
+ /**
460
+ * A check, per unit module, that it provides no port the application scope
461
+ * already provides. A fork builds its whole module tree per unit, so such a
462
+ * provider would shadow the application's instance with a fresh one per unit,
463
+ * released per unit — the application's configuration re-parsed, its clients
464
+ * reopened. A unit's member of the SAME set port is the one exemption, since
465
+ * the fork appends it to the application's members; a set port meeting an
466
+ * ordinary one, or two classes sharing an id, is refused like any other.
467
+ *
468
+ * Answers the refusal rather than throwing it, so the boot can throw it and a
469
+ * fork can reject with it. A module is walked once, however often it forks.
470
+ */
471
+ const rebuildGuard = (runtime, application) => {
472
+ const owned = new Map(providedPorts(application).map((port) => [port.portId, port]));
473
+ const clean = /* @__PURE__ */ new WeakSet();
474
+ return (module) => {
475
+ if (clean.has(module)) return void 0;
476
+ const rebuilt = providedPorts(module).find((port) => {
477
+ const owner = owned.get(port.portId);
478
+ return owner !== void 0 && !(owner === port && port.many === true);
479
+ });
480
+ if (rebuilt === void 0) {
481
+ clean.add(module);
482
+ return;
483
+ }
484
+ return /* @__PURE__ */ new Error(`[core] unit module "${module.name}" of runtime "${runtime}" provides port ${JSON.stringify(rebuilt.portId)}, which the application scope already provides — a unit would rebuild it per unit; remove it from the unit module and let the unit read the application's`);
485
+ };
486
+ };
457
487
  /**
458
488
  * Each overridden kind's module, mapped to the module the kernel forks in its
459
489
  * place. Keyed by module IDENTITY, because `UnitHost.fork` is handed the
@@ -803,6 +833,11 @@ const start = (module, options = {}) => {
803
833
  runtimeName = runtime.name;
804
834
  healthChecks = ctx.get(HealthChecks);
805
835
  const runtimeCtx = ctx;
836
+ const rebuilt = rebuildGuard(runtime.name, root);
837
+ for (const unit of Object.values(runtime.units ?? {})) {
838
+ const refusal = unit === void 0 ? void 0 : rebuilt(unit);
839
+ if (refusal !== void 0) throw refusal;
840
+ }
806
841
  const substitutes = unitSubstitutes(runtime.name, runtime.units ?? {}, ctx.get(UnitOverrides));
807
842
  const run = (meta, work) => registry.run(meta, (signal) => {
808
843
  const settled = Promise.withResolvers();
@@ -811,6 +846,8 @@ const start = (module, options = {}) => {
811
846
  const fork = (module, seed) => {
812
847
  if (hasSettled) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks after it has already settled")));
813
848
  if (closing !== void 0) return fromSafePromise(Promise.reject(/* @__PURE__ */ new Error("a unit forks its scope once")));
849
+ const refusal = rebuilt(module);
850
+ if (refusal !== void 0) return fromSafePromise(Promise.reject(refusal));
814
851
  const ready = Promise.withResolvers();
815
852
  closing = Module.forkScope(ctx, substitutes.get(module) ?? module, (forked) => {
816
853
  ready.resolve(forked);
@@ -947,8 +984,7 @@ const awaitExit = async (app, exit) => {
947
984
  *
948
985
  * @example
949
986
  * ```ts
950
- * // `OrderApi` imports the application next to `http()` and exports
951
- * // `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
987
+ * // `OrderApi` is an `HttpModule`, which exports `HttpRuntime`. `PORT`, `HOST` and `PROBE_PORT` are read inside the graph.
952
988
  * await runMain(OrderApi);
953
989
  * ```
954
990
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/core",
3
- "version": "0.32.0",
3
+ "version": "0.33.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",
@@ -45,8 +45,8 @@
45
45
  "./package.json": "./package.json"
46
46
  },
47
47
  "devDependencies": {
48
- "@btravstack/config": "0.32.0",
49
- "@btravstack/di": "0.32.0",
48
+ "@btravstack/config": "0.33.0",
49
+ "@btravstack/di": "0.33.0",
50
50
  "@btravstack/tsconfig": "0.4.0",
51
51
  "@stryker-mutator/core": "10.0.0",
52
52
  "@stryker-mutator/vitest-runner": "10.0.0",
@@ -60,8 +60,8 @@
60
60
  "vitest": "5.0.3"
61
61
  },
62
62
  "peerDependencies": {
63
- "@btravstack/config": "^0.32.0",
64
- "@btravstack/di": "^0.32.0",
63
+ "@btravstack/config": "^0.33.0",
64
+ "@btravstack/di": "^0.33.0",
65
65
  "unthrown": "^5.0.0"
66
66
  },
67
67
  "engines": {