Skip to content

Commit 7b61487

Browse files
committed
refactor(objectql): move checkDriversHealth and resolvePrimaryDatasource bodies to engine-lifecycle-health.ts
Cut 3 of the engine.ts split: the two lifecycle health bodies move unedited into a new module through LifecycleHealthHost (drivers and _registry as getters, isSystemLedgerObject and resolveDatasourceBinding as late-bound closures). The class keeps both methods with their signatures and docblocks. init stays (inferred return type). Claude-Session: https://claude.ai/code/session_0161WE4VX4T7VT66M6pYcVjD Co-authored-by: Claude <noreply@anthropic.com>
1 parent 24d350b commit 7b61487

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)