Skip to content

Commit 9a7838e

Browse files
perf(core): read the Default Organization once per engine, not on every authenticated request (#24627)
Fixes #24523 Clause-②: no (a per-engine memo of an unchanged read: no accept set, standing verdict or served body moves; owes the seat's review of record before the queue, at `CONTRACT_REVIEW_TIER` because it is the platform-standing anchor) Dispatched by the `domain:engine` seat 1 PM (`session_01JtSQQ3EWsbNPzyYMug1tXy`, claim 6271076320). Dev session: `session_01Ug7sGD6qWVrzQNdihVX3T6`. ⛔ **Landing waits** on the `epic:#15194` seat's answer to the territory declaration 6271107528, or an objection-free read of that thread, and on the seat's `CONTRACT_REVIEW_TIER` review of record. No answer was posted when this PR opened. ## What changed `resolveUserAuthzGrants` asks for the Default Organization whenever a principal holds an organization-owned `admin_full_access` grant under `single`. `readDefaultOrganizationId` answers with one `sys_organization` read by slug, plus a second read when the only organization is not slugged `default`. Nothing carried that answer across requests, so every such request paid one or two reads. This PR keeps the answer per engine, across requests (`memoisedDefaultOrganizationId`, `packages/core/src/security/resolve-authz-context.ts`). The call site in `resolveUserAuthzGrants` is the only caller changed. - **In-process retirement:** any `sys_organization` write through the engine (insert, update, delete, a slug move included) retires the answer. The write is observed after it settles, by #10757 leg 5's write seam: a per-engine `WeakMap`, a watched-object set (`sys_organization`), and a generation read before the reads and checked on the next hit. A write that lands while the read is in flight leaves that answer dead on arrival. - **Across replicas:** a peer's `sys_organization` write arrives only when the `authz.invalidated` bridge is attached, which needs the grants cache on (`OS_AUTHZ_GRANTS_CACHE_TTL_MS` above 0) and a cross-process cluster driver. It arrives as a `remote` epoch bump. That bump, and every other non-`write` bump (`metadata`, which is plugin-security's local declaration bump, and `manual`), retires the answer. On the shipped default, no bump carries a peer's write. - **Residual bound, 30 s** (`DEFAULT_ORGANIZATION_MEMO_TTL_MS`, a constant, no new knob). It is never the in-process rule. It covers only what no seam can see: - a peer replica's write when no bridge is attached (#21868's delete-and-recreate on another replica). On the shipped default that is every peer's write, so the 30 s bound is its only cover; - a write that started before this engine's observer registered (measured below); - a watched write inside an engine transaction, whose statement completes before its commit (leg 5's window, named in the code). - **An outage is not an answer.** A read that throws (`AuthzStoreUnavailableError`, `SERVICE_UNAVAILABLE` / 503) memoises nothing and propagates. The next request reads again. - **`undefined` is memoised.** No organization, or several with none slugged `default`, is a real answer, and a write retires it like any other. A table that was never provisioned reads as zero organizations, which is the true answer there. - **Declines, reading every time as before**, in two cases: - an engine without the write epoch and `registerMiddleware` (every recording double); - a registry that does not hold `sys_organization`. "Not registered" is a verdict the boot can still contradict, and registering the object writes no row the seam would see. - **Which way a stale answer fails.** A stale id withholds standing from the new Default Organization's grant. It also keeps granting standing to the old organization's grant while that row survives. No design at zero reads per request avoids the granting direction, so every window in which the answer can be stale is capped by the residual bound. The in-process slug-move pins assert the granting direction is retired. **One copy of the seam.** `localizationWriteState`'s body is now `watchedWriteState(ql, states, watched)`. Leg 5's localization cache and this memo each call it with their own `WeakMap` and watched set. The localization behaviour is unchanged, and its suite (`resolve-localization-cache.test.ts`) passes unedited. This touches leg 5's function, which is outside the claim's file-surface list but in the same file. Two copies of the seam would be the drift shape. ⛔ **Byte-identical:** `readDefaultOrganizationId` (both reads, filters and limits), `grantAnchorsPlatformStanding`, `grantOrganizationOf` and `grantAppliesInTenant` are not edited. The diff touches none of their lines. Unit pins assert the two reads byte-for-byte wherever they are issued. ## Measurement (M, a booted composition) The composition is `default-organization-boot-invariant.pin.test.ts`'s plugin set with `objectstack dev`'s seeded administrator, on sqlite-wasm `:memory:`. The administrator's first-user promotion grant is owned by the Default Organization. Each count is the number of statements the SQL driver issued inside the request's own async context (knex `query` events), warm (after two requests). | warm request | `main` `5a871e4e41` | this branch | |:--|--:|--:| | `get-session`, organization-owned grant, slug `default` | 9 | **8** | | data read (`GET /data/pin_note?$top=1`), organization-owned, slug `default` | 11 | **10** | | `get-session`, organization-owned, sole organization slugged `acme` | 10 | **8** | | data read, organization-owned, `acme` | 12 | **10** | | `get-session`, organization-less grant (either slug) | 8 | 8 | | data read, organization-less grant (either slug) | 10 | 10 | The `sys_organization` statements per warm request go from 1 (slug `default`) or 2 (`acme`) to 0. After a `sys_organization` write, the next request reads it once. **Controls, byte-identical:** the resolved authz envelope (positions, permissions, `org_user_ids`, `accessible_org_ids`, posture, tab permissions) on a booted composition. Three personas: the administrator, a member, and a user with no organization. Each was resolved with no tenant and in the Default Organization, twice each, for 12 envelopes. The run used the resolver at `BASE` (`5a871e4e41`, blob `4029c98d…`) and at `HEAD` (blob `a4c2ec74…`), with random ids normalized. All 12 are deep-equal: canonical md5 `3fd8be5122375762cf9d49f28bae84e8` on both. The raw byte difference is only key order, from per-boot random user ids. This was a one-off scratch file, deleted, and the restore was checked blob-equal to `HEAD`. ## Pins - `packages/runtime/src/default-organization-statements.golden.test.ts` (new, 6 cases, a real kernel): - the statements golden (8 / 10, with no `sys_organization` statement, for both slugs); - the organization-less control at the same counts; - the Default Organization deleted and recreated, and the next `get-session` reads the new id (#21868); - `default` moved to another organization: the old organization's grant stops anchoring, and the new one's starts. - `packages/core/src/security/resolve-authz-context.default-organization-memo.test.ts` (new, 14 cases). Each case drives `resolveUserAuthzGrants` on a recording double that carries the engine's seams. Covered: - one read, then none (both slugs); - `undefined` memoised; - the no-seam control; - delete-and-recreate; - the slug move, granting direction; - unrelated writes do not retire it, a watched grants table included; - `remote` and `metadata` retire it, a bare `write` bump does not; - in-flight write: dead on arrival; - the TTL bound; - the outage (envelope `SERVICE_UNAVAILABLE` / 503, then a re-read); - the registry case, re-read once the object is registered; - the pre-registration window, measured. - **Pre-registration window, measured:** a write that starts before the observer registers is not seen. The first resolution registers the observer and reads the pre-write row. That answer stands, stale, until the 30 s bound, and is re-read after it. This is the same blind spot leg 5 documents. At boot, plugin-auth's invariant writes the Default Organization before the listener accepts, so a request racing that write is the only way to reach the window. ## Ablations (each through `scripts/ablation-replace.mjs`, restore proven blob == `HEAD`, `git diff HEAD` empty) Runtime resolves `@objectstack/core` to source through its vitest alias, and core's tests import it relatively, so no `dist/` leg applies. The alias was also observed directly: the counts moved with no core rebuild. 1. **Memo removed** (the call site back to `readDefaultOrganizationId`): golden red, 4 of 6. It reads 9 / 11 with 1 organization statement, and 10 / 12 with 2. Both retirement cases go red on `organizationReads: 0`. 2. **Write-seam retirement removed** (an empty watched set for the memo): golden red, 2 of 6. Delete-and-recreate reads `standing: false, organizationReads: 0`, and so does the slug move. Unit suite red, 4 of 14. 3. **A failed read memoised:** - variant `.catch(() => undefined)`: the outage pin is red (`expected undefined to match { code: 'SERVICE_UNAVAILABLE', status: 503 }`); - variant "memoise, then rethrow": red on the re-read (`reads: []`, `standing: false`). ## Verification (final head `307260452`, `origin/main` `c03b17d78` merged) - `pnpm --filter @objectstack/core test`: 107 files, 2594 tests passed. - `pnpm --filter @objectstack/core test:repo`: 6 files, 82 tests passed. No new caller of the reference-target census. - `pnpm --filter @objectstack/plugin-security test`: 188 files, 3754 passed, 45 skipped. - `pnpm --filter @objectstack/plugin-auth test`: 165 files, 3165 passed, 10 skipped. - `pnpm --filter @objectstack/rest test`: 300 files, 6095 passed, 326 skipped. - `pnpm --filter @objectstack/runtime test`: 423 files, 6651 passed, 19 skipped. - `pnpm --filter @objectstack/core typecheck` exit 0 and `pnpm --filter @objectstack/runtime typecheck` exit 0. Both test layers compile under `tsconfig.test.json` with no new debt. - After the merge: build of core, runtime, plugin-auth and plugin-email (35 turbo tasks) exit 0; both typechecks exit 0; the two new pin files and the leg 4/5 suites (request-rounds, batch-equivalence, localization cache) pass. - Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths, at `307260452`. All 67 were run, each exit 0, captured before any pipe. `--ran` reports: "67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). `check:dual-build-cjs-loads` exited 3 on the first pass, before a full build. After `turbo run build` (76 tasks) it exits 0. ## Acceptance notes - **Pin sweep.** Searched for `readDefaultOrganizationId`, the `{ slug: 'default' }` read, `grantAnchorsPlatformStanding`, and the `*admin-standing*`, `*batch-equivalence*`, `*request-rounds*` and `*default-organization*` tests. None moves: their doubles carry no write seam, so the memo declines and their read multisets hold. `resolve-authz-context.request-grants-memo.test.ts` and `resolve-user-grants-cache.test.ts` carry the seams but hold no organization-owned admin grant, and both pass unedited. - **plugin-auth's own `readDefaultOrganizationId`** (`last-admin-guard.ts`) is untouched. It runs on guarded writes (`anchorOwnerTest`), not on every authenticated request, so it does not pay this per-request cost. - **The golden boots a real kernel:** test body about 3 s; most of the wall is module import on the shared box. - The PR body is written once at creation. Any later change is named in the report for the seat to write. --- _Generated by [Claude Code](https://claude.ai/code/session_01Ug7sGD6qWVrzQNdihVX3T6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b61353f commit 9a7838e

4 files changed

Lines changed: 673 additions & 20 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
perf(core): an administrator whose `admin_full_access` grant belongs to the Default Organization no longer pays a `sys_organization` read on every authenticated request
6+
7+
Clause-②: no (a per-engine memo of an unchanged read: no accept set, standing verdict or served body moves; owes the seat's review of record before the queue, at `CONTRACT_REVIEW_TIER` because it is the platform-standing anchor)
8+
9+
- **The Default Organization is read once per engine, not once per request.** Under `single`, `resolveUserAuthzGrants` asks for the Default Organization whenever the principal holds an organization-owned `admin_full_access` grant: one `sys_organization` read, or two when the only organization is not slugged `default`. The answer is now kept per engine across requests. On a booted composition the warm `get-session` costs 8 statements and a data read 10, the same as for an organization-less grant. Before, it cost 9 and 11, or 10 and 12.
10+
- **What retires it.** Any `sys_organization` write through the engine (insert, update, delete), observed after it settles. With the `authz.invalidated` bridge attached (the grants cache on, `OS_AUTHZ_GRANTS_CACHE_TTL_MS` above 0, and a cross-process cluster driver), a peer replica's write arrives as a `remote` epoch bump and retires it; every other non-`write` bump (`metadata`, `manual`) retires it too. A 30-second bound covers what no seam sees: a peer's write with no bridge attached, which on the shipped default (the grants cache off) is every peer's write, so there the bound is the only cover for it; a write that started before the engine's observer registered; and a write inside a transaction that has not committed yet. A Default Organization that is deleted and recreated, or a `default` slug moved to another organization, is read again on the next request.
11+
- **A failed read is not kept.** A read that fails with the authorization store unavailable propagates as before, and the next request reads again. "No Default Organization" (no organization, or several and none `default`) is a real answer and is kept.
12+
- **Unchanged:** the two reads, their filters and limits, and the platform-standing predicate the answer feeds. An engine without the write epoch and `registerMiddleware`, or a registry that does not hold `sys_organization`, keeps reading every time.
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#24523] The Default Organization answer is memoised per engine across
5+
* requests, and retired by a `sys_organization` write through the engine's
6+
* write seam, by a non-`write` epoch bump, and by the residual TTL.
7+
*
8+
* Every case drives `resolveUserAuthzGrants` — the one caller — on a recording
9+
* double that carries the two engine seams the memo requires, shaped as the
10+
* engine's own (the same shape `resolve-localization-cache.test.ts` drives the
11+
* leg-5 seam with): the write epoch, bumped with `'write'` when a write STARTS,
12+
* and a middleware chain each write SNAPSHOTS when it starts.
13+
*
14+
* What each case reads: the `sys_organization` reads a resolution issued
15+
* (zero on a memo hit), and the platform standing the answer feeds. ⛔ The
16+
* reads themselves — `{ slug: 'default' }` limit 1, then `{}` limit 2 — are
17+
* asserted byte-for-byte wherever they are issued: only how often they run
18+
* moves.
19+
*/
20+
21+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
22+
import { ADMIN_FULL_ACCESS } from '@objectstack/spec';
23+
24+
import { resetPlatformAdminEmailMemo } from './platform-admin.js';
25+
import { resolveUserAuthzGrants } from './resolve-authz-context.js';
26+
import { makeRecordingQl } from './__tests__/resolve-authz-context.batch-equivalence.testkit.js';
27+
28+
const NOW = Date.parse('2026-10-19T00:00:00.000Z');
29+
const ENVS = ['OS_TENANCY_POSTURE', 'OS_MULTI_ORG_ENABLED', 'OS_PLATFORM_OWNER_EMAIL', 'OS_AUTHZ_GRANTS_CACHE_TTL_MS'] as const;
30+
31+
let ambient: Record<string, string | undefined> = {};
32+
beforeEach(() => {
33+
ambient = Object.fromEntries(ENVS.map((k) => [k, process.env[k]]));
34+
// `single`, no configured administrator, the grants cache at its default (off).
35+
for (const k of ENVS) delete process.env[k];
36+
resetPlatformAdminEmailMemo();
37+
});
38+
afterEach(() => {
39+
vi.restoreAllMocks();
40+
for (const k of ENVS) {
41+
if (ambient[k] === undefined) delete process.env[k];
42+
else process.env[k] = ambient[k];
43+
}
44+
resetPlatformAdminEmailMemo();
45+
});
46+
47+
type Middleware = (ctx: { object: string; operation: string }, next: () => Promise<void>) => Promise<void>;
48+
49+
const BY_SLUG = { object: 'sys_organization', where: { slug: 'default' }, limit: 1, isSystem: true };
50+
const SOLE = { object: 'sys_organization', where: {}, limit: 2, isSystem: true };
51+
52+
/**
53+
* The administrator's tables: one `admin_full_access` grant owned by
54+
* `grantOrganization`, and the organizations given.
55+
*/
56+
function tablesOf(grantOrganization: string, organizations: Array<Record<string, unknown>>) {
57+
return {
58+
sys_user: [{ id: 'usr_1', email: 'admin@example.com' }],
59+
sys_member: [],
60+
sys_user_position: [],
61+
sys_user_permission_set: [
62+
{ id: 'ups_1', user_id: 'usr_1', permission_set: ADMIN_FULL_ACCESS, organization_id: grantOrganization },
63+
],
64+
sys_permission_set: [{ id: 'pst_1', name: ADMIN_FULL_ACCESS, active: true }],
65+
sys_organization: organizations,
66+
} as Record<string, Array<Record<string, unknown>>>;
67+
}
68+
69+
/**
70+
* {@link makeRecordingQl} with the engine's write seams. `write(object,
71+
* operation, apply)` is one engine write: the epoch first, then the chain as
72+
* it stood when the write started, `apply` as its driver step. `pause` holds a
73+
* write between its start and its driver step, for the in-flight cases.
74+
*/
75+
function makeEngineQl(tables: Record<string, Array<Record<string, unknown>>>, seams = true) {
76+
const ql = makeRecordingQl(tables) as ReturnType<typeof makeRecordingQl> & Record<string, any>;
77+
const listeners = new Set<(epoch: number, reason: string) => void>();
78+
const middlewares: Middleware[] = [];
79+
let epoch = 0;
80+
const writeEpoch = {
81+
get current() {
82+
return epoch;
83+
},
84+
bump(reason: string) {
85+
epoch += 1;
86+
for (const l of [...listeners]) l(epoch, reason);
87+
return epoch;
88+
},
89+
subscribe(l: (e: number, r: string) => void) {
90+
listeners.add(l);
91+
return () => listeners.delete(l);
92+
},
93+
};
94+
if (seams) {
95+
ql.writeEpoch = writeEpoch;
96+
ql.registerMiddleware = (fn: Middleware) => {
97+
middlewares.push(fn);
98+
};
99+
}
100+
const write = async (
101+
object: string,
102+
operation: 'insert' | 'update' | 'delete',
103+
apply: () => void,
104+
pause?: Promise<void>,
105+
): Promise<void> => {
106+
writeEpoch.bump('write');
107+
const chain = [...middlewares];
108+
let i = 0;
109+
const next = async (): Promise<void> => {
110+
if (i < chain.length) return chain[i++]({ object, operation }, next);
111+
if (pause) await pause;
112+
apply();
113+
};
114+
await next();
115+
};
116+
const organizationReads = () => ql.calls.filter((c) => c.object === 'sys_organization');
117+
/** The `sys_organization` reads one resolution issued, and its platform standing. */
118+
const resolve = async () => {
119+
const before = organizationReads().length;
120+
const grants = await resolveUserAuthzGrants(ql, 'usr_1', { nowMs: NOW, seedEmail: 'admin@example.com' });
121+
return { reads: organizationReads().slice(before), standing: grants.positions.includes('platform_admin') };
122+
};
123+
return { ql, write, writeEpoch, middlewares, resolve };
124+
}
125+
126+
describe('[#24523] the Default Organization answer is read once per engine', () => {
127+
it('⭐ slugged `default`: the first resolution reads it, the next ones do not', async () => {
128+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]));
129+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
130+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
131+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
132+
});
133+
134+
it('⭐ the only organization, slugged otherwise: two reads once, then none', async () => {
135+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'acme' }]));
136+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG, SOLE], standing: true });
137+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
138+
});
139+
140+
it('`undefined` is a real answer and is memoised: several organizations, none `default`', async () => {
141+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'acme' }, { id: 'org_b', slug: 'beta' }]));
142+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG, SOLE], standing: false });
143+
expect(await e.resolve()).toEqual({ reads: [], standing: false });
144+
// …and an organization write retires it like any other answer.
145+
await e.write('sys_organization', 'update', () => {});
146+
expect((await e.resolve()).reads).toEqual([BY_SLUG, SOLE]);
147+
});
148+
149+
it('CONTROL — an engine without the write seams memoises nothing: every resolution reads, as before', async () => {
150+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]), false);
151+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
152+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
153+
});
154+
155+
it('CONTROL — an organization-less grant never reads it, memo or not', async () => {
156+
const e = makeEngineQl(tablesOf('', [{ id: 'org_a', slug: 'default' }]));
157+
// The grant's organization is blank: the legacy organization-less spelling.
158+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
159+
expect(e.middlewares).toHaveLength(0);
160+
});
161+
});
162+
163+
describe('[#24523 / #21868] a sys_organization write through the engine retires the answer', () => {
164+
it('⭐ the Default Organization deleted and recreated: the next resolution reads the new id', async () => {
165+
const tables = tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]);
166+
const e = makeEngineQl(tables);
167+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
168+
169+
await e.write('sys_organization', 'delete', () => {
170+
tables.sys_organization.splice(0);
171+
});
172+
await e.write('sys_organization', 'insert', () => {
173+
tables.sys_organization.push({ id: 'org_b', slug: 'default' });
174+
});
175+
// The first-user promotion's grant now belongs to the new organization.
176+
tables.sys_user_permission_set[0].organization_id = 'org_b';
177+
178+
// A memo still answering `org_a` would withhold standing from `org_b`'s grant.
179+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
180+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
181+
});
182+
183+
it('⭐ a slug change that moves `default` to another organization: the old organization\'s grant stops anchoring', async () => {
184+
const tables = tablesOf('org_a', [{ id: 'org_a', slug: 'default' }, { id: 'org_b', slug: 'beta' }]);
185+
const e = makeEngineQl(tables);
186+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
187+
188+
await e.write('sys_organization', 'update', () => {
189+
tables.sys_organization[0].slug = 'alpha';
190+
});
191+
await e.write('sys_organization', 'update', () => {
192+
tables.sys_organization[1].slug = 'default';
193+
});
194+
// The GRANTING direction: a memo still answering `org_a` would keep
195+
// conferring standing on `org_a`'s grant.
196+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: false });
197+
198+
tables.sys_user_permission_set[0].organization_id = 'org_b';
199+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
200+
});
201+
202+
it('a write to another object — a watched grants table included — does not retire it', async () => {
203+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]));
204+
await e.resolve();
205+
await e.write('sys_user_permission_set', 'update', () => {});
206+
await e.write('sys_session', 'update', () => {});
207+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
208+
});
209+
210+
it('the `remote` and `metadata` epoch bumps retire it; a bare `write` bump does not', async () => {
211+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]));
212+
await e.resolve();
213+
e.writeEpoch.bump('write');
214+
expect((await e.resolve()).reads).toEqual([]);
215+
e.writeEpoch.bump('remote');
216+
expect((await e.resolve()).reads).toEqual([BY_SLUG]);
217+
e.writeEpoch.bump('metadata');
218+
expect((await e.resolve()).reads).toEqual([BY_SLUG]);
219+
expect((await e.resolve()).reads).toEqual([]);
220+
});
221+
222+
it('a write that lands WHILE the read is in flight leaves that answer dead on arrival', async () => {
223+
const tables = tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]);
224+
const e = makeEngineQl(tables);
225+
// Register the observer first (one resolution), then retire it so the next one reads.
226+
await e.resolve();
227+
e.writeEpoch.bump('manual');
228+
const find = e.ql.find.bind(e.ql);
229+
let landed = false;
230+
e.ql.find = async (object: string, opts: any) => {
231+
const rows = await find(object, opts);
232+
if (object === 'sys_organization' && !landed) {
233+
landed = true;
234+
// The rows above are the pre-write snapshot; the write settles before the resolution does.
235+
await e.write('sys_organization', 'update', () => {
236+
tables.sys_organization[0].slug = 'gone';
237+
});
238+
}
239+
return rows;
240+
};
241+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
242+
// Not served from the entry that read was stored under.
243+
expect((await e.resolve()).reads).toEqual([BY_SLUG, SOLE]);
244+
});
245+
246+
it('the TTL is the residual bound: an answer older than 30 s is read again', async () => {
247+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]));
248+
const clock = vi.spyOn(Date, 'now').mockReturnValue(NOW);
249+
await e.resolve();
250+
clock.mockReturnValue(NOW + 29_999);
251+
expect((await e.resolve()).reads).toEqual([]);
252+
clock.mockReturnValue(NOW + 30_001);
253+
expect((await e.resolve()).reads).toEqual([BY_SLUG]);
254+
});
255+
256+
it('MEASURED — a write that STARTED before the observer registered is not seen; the TTL bounds it', async () => {
257+
const tables = tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]);
258+
const e = makeEngineQl(tables);
259+
const clock = vi.spyOn(Date, 'now').mockReturnValue(NOW);
260+
let release!: () => void;
261+
const pause = new Promise<void>((r) => {
262+
release = r;
263+
});
264+
// The write starts with an empty chain: no observer is registered yet.
265+
const inFlight = e.write('sys_organization', 'update', () => {
266+
tables.sys_organization[0].slug = 'alpha';
267+
}, pause);
268+
// The first resolution registers the observer and reads the pre-write row.
269+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
270+
release();
271+
await inFlight;
272+
// The write landed unobserved: the answer stands, stale, until the TTL.
273+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
274+
clock.mockReturnValue(NOW + 30_001);
275+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG, SOLE], standing: true });
276+
});
277+
});
278+
279+
describe('[#24523] ⛔ an outage is not an answer', () => {
280+
it('⭐ a failed read memoises nothing: it propagates, and the next resolution reads again', async () => {
281+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]));
282+
const find = e.ql.find.bind(e.ql);
283+
let down = true;
284+
e.ql.find = async (object: string, opts: any) => {
285+
if (object === 'sys_organization' && down) {
286+
await find(object, opts); // recorded as issued
287+
throw new Error('connect ECONNREFUSED 127.0.0.1:5432');
288+
}
289+
return find(object, opts);
290+
};
291+
const refused = await resolveUserAuthzGrants(e.ql, 'usr_1', { nowMs: NOW, seedEmail: 'admin@example.com' })
292+
.then(() => undefined, (err: unknown) => err as { code?: unknown; status?: unknown });
293+
expect(refused).toMatchObject({ code: 'SERVICE_UNAVAILABLE', status: 503 });
294+
295+
down = false;
296+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
297+
expect(await e.resolve()).toEqual({ reads: [], standing: true });
298+
});
299+
300+
it('a registry that does not hold sys_organization: no read, nothing wired, nothing memoised', async () => {
301+
const e = makeEngineQl(tablesOf('org_a', [{ id: 'org_a', slug: 'default' }]));
302+
let registered = false;
303+
e.ql.registry = { getObject: (name: string) => (name === 'sys_organization' ? registered : true) };
304+
expect(await e.resolve()).toEqual({ reads: [], standing: false });
305+
expect(e.middlewares).toHaveLength(0);
306+
// Registered later in the same boot: read at once, not answered from a verdict.
307+
registered = true;
308+
expect(await e.resolve()).toEqual({ reads: [BY_SLUG], standing: true });
309+
});
310+
});

0 commit comments

Comments
 (0)