Skip to content

Commit 97277f1

Browse files
refactor(objectql): move the lifecycle health bodies out of engine.ts into engine-lifecycle-health.ts (#25522 cut 3) (#25686)
Part of #25522 Clause-②: no Metadata-protocol: none Ruling-ref: 6348891160 Cut 3 of the `packages/objectql/src/engine.ts` split (#24586 ruling A, recorded in `6270097840`). The claim is `6386147359`. Two public `ObjectQL` method bodies move unedited into a new module, `packages/objectql/src/engine-lifecycle-health.ts`: `checkDriversHealth` and `resolvePrimaryDatasource`. `init` stays: its return type is inferred. #25522 stays open for the later cuts. Branch `claude/issue-25522-objectql-engine-cut-3`, cut from `origin/main` at `24d350b45`. Since then `main` has moved five commits (to `b2909fd46`). None touches `packages/objectql/src/**`; #25657 changes `packages/objectql/package.json`'s zod range only. The branch has not been merged forward; CI builds the merge ref. **Serial with the file's in-flight PRs (A1).** At start, none of the 14 open PRs touched `packages/objectql/src/engine.ts` (every file list read through the REST API, the 183-file Version Packages PR #25640 page by page). Just before this PR was opened I measured again: 10 PRs are open, and none touches it (the two new ones, #25682 and #25684, read file by file; the other 8 unchanged). ## What moves, re-measured on `origin/main` `24d350b45` The cut-2 dev proposed this area in PR #25636's body. I measured it again before moving anything, and every premise holds: - **Both are public with explicit return types:** `checkDriversHealth(opts?: { timeoutMs?: number })`, which returns a `Promise` of `DriverHealth[]`, and `resolvePrimaryDatasource(): PrimaryDatasourceVerdict`. The stubs keep both signatures, so the `.d.ts` keeps its bytes (measured below). - **0 lines from the last 30 days.** `git blame --since=2026-09-24T16:00Z` over both methods, docblocks included (156 lines), finds 0 recent lines. Control on the same blame: the whole file has 8,293 recent lines. History was deepened to 2026-09-15 first (126 commits touch the file in 30 days, 35 in 7). - **No `this.FIELD.delete/insert/update(` site, and no static read.** `PLATFORM_PROVIDED_OBJECT_NAMES` is a module import from `@objectstack/spec/system`, not a class static. It has no other reader in `engine.ts`, so it leaves `engine.ts`'s import and the new module imports it. - **No `engine.ts#…` path reader** for either symbol anywhere in the repo (`packages/spec/liveness/**`, `scripts/**`, `docs/**`, `content/**`). - **No spy and no instance double on a real engine.** The two names appear in tests only as direct calls on a real engine (`engine-driver-health.test.ts`, `engine-primary-datasource.test.ts`, `engine-driver-eviction.test.ts`, `datasource-unavailable.test.ts`) or as whole-engine fakes (`packages/runtime/src/http-dispatcher.ready.test.ts`), which never reach `ObjectQL`. ## The shape (#23050 shape B, as cuts 1 and 2 apply it) - The class keeps each method with its name, signature, docblock and position. Each body is now one host literal plus `fn.call(host, …)`, marked `[#25522 cut 3]`. - `LifecycleHealthHost` is new. It types each member as the class member itself (`ObjectQL['drivers']`, `ObjectQL['_registry']`, `ObjectQL['isSystemLedgerObject']`, `ObjectQL['resolveDatasourceBinding']`). Each moved function types its `this` as the `Pick` of the host over exactly the members its body reads, and each stub annotates its literal as the `ThisParameterType` of the moved function. There is no cast, and no member becomes public or protected. - **Fields go as getters:** `drivers` (both bodies) and `_registry` (`resolvePrimaryDatasource`). Each read happens when the body makes it, so a driver registered or evicted later is seen as before. - **Methods go as late-bound closures:** `isSystemLedgerObject` and `resolveDatasourceBinding`. A double set on the instance is still the one the body calls. - `checkDriversHealth` stays `async` on both the stub and the moved function, so a throw inside the body is still a rejected promise. - **Imports.** The module imports `ObjectQL` as a type only, so there is no module cycle at run time. It imports `DriverHealth` and `PrimaryDatasourceVerdict` as types from `./driver-connect-errors.js` and `PLATFORM_PROVIDED_OBJECT_NAMES` from `@objectstack/spec/system`, the specifiers `engine.ts` uses. `engine.ts` gains one import and drops `PLATFORM_PROVIDED_OBJECT_NAMES` from its `@objectstack/spec/system` import, its only other reader being a backticked docblock mention. Nothing re-exports the module. ## Net lines (this seat's rule: an area moves only when `engine.ts` shrinks) | | base `24d350b45` | head | |---|---|---| | `engine.ts` (`wc -l`) | 23,807 | 23,755 (−52) | | `engine-lifecycle-health.ts` | — | 143 | Diff: +163 / −72 over 2 files, under the 3,000-line manual-merge line. ## The published `.d.ts` does not change (ruling `6348891160`) Base: the `@objectstack/objectql` dependency closure built at this branch (`pnpm --filter '@objectstack/objectql^...' build`, which the diff does not touch), then `pnpm --filter @objectstack/objectql build` with `engine.ts` restored from `24d350b45` (tree only) and the base `dist/` saved; the head build runs after `git checkout HEAD -- packages/objectql/src/engine.ts` with `git status` clean. The head `dist/index.mjs` carries `readRegistry` twice and the base copy 0 times, so each build is the one it claims to be. All six declaration files are **byte-identical**. The raw diff of each is 0 lines. The util chunk name, `util-ByiLm3B6`, is the same at both ends. | file | raw sha256, base = head | normalised (ruling, verbatim), base = head | normalised (single-quoted spelling), base = head | |---|---|---|---| | `index.d.ts` | `9ca22071717e3f761198115fa54a143df4151a52950d826a10677420dcad16ae` | `9ca22071717e…` | `c15106196eb2…` | | `index.d.mts` | `69dd1cfe3875024f434b2d7defce771ebc8c7e48a05be7c946fa6ecdbd912b40` | `69dd1cfe3875…` | `a2d6d7b241e8…` | | `core.d.ts` | `d27b72bde2eec9a47c7dd3c104976321533cf397452ca9f84491a455ec28e73b` | `d27b72bde2ee…` | `d27b72bde2ee…` | | `core.d.mts` | `5a0cca505296b678b31c742c285e028fe434f5467bb8d27e052e470ce0e09652` | `5a0cca505296…` | `5a0cca505296…` | | `util-ByiLm3B6.d.ts` and `.d.mts` | `1742a9cb029654d1ca4e8e392e98001eec85963987f516b1b76f672ecd08e5a9` | `1742a9cb0296…` | `bffd1b630dab…` | - Normaliser, the ruling's one-liner verbatim (double-quoted unions): `node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' FILE | sha256sum`. This package prints single-quoted literals, so on these files it changes nothing and normalised = raw. The third column is the same single class spelled with a single quote; it also matches. This cut does not depend on it, because the raw bytes are identical. - **Positive control**, in the same run, on a copy of the head `util-ByiLm3B6.d.ts`: line 8318 `private isSystemLedgerObject;` made public (`isSystemLedgerObject;`, raw diff 2 lines) gives raw = ruled-normalised `c944677ee7e7c23a9ecabb6466a616a9f993d3e41fbee3b854527e039c7d19cd`, which differs from `1742a9cb…`. The single-quote normalisation gives `e86dbb567ef0…`, which differs from `bffd1b630dab…`. - **Export lists**, loaded at run time from the built bundles (ESM `import()` and CJS `require`), are identical at base and head. `.` has 182 exports under ESM and 182 under CJS; `./core` has 54 and 54. The sorted lists hash `881616cc179189098182bd6b8a95701d9922f9677b786bc12e2d31c1adb0adaa` and `393e806906e876c2e020aa96617d54050a35ae63cd483b67d8e968b7b5390f49` at both ends (the same as cut 2's). No changeset: `skip-changeset`, per ruling `6348891160` item 4. ## Proof of move-only - **Bodies, by AST token** (recursive `getChildren` leaves of the TypeScript parse). For each moved function, the parameters (without the added `this`), the return type, the `async` modifier and the body have the same token sequence as the base method: `checkDriversHealth` 251 tokens, `resolvePrimaryDatasource` 218 tokens, all SAME. Every body line is the base line less two leading spaces (36 and 39 lines, braces included). **Negative control:** one changed token in the moved `resolvePrimaryDatasource` (`'system-object-unbound'` misspelled in memory) makes the same check report DIFFER. - **Class side.** The class has 317 members at base and at head. The 315 that did not move are byte-identical to base (full text, docblocks included). The 2 stubs keep their docblocks and signatures byte-identical. - **Top level of `engine.ts`.** 234 of the 235 base statements outside the class are byte-identical at head. The other is the `@objectstack/spec/system` import less `PLATFORM_PROVIDED_OBJECT_NAMES`. The one new statement is the import of the new module. - **The tests reach the moved bodies** through the stubs: `engine-driver-health.test.ts` (7 cases on `checkDriversHealth`, timeout and skip paths included), `engine-primary-datasource.test.ts` (every `resolvePrimaryDatasource` verdict), `engine-driver-eviction.test.ts` and `datasource-unavailable.test.ts`. All pass at head (below). ## Readers of the moved lines - **ADR anchors (A2).** `scripts/adr-anchors/packages__objectql__src__engine.ts.json`'s one paragraph is about the transaction seam (`buildDriverOptions`, `enforceTransactionOrigin`), neither of which moves. The ADR-0057 §3.6 mention in `resolvePrimaryDatasource`'s docblock stays on the stub; the one-line `// ADR-0057 §3.6` comment inside the body moves with it. No anchor entry describes a moved body, so none is added. `check:adr-anchors` exit 0 at both ends: 85 anchored files; **1,847 decision-letter citations on the anchored surface at base and at head**; 52,546 citations at both ends (6,610 files at base, 6,611 at head: the new module). - **Liveness ledgers.** `pnpm --filter @objectstack/spec check:liveness` exits 0 at head. No ledger points at either moved symbol. - **Other path readers.** `check-adr-symbol-anchors`: 2,384 anchors resolve at head. `check-platform-checklist --changed-from origin/main`: 1 anchored file in the change set, 14 anchors judged, 0 lost. ## Direct dependants (A3) The moved members are reached from `@objectstack/runtime` (`http-dispatcher.ts`'s `/ready` handler calls both) and named in `@objectstack/service-datasource`'s comments. I built their dependency closures (`pnpm --filter '@objectstack/runtime^...' --filter '@objectstack/service-datasource^...' build`, under `os-verify-lock`, VERDICT command-exit 0) and ran `pnpm --filter @objectstack/runtime --filter @objectstack/service-datasource typecheck` (VERDICT command-exit 0): `service-datasource` `tsc --noEmit` done; `runtime` `tsc --noEmit` and `check:test-typecheck` OK; 0 `error TS` lines. Both read the published declarations, which are byte-identical above. ## For the next dispatch (cut 4) - **Proposed area: Realtime publish** (the tail of the "Write guard and realtime publish" row): `publishDataEvent` (58-line body) and `publishBulkDataEvent` (55-line body). - Both private with explicit return types; no `this.FIELD.delete/insert/update(` site; no static read; no `engine.ts#…` path reader; no `vi.spyOn` or instance assignment on either, measured repo-wide. - 18 lines from the last 30 days (16 in `publishDataEvent`'s range, 2 in `publishBulkDataEvent`'s). - Host: three getters only (`realtimeService`, `logger`, `_registry`). - **Priced against the net-line rule:** 113 body lines out, about 2 × 7 stub lines and 3 import lines in, so `engine.ts` drops by roughly 95 lines. - **Stays from that row:** `assertWriteAllowed`. `packages/spec/liveness/datasource.json` cites `packages/objectql/src/engine.ts#assertWriteAllowed` as evidence, the same reason `resolveDatasourceBinding` stayed in cut 2. `setRealtimeService` and `listUnavailableDatasources` are 1–2-line bodies, shorter than a host literal. - **Still waiting:** Roll-up summaries (spec-coupled, see cut 1); the Datasource registry (U = 3); Related-record expansion (`expandRelatedRecords` reads the private static `MAX_EXPAND_DEPTH`). ## Verification All at head `7b61487ac`. - `pnpm --filter @objectstack/objectql typecheck` (`tsc --noEmit` and `check:test-typecheck`), under `os-verify-lock`: VERDICT command-exit 0; 39 files / 233 errors / 64 pinned signatures held. - `packages/objectql` suite (`vitest run --project local --maxWorkers=2`, under `os-verify-lock`, VERDICT command-exit 0): 466 files, 1,899 suites, 8,700 tests, 8,700 passed, 0 failed, 0 skipped. - `eslint --no-inline-config packages/objectql`: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 56 families on the real diff at `7b61487ac` (stderr names the repo and the commit). All 56 were run, each exit code recorded before any pipe. 53 exit 0 on the first pass, and `check-engine-split-ratio --days 90` exit 0 after the history deepening it names as its remedy (`git fetch --shallow-since=2026-07-19`; it had refused with exit 2, CANNOT COMPUTE; ratio 98.1%). `--ran` reconciles: 56 accounted for, 54 run, 2 NOT MEASURED. CI runs both: - `check-plugin-teardown-shape --self-test` exit 3: its pinned fixture commit is not in this shallow clone. - `check:dual-build-cjs-loads` exit 3, PREREQUISITE NOT MET: 54 export entries of other packages have no `dist/` (9 listed, "… 45 more"). The export lists above answer its question for this package. - Hand gates, all exit 0: `check-adr-anchors` (base and head), `check-adr-symbol-anchors`, `check-platform-checklist --changed-from origin/main`, `pnpm --filter @objectstack/spec check:liveness`. - `test:repo` was not run locally; CI's `Test Core` runs it. --- _Generated by [Claude Code](https://claude.ai/code/session_0161WE4VX4T7VT66M6pYcVjD)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5cf1b9d commit 97277f1

2 files changed

Lines changed: 163 additions & 72 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25522 cut 3] The lifecycle health bodies of `ObjectQL` —
5+
* `checkDriversHealth` (ping every registered driver, each check bounded by a
6+
* timeout: framework#3756) and `resolvePrimaryDatasource` (which datasource
7+
* holds this deployment's platform system objects, the #13408 criterion) —
8+
* moved out of `engine.ts` whole.
9+
*
10+
* A MOVE, not a redesign. Each function below is the body of the `ObjectQL`
11+
* method of the same name as it stood, re-indented one level and otherwise
12+
* unedited: the same default timeout and unref'd timer, the same healthy
13+
* verdict for a driver with no `checkHealth()`, the same voters and the same
14+
* four refusals.
15+
*
16+
* What stays in the class: `init`, whose return type is inferred, so moving
17+
* its body would re-infer the published `.d.ts` through this module.
18+
*
19+
* ## How the moved bodies reach the class
20+
*
21+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
22+
* header carries the full reasoning), applied per method as
23+
* `engine-datasource-routing.ts` applies it. The class keeps every method —
24+
* the published `.d.ts` lists each one's name, signature, docblock and
25+
* position — and each body is one literal of exactly the
26+
* {@link LifecycleHealthHost} members that method reads, plus a `.call`. A
27+
* method is handed over as a closure that calls it on the engine at call
28+
* time, so a test double or spy set on the instance is still the one the body
29+
* reaches: `resolvePrimaryDatasource` reaches `isSystemLedgerObject` and
30+
* `resolveDatasourceBinding` through the engine, exactly as before.
31+
*
32+
* The fields the bodies read (`drivers`, `_registry`) go as GETTERS, so each
33+
* read happens when the body makes it: `registerDriver` and the eviction path
34+
* change the driver map, and a body sees whatever is installed at the moment
35+
* it reads, as it did.
36+
*
37+
* This module imports the engine only as a TYPE, so there is no module cycle
38+
* at run time, and nothing re-exports it, so the published surface does not
39+
* move.
40+
*/
41+
42+
import { PLATFORM_PROVIDED_OBJECT_NAMES } from '@objectstack/spec/system';
43+
import type { ObjectQL } from './engine.js';
44+
import type { DriverHealth, PrimaryDatasourceVerdict } from './driver-connect-errors.js';
45+
46+
/**
47+
* The `ObjectQL` members the functions below read — every one of them, and no
48+
* other; each function's `this` is the `Pick` of the members IT reads. Each is
49+
* typed AS the class member (an indexed access on the class), so a change to a
50+
* member's signature on the class is a change here too, and naming a member
51+
* the class does not have is a compile error. See the module header for which
52+
* members are getters.
53+
*/
54+
export interface LifecycleHealthHost {
55+
readonly drivers: ObjectQL['drivers'];
56+
readonly _registry: ObjectQL['_registry'];
57+
isSystemLedgerObject: ObjectQL['isSystemLedgerObject'];
58+
resolveDatasourceBinding: ObjectQL['resolveDatasourceBinding'];
59+
}
60+
61+
/** The body of `ObjectQL#checkDriversHealth`; the method's docblock there is its contract. */
62+
export async function checkDriversHealth(
63+
this: Pick<LifecycleHealthHost, 'drivers'>,
64+
opts?: { timeoutMs?: number },
65+
): Promise<DriverHealth[]> {
66+
const timeoutMs = opts?.timeoutMs ?? 2_000;
67+
68+
return Promise.all(
69+
Array.from(this.drivers, async ([driverName, driver]): Promise<DriverHealth> => {
70+
if (typeof driver.checkHealth !== 'function') {
71+
return { driverName, healthy: true, skipped: true };
72+
}
73+
let timer: ReturnType<typeof setTimeout> | undefined;
74+
try {
75+
const healthy = await Promise.race([
76+
Promise.resolve(driver.checkHealth()),
77+
new Promise<never>((_, reject) => {
78+
timer = setTimeout(
79+
() => reject(new Error(`checkHealth did not settle within ${timeoutMs}ms`)),
80+
timeoutMs,
81+
);
82+
// Never hold the event loop open for a probe.
83+
(timer as { unref?: () => void }).unref?.();
84+
}),
85+
]);
86+
return healthy
87+
? { driverName, healthy: true }
88+
: { driverName, healthy: false, error: 'checkHealth() returned false' };
89+
} catch (e) {
90+
return {
91+
driverName,
92+
healthy: false,
93+
error: e instanceof Error ? e.message : String(e),
94+
};
95+
} finally {
96+
if (timer) clearTimeout(timer);
97+
}
98+
}),
99+
);
100+
}
101+
102+
/** The body of `ObjectQL#resolvePrimaryDatasource`; the method's docblock there is its contract. */
103+
export function resolvePrimaryDatasource(
104+
this: Pick<LifecycleHealthHost, '_registry' | 'isSystemLedgerObject' | 'resolveDatasourceBinding' | 'drivers'>,
105+
): PrimaryDatasourceVerdict {
106+
const byDatasource = new Map<string, number>();
107+
for (const objectName of PLATFORM_PROVIDED_OBJECT_NAMES) {
108+
// Not registered in THIS deployment (OSS runtime vs cloud, plugins not
109+
// installed) — no evidence either way, so it does not vote.
110+
if (!this._registry.getObject(objectName)) continue;
111+
// ADR-0057 §3.6: deliberately off the primary. See the header.
112+
if (this.isSystemLedgerObject(objectName)) continue;
113+
114+
const binding = this.resolveDatasourceBinding(objectName);
115+
// A registered platform object that routes NOWHERE is a broken
116+
// deployment, not a quiet abstention: there is no binding and no default
117+
// driver, so no answer about "the primary" can be true.
118+
if (!binding) return { resolved: false, reason: 'system-object-unbound' };
119+
120+
byDatasource.set(binding.datasource, (byDatasource.get(binding.datasource) ?? 0) + 1);
121+
}
122+
123+
if (byDatasource.size === 0) return { resolved: false, reason: 'no-system-objects-registered' };
124+
if (byDatasource.size > 1) {
125+
return {
126+
resolved: false,
127+
reason: 'system-objects-split',
128+
candidates: [...byDatasource.keys()].sort(),
129+
};
130+
}
131+
132+
const [[datasource, witnesses]] = [...byDatasource];
133+
// Steps 1-2 of the resolution order answer with a name even when nothing is
134+
// registered under it (that is deliberate — `getDriver` throws loudly on
135+
// it). A readiness probe cannot use such a name: `checkDriversHealth()`
136+
// reports only REGISTERED drivers, so an unregistered primary would be
137+
// absent from the unhealthy list and read as healthy — the silent
138+
// don't-drain the ruling forbids.
139+
if (!this.drivers.has(datasource)) {
140+
return { resolved: false, reason: 'no-driver-registered', candidates: [datasource] };
141+
}
142+
return { resolved: true, datasource, witnesses };
143+
}

‎packages/objectql/src/engine.ts‎

Lines changed: 20 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ import { negotiatedMessageLocale, objectDisplayLabel, validationMessageContext }
105105
// [#25522 cut 2] The datasource-routing bodies; the methods of the same names
106106
// below hand each body the members it reads (`DatasourceRoutingHost`).
107107
import { getDriver, matchPattern, resolveDatasourceFromMapping } from './engine-datasource-routing.js';
108+
// [#25522 cut 3] The lifecycle health bodies; the methods of the same names
109+
// below hand each body the members it reads (`LifecycleHealthHost`).
110+
import { checkDriversHealth, resolvePrimaryDatasource } from './engine-lifecycle-health.js';
108111
import { ExecutionContext, ExecutionContextSchema, type InstalledPackageSource } from '@objectstack/spec/kernel';
109112
// [#21120] The family-wide stored-metadata-body seam. A `data.record.*` event
110113
// carries the written row in `after` (and the input patch in `changes`), so a
@@ -250,7 +253,7 @@ import {
250253
export type InsertManyRowOutcome =
251254
| { ok: true; record: any; droppedFields?: DroppedFieldsEvent[] }
252255
| { ok: false; error: unknown };
253-
import { CoreServiceName, StorageNameMapping, PLATFORM_PROVIDED_OBJECT_NAMES } from '@objectstack/spec/system';
256+
import { CoreServiceName, StorageNameMapping } from '@objectstack/spec/system';
254257
import { IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';
255258
import {
256259
BulkDataEventSchema,
@@ -12662,40 +12665,12 @@ export class ObjectQL implements IObjectQLEngine {
1266212665
* optional method.
1266312666
*/
1266412667
async checkDriversHealth(opts?: { timeoutMs?: number }): Promise<DriverHealth[]> {
12665-
const timeoutMs = opts?.timeoutMs ?? 2_000;
12666-
12667-
return Promise.all(
12668-
Array.from(this.drivers, async ([driverName, driver]): Promise<DriverHealth> => {
12669-
if (typeof driver.checkHealth !== 'function') {
12670-
return { driverName, healthy: true, skipped: true };
12671-
}
12672-
let timer: ReturnType<typeof setTimeout> | undefined;
12673-
try {
12674-
const healthy = await Promise.race([
12675-
Promise.resolve(driver.checkHealth()),
12676-
new Promise<never>((_, reject) => {
12677-
timer = setTimeout(
12678-
() => reject(new Error(`checkHealth did not settle within ${timeoutMs}ms`)),
12679-
timeoutMs,
12680-
);
12681-
// Never hold the event loop open for a probe.
12682-
(timer as { unref?: () => void }).unref?.();
12683-
}),
12684-
]);
12685-
return healthy
12686-
? { driverName, healthy: true }
12687-
: { driverName, healthy: false, error: 'checkHealth() returned false' };
12688-
} catch (e) {
12689-
return {
12690-
driverName,
12691-
healthy: false,
12692-
error: e instanceof Error ? e.message : String(e),
12693-
};
12694-
} finally {
12695-
if (timer) clearTimeout(timer);
12696-
}
12697-
}),
12698-
);
12668+
// [#25522 cut 3] The body moved to `engine-lifecycle-health.ts` (see `LifecycleHealthHost`).
12669+
const readDrivers = () => this.drivers;
12670+
const host: ThisParameterType<typeof checkDriversHealth> = {
12671+
get drivers() { return readDrivers(); },
12672+
};
12673+
return checkDriversHealth.call(host, opts);
1269912674
}
1270012675

1270112676
/**
@@ -12754,43 +12729,16 @@ export class ObjectQL implements IObjectQLEngine {
1275412729
* readiness caller catches it into the same drain direction.)
1275512730
*/
1275612731
resolvePrimaryDatasource(): PrimaryDatasourceVerdict {
12757-
const byDatasource = new Map<string, number>();
12758-
for (const objectName of PLATFORM_PROVIDED_OBJECT_NAMES) {
12759-
// Not registered in THIS deployment (OSS runtime vs cloud, plugins not
12760-
// installed) — no evidence either way, so it does not vote.
12761-
if (!this._registry.getObject(objectName)) continue;
12762-
// ADR-0057 §3.6: deliberately off the primary. See the header.
12763-
if (this.isSystemLedgerObject(objectName)) continue;
12764-
12765-
const binding = this.resolveDatasourceBinding(objectName);
12766-
// A registered platform object that routes NOWHERE is a broken
12767-
// deployment, not a quiet abstention: there is no binding and no default
12768-
// driver, so no answer about "the primary" can be true.
12769-
if (!binding) return { resolved: false, reason: 'system-object-unbound' };
12770-
12771-
byDatasource.set(binding.datasource, (byDatasource.get(binding.datasource) ?? 0) + 1);
12772-
}
12773-
12774-
if (byDatasource.size === 0) return { resolved: false, reason: 'no-system-objects-registered' };
12775-
if (byDatasource.size > 1) {
12776-
return {
12777-
resolved: false,
12778-
reason: 'system-objects-split',
12779-
candidates: [...byDatasource.keys()].sort(),
12780-
};
12781-
}
12782-
12783-
const [[datasource, witnesses]] = [...byDatasource];
12784-
// Steps 1-2 of the resolution order answer with a name even when nothing is
12785-
// registered under it (that is deliberate — `getDriver` throws loudly on
12786-
// it). A readiness probe cannot use such a name: `checkDriversHealth()`
12787-
// reports only REGISTERED drivers, so an unregistered primary would be
12788-
// absent from the unhealthy list and read as healthy — the silent
12789-
// don't-drain the ruling forbids.
12790-
if (!this.drivers.has(datasource)) {
12791-
return { resolved: false, reason: 'no-driver-registered', candidates: [datasource] };
12792-
}
12793-
return { resolved: true, datasource, witnesses };
12732+
// [#25522 cut 3] The body moved to `engine-lifecycle-health.ts` (see `LifecycleHealthHost`).
12733+
const readRegistry = () => this._registry;
12734+
const readDrivers = () => this.drivers;
12735+
const host: ThisParameterType<typeof resolvePrimaryDatasource> = {
12736+
get _registry() { return readRegistry(); },
12737+
isSystemLedgerObject: (...a) => this.isSystemLedgerObject(...a),
12738+
resolveDatasourceBinding: (...a) => this.resolveDatasourceBinding(...a),
12739+
get drivers() { return readDrivers(); },
12740+
};
12741+
return resolvePrimaryDatasource.call(host);
1279412742
}
1279512743

1279612744
/**

0 commit comments

Comments
 (0)