Skip to content

Commit ffecf03

Browse files
refactor(metadata-protocol): move the stored-overlay row reads out of protocol.ts into protocol-stored-overlay-reads.ts (#25467 cut 16) (#26168)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X7JKgiM29GUcitRvoGGj5BU)_ Part of #25467 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Cut 16 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cuts 1–15 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797, #25939, #25972, #26014 and #26104. The claim is `6417245092`; the scope is the cut 16 proposal of the cut 15 report (`6415780867`), as the server seat approved it in its dispatch order. All four members moved unedited, so no member was left behind. Four `ObjectStackProtocolImplementation` members move unedited into a new module, `packages/metadata-protocol/src/protocol-stored-overlay-reads.ts` (#23050 shape B). They are the stored-overlay row reads: - `readActiveOverlayRows`: a type's active `sys_metadata` rows, through the cross-request overlay cache. - `storedOverlayEntries`: each row parsed as the list read parses it. - `namesWithOwnStoredRow`: the names that have a stored row of their own. - `readItemActiveRows`: one item's active rows. `protocol.ts` loses 113 net lines (29,440 to 29,327). The cut is move-only, and the card stays open for later cuts. Branch `claude/project-thread-ybjm3h` is the harness-pinned branch for this thread, restarted from `main` `95294be389` after cut 15 merged. It names no issue, so the claim comment is the identity record. It holds the move commit `2b680ef22` and the merge of `main` `601c77384` (`afd0fc791`). That merge touches no file of `packages/metadata-protocol/`: the package's `src/` tree is `d9c112bc` at both ends. **Serial with the file's in-flight PRs.** Measured just before this PR was opened: of the 36 open PRs, one touches `packages/metadata-protocol/src/`. #26005 edits `protocol.meta-types-degenerate-derivation.test.ts`, which does not overlap this cut. No open PR edits `protocol.ts` or the new module. ## The order's `new Error(` question The `new Error(` near these members is at base line `:10741`, inside `resolveAppPackageIdKey`, which does not move. None of the four moved bodies constructs an error (0 `new Error(`, no other error constructor), so no `PRODUCERS` row is owed. The only `catch` in the range (`:10497`) is in `resolveRowlessExpandedView`, which also stays. ## What moves | Member | Base line | Visibility | Body lines | AST tokens (params + return + body) | |---|---|---|---|---| | `readActiveOverlayRows` | `:10262` | private async | 77 | 330 | | `storedOverlayEntries` | `:10349` | private | 29 | 166 | | `namesWithOwnStoredRow` | `:10431` | private | 9 | 98 | | `readItemActiveRows` | `:10662` | private async | 24 | 235 | 139 body lines (the proposal said about 139). ## The shape (#23050 shape B, as cuts 1–15 apply it) - **The class keeps every member** with its name, docblock, signature and position. Each body now delegates, marked `[#25467 cut 16]`. - **Typing.** `StoredOverlayReadHost` types every member as the class member itself (an indexed access on `ObjectStackProtocolImplementation`). Each moved function types its `this` as the `Pick` of the host over exactly the members its body reads. Each stub annotates its literal as `ThisParameterType` of `typeof fn`. No member changes visibility. - **Members read, measured by AST walk** (property and element accesses on `this`, with `as` casts, parentheses and `!` stripped; bare `this` checked too): - `readActiveOverlayRows` and `readItemActiveRows` read `engine` (a live getter; the overlay cache keys its bucket on the engine) and `findEnvironmentRows` (a closure). - `storedOverlayEntries` reads `convertStoredItem` (a closure). - `namesWithOwnStoredRow` reads **no** member. It is a plain function, and its method calls it directly, the way cut 3's `orderByFieldNames` and `groupByFieldNames` stubs do. There is no host and no `.call`. - **No private-static read.** The AST walk found no access on the `ObjectStackProtocolImplementation` identifier and none on `this.constructor`, in any of the four bodies. - **Every other caller still calls the class method.** That covers the list and by-name reads in `protocol.ts` (20 `this.` call sites) and `protocol-save-door-refusals.ts`, whose own host hands `readActiveOverlayRows` and `storedOverlayEntries` over as closures to the class methods. ## Module-level declarations, by reader - **Two gain `export` in place, none moves.** `addressPackages` (read by `namesWithOwnStoredRow`; its other reader stays) and the `StoredOverlayEntry` interface (the return type of `storedOverlayEntries`; `RowlessExpandedView` still names it). Neither is re-exported from `index.ts`, so the package's export table and `.d.ts` do not change (below). The new module's import from `./protocol.js` of `addressPackages` is a runtime import; earlier modules already use that cycle (`protocol-publish-batch.ts`, `protocol-drafts.ts`), and it reads only a function declaration, at call time. - **Imports in `protocol.ts`.** The overlay-cache reads lose their last readers and leave the `./meta-overlay-cache.js` import, which keeps `bumpWriteEpoch`: - `metaOverlayCacheTtlMs` - `readMetaOverlayCache` - `readOverlayCacheGeneration` - `writeMetaOverlayCache` - the type `MetaOverlayCacheKey` The new module imports them from there, and `PLURAL_TO_SINGULAR` / `SINGULAR_TO_PLURAL` and the type `StoredOverlayRow` from where `protocol.ts` does; those keep other readers in `protocol.ts`. The one new statement in `protocol.ts` is the import of the new module. ## The durability `--list` (the order's re-measure) The order expected the members' `engine.find` seams to change file. Measured, they do not. Neither read calls the engine directly: both route through `this.findEnvironmentRows(...)`, which stays in the class and holds the find. `check-durability-degradation-log-level` exits 0 at both ends with identical output, and its `--list` is **identical once `[@:][0-9]+` is stripped** (159 lines; no entry changes file). ## The published `.d.ts` The series bar is ruling `6348891160`, sorted-union identity, measured first. Base `95294be389` and head `2b680ef22` were built with `pnpm --filter @objectstack/metadata-protocol build` against one dependency tree. The closure was built at head by a full `pnpm build` (119/119). For the base build, the package's `src/` was swapped tree-only to base and restored from `HEAD` under a trap; `git diff HEAD` was empty, `git status` clean, and both touched blobs equal to `HEAD`'s after. Measured twice: on the pre-merge tree (base `95294be389`, head `2b680ef22`) and after merging `main` (head `afd0fc791`, closure rebuilt by a full `pnpm build`, 119/119). The readings below hold for both. - **Raw sha256**: `08914e65f936a2b031c3f3f215481a97f53d132eefd869e4f42d928947ebe173` at base and at head, on both `dist/index.d.ts` and `dist/index.d.cts`, 775,874 bytes each. **Raw identity holds**, so sorted-union identity holds too. All four members stay `private NAME;` lines. The newly exported `addressPackages` and `StoredOverlayEntry` do not appear. - **Normalised sha256**: `46ccd7e5c4db673652513d1801ec9c9f06bf34a0a7fb6ba97fb2531ef0cc544b` at both ends (80 string-literal union runs sorted; the cut 12 normaliser). - **The two builds differ where they should**: `index.js` hashes `5671669a82bc3837…` at base and `bfb1ef96fc8499bc…` at head. The `.call(host` sites of `readActiveOverlayRows`, `storedOverlayEntries` and `readItemActiveRows` occur 0 times in the base bundles and once in the head bundles (ESM and CJS alike); `function namesWithOwnStoredRow(` appears 0 times at base and once at head. - **Positive control** on the head tree: `private` was dropped from `readActiveOverlayRows` (on disk, restored from `HEAD` under a trap) and the package rebuilt. Both `.d.ts` files move to `626578dcab46fa8f…` and print `readActiveOverlayRows(request: { type: string; packageId?: string; }): Promise<any[]>;`, so the bar can see a change on these members. - **Export tables.** The ESM and CJS bundles, loaded at runtime from the package, each export the same 78 names at base and head; the sorted list hashes to `9f18a0177ac736ed…` on all four. No moved function, the host type, `addressPackages` or `StoredOverlayEntry` appears. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only **Bodies, by AST token** (the TypeScript compiler's AST leaf tokens, JSDoc skipped, over parameters less the added `this`, return type and body), base `protocol.ts` at `95294be389` against the head module: ``` readActiveOverlayRows: base tokens 330, head tokens 330, differ 0, text-equal(ws-collapsed, comments incl.) true storedOverlayEntries: base tokens 166, head tokens 166, differ 0, text-equal(ws-collapsed, comments incl.) true namesWithOwnStoredRow: base tokens 98, head tokens 98, differ 0, text-equal(ws-collapsed, comments incl.) true readItemActiveRows: base tokens 235, head tokens 235, differ 0, text-equal(ws-collapsed, comments incl.) true TOTAL tokens=829 differ=0 members=4 CLASS members base=300 head=300 order-same=true byte-identical=296 stubs-with-identical-docblock+signature=4 other=[] TOP base=244 head=245 byte-identical=241 ``` **Negative control**, one token changed in an in-memory copy of the head module (`overlayCacheTtlMs > 0 ?` to `overlayCacheTtlMs >= 0 ?`, one occurrence): ``` MUTATED packages/metadata-protocol/src/protocol-stored-overlay-reads.ts "overlayCacheTtlMs > 0 ? readOverlayCacheGeneration" -> "overlayCacheTtlMs >= 0 ? readOverlayCacheGeneration" readActiveOverlayRows: base tokens 330, head tokens 330, differ 1, text-equal(ws-collapsed, comments incl.) false storedOverlayEntries: base tokens 166, head tokens 166, differ 0, text-equal(ws-collapsed, comments incl.) true namesWithOwnStoredRow: base tokens 98, head tokens 98, differ 0, text-equal(ws-collapsed, comments incl.) true readItemActiveRows: base tokens 235, head tokens 235, differ 0, text-equal(ws-collapsed, comments incl.) true TOTAL tokens=829 differ=1 members=4 ``` - **Class side.** 300 members at base and at head, in the same order. The 296 that did not move are byte-identical to base, leading comments included. Each of the 4 stubs sits at its base position and keeps its docblock and signature byte-identical (everything before the body's opening brace). - **Top level of `protocol.ts`.** 241 of the 244 base statements are byte-identical, leading comments included. The other three: - the `./meta-overlay-cache.js` import, losing the five names above; - `addressPackages`, gaining `export`; - `StoredOverlayEntry`, gaining `export`. Head adds one statement, the new module's import. - **Size.** +241 / −136 over 2 files, 377 changed lines (the proposal's estimate was 400–480). `protocol-stored-overlay-reads.ts` has 218 lines. ## Readers of the moved lines Every test and script that reads `protocol.ts` by path was grepped for the four member names and for the overlay-cache names their bodies use. So was every test file in `packages/` and `ui/`, and `docs/qa/`. - **By path: none.** No by-path reader names a moved member or reads a moved body's text. `scripts/docs-audit/affected-docs.mjs` matched only because its self-test carries inline excerpts (a `MetaOverlayCacheKey` struct, an `auditMetaItem` signature); it does not read the moved bodies. - **Behaviour through the class method, no edit owed:** - `protocol.platform-store-system-opt-in.test.ts` calls `protocol.readActiveOverlayRows(...)` at line 265. - `protocol.item-active-rows-one-read.test.ts` calls `p.readItemActiveRows(...)` at lines 114 and 123. Both pass at both ends (in the package suite). `storedOverlayEntries` and `namesWithOwnStoredRow` are named by no test. - No hit in `docs/qa/`. - **ADR anchors and decision letters.** `check-adr-anchors` exits 0 at both ends: 88 anchored files, 59,105 citations, files 8,299 → 8,300, and **1,879 → 1,877** decision-letter citations. The two that leave the anchored file are the `[ADR-0131 D6/D7]` comments in `readActiveOverlayRows`' and `storedOverlayEntries`' bodies (`protocol.ts` `ADR-0131 D6` 113 → 111). The `protocol.ts` anchor entry does not cover ADR-0131, and no entry paragraph describes these reads, so the new module gets no entry (disclosure only, as cuts 8, 12, 13 and 14). - **Engine-double contract** (`check-engine-double-contract`): exits 0 at both ends. The `updateData`/`deleteData` seams keep their entries at new line numbers (14087 → 13974, 14112 → 13999). `productionFiles` is 3,725 at base and 3,727 at head: +1 is the new file, +1 is the gitignored `packages/spec/src/migrations/registry.ts` the spec build writes in the head tree. - **Identical output at both ends:** `check-adr-symbol-anchors`, `check-system-context-census`, `check-tenant-audit-census`, `check-query-options-erasure-ratchet`, `check-filter-alias-parity`, `check-durability-degradation-log-level`. `check-nul-bytes` differs only in the scanned file count, +1. The base-end readings were taken in a clean detached worktree at `95294be389`. - **Governed surface.** `check-governed-merges --branch claude/project-thread-ybjm3h`: 0 of 2 paths hit the register, NOT governed; 377 changed lines, under the 3,000-line human-merge bar. ## Verification - `tsc --noEmit` in `packages/metadata-protocol` exits 0 before and after the merge. `pnpm exec eslint --no-inline-config packages/metadata-protocol` (the whole touched package; it has no `lint` script, these are the root `lint` script's flags) exits 0, no output. - **`packages/metadata-protocol` suite**, base and head, compared per test name and status (vitest `--maxWorkers=2`, JSON reporter, each run under the shared verify lock, `VERDICT command-exit 0` both legs), on the pre-merge tree (the merge changed no file of the package): - Base `95294be389`: 21,558 tests (21,539 passed, 0 failed, 19 skipped), 269 files. - Head `2b680ef22`: 21,558 tests (21,539 passed, 0 failed, 19 skipped), 269 files. - None lost, none added, none changed status. The base leg is the package `src/` swapped tree-only from `95294be389` (the new module removed) and restored from `HEAD` under a trap. - No test outside the package names a moved member, so no cross-package run is owed. - `check-platform-checklist --changed-from origin/main` (merged tree): OK, 1 anchored file in the change set, 29 anchors judged, 0 lost. - **Derived gates.** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 53 gates on 2 changed paths from the merged tree `afd0fc791`, with no stale-tree warning; all 53 exit 0 after the full `pnpm build` (119/119). `dispatch-gates --ran`: 53 derived, 53 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. ## Carry-forward Unchanged from cut 15. These stay in `protocol.ts`: - Public members whose return type is inferred. - The bodies kept by a reader: `probeRecord`, `getMetaItemCached`, `findReferencesToMeta`, `packagelessAddress`, `refuseUnmintableMetaType`, `metaTypeNamespaceExists`, `viewContainerNameCollisionRefusal` with `overlaidShippedContainerViewNames`, `reportPageRequiresAbsentAtLoad`, the 7 `applyRegistryWriteThrough` callers, and `readCodeLayerForCarryForward`. - The private-static readers: `refusePackagedBaseRemoval`, `packagedBaseRefusal`, `runPublishSideEffects`, `promoteDraftForPublish`, `publishPackageDrafts` and `discardPackageDrafts`. - The field writers: `warnClosureReadUnavailable`. The overlay-row family's other members stay for now: - `overlayLockLayerAt` is named by the by-path kit `protocol.lock-one-resolution.kit.test.ts`. - `findServedOverlayRow`, `expandStoredViewContainers` and `resolveRowlessExpandedView` were outside this order. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent f143cd8 commit ffecf03

2 files changed

Lines changed: 241 additions & 136 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 16] The stored-overlay row reads of
5+
* `ObjectStackProtocolImplementation`: a type's active `sys_metadata` rows
6+
* through the cross-request overlay cache (`readActiveOverlayRows`), each row
7+
* parsed as the list read parses it (`storedOverlayEntries`), the names that
8+
* have a stored row of their own (`namesWithOwnStoredRow`), and one item's
9+
* active rows (`readItemActiveRows`), moved out of `protocol.ts` whole.
10+
*
11+
* A MOVE, not a redesign. Each function below is the body of the
12+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
13+
* re-indented one level and otherwise unedited: the same reads, the same
14+
* cache key, epoch and TTL readings in the same order, and the same parse.
15+
*
16+
* ## How the moved bodies reach the class
17+
*
18+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
19+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
20+
* The class keeps every method — the published `dist/*.d.ts` lists each one's
21+
* name, docblock and declared signature — and each body is one literal of
22+
* exactly the {@link StoredOverlayReadHost} members that method reads, plus a
23+
* `.call`. The engine is handed over as a getter, so the body reads the
24+
* protocol's engine at call time (the overlay cache keys its bucket on it); a
25+
* method is handed over as a closure that calls it on the protocol at call
26+
* time, so a test double or spy set on the instance is still the one the body
27+
* reaches. `namesWithOwnStoredRow` reads no member at all, so its method calls
28+
* it directly, as `orderByFieldNames` does (cut 3). Every other reader of
29+
* these four methods, `protocol-save-door-refusals.ts` among them, keeps
30+
* calling the class method.
31+
*
32+
* What the bodies read of `protocol.ts`'s own module-level declarations they
33+
* import from it; what they import from elsewhere, they import from where
34+
* `protocol.ts` imported it. Nothing re-exports this module, so the published
35+
* surface does not move.
36+
*/
37+
38+
import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared';
39+
import type { StoredOverlayRow } from './item-lock.js';
40+
import {
41+
metaOverlayCacheTtlMs,
42+
readMetaOverlayCache,
43+
readOverlayCacheGeneration,
44+
writeMetaOverlayCache,
45+
type MetaOverlayCacheKey,
46+
} from './meta-overlay-cache.js';
47+
import type { ObjectStackProtocolImplementation, StoredOverlayEntry } from './protocol.js';
48+
import { addressPackages } from './protocol.js';
49+
50+
/**
51+
* The `ObjectStackProtocolImplementation` members the functions below read —
52+
* every one of them, and no other; each function's `this` is the `Pick` of the
53+
* members IT reads. Each is typed AS the class member (an indexed access on
54+
* the class), so a change to a member's signature on the class is a change
55+
* here too, and naming a member the class does not have is a compile error.
56+
*/
57+
export interface StoredOverlayReadHost {
58+
readonly engine: ObjectStackProtocolImplementation['engine'];
59+
findEnvironmentRows: ObjectStackProtocolImplementation['findEnvironmentRows'];
60+
convertStoredItem: ObjectStackProtocolImplementation['convertStoredItem'];
61+
}
62+
63+
/** The body of `ObjectStackProtocolImplementation#readActiveOverlayRows`; the method's docblock there is its contract. */
64+
export async function readActiveOverlayRows(
65+
this: Pick<StoredOverlayReadHost, 'engine' | 'findEnvironmentRows'>,
66+
request: { type: string; packageId?: string },
67+
): Promise<any[]> {
68+
const { packageId } = request;
69+
const queryActive = async (): Promise<any[]> => {
70+
const whereClause: Record<string, unknown> = {
71+
type: request.type,
72+
state: 'active',
73+
};
74+
if (packageId) whereClause.package_id = packageId;
75+
// [#21911] The explicit system opt-in — see findServedOverlayRow.
76+
// [ADR-0131 D6/D7] The environment's rows only, on every find —
77+
// see {@link findEnvironmentRows}.
78+
let rs = await this.findEnvironmentRows('sys_metadata', { where: whereClause, context: { isSystem: true } });
79+
if ((!rs || rs.length === 0)) {
80+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
81+
if (alt) {
82+
const altWhere: Record<string, unknown> = { type: alt, state: 'active' };
83+
if (packageId) altWhere.package_id = packageId;
84+
rs = await this.findEnvironmentRows('sys_metadata', { where: altWhere, context: { isSystem: true } });
85+
}
86+
}
87+
return rs ?? [];
88+
};
89+
// ── Leg D of #11633 (#11967): the cross-request overlay cache ──
90+
//
91+
// ⭐ The cache sits HERE, and its position IS the resolution of the
92+
// SchemaRegistry-hydration trap #11633 §4 names. What is cached is
93+
// the ROW SET — the value the `queryActive` call produces —
94+
// never the merged answer below it. Everything downstream of this
95+
// point still runs on every call, hit or miss: the overlay parse,
96+
// the package-aware merge, `hydrateOverlayIntoRegistry`, the
97+
// MetadataService merge, the disabled-package filter, the nav
98+
// contributions, the decorations. A hit changes where the rows came
99+
// from and nothing about what is done with them, so the read-side
100+
// registry hydration cannot be skipped by one.
101+
//
102+
// ⛔ Do NOT move this below the merge. That is precisely the naive
103+
// shape the trap describes, and it would additionally serve three
104+
// mutable sources — the SchemaRegistry, the MetadataService and the
105+
// artifact table — whose changes nothing in this key can observe.
106+
// See `meta-overlay-cache.ts` for the measured four-source table.
107+
//
108+
// ⭐ The epoch reading is taken BEFORE the read, and it is this
109+
// pre-read value that is stored with the rows. A write landing
110+
// WHILE this read is in flight therefore moves the epoch past what
111+
// the entry records, so the entry is already dead when it is
112+
// written — the safe direction. Reading it afterwards would stamp
113+
// pre-write rows with a post-write epoch and make that staleness
114+
// permanent: the clear-then-repopulate-from-a-stale-read failure
115+
// #11633 §7 pin 2 names.
116+
const overlayCacheKey: MetaOverlayCacheKey = {
117+
type: request.type,
118+
packageId,
119+
};
120+
const overlayCacheTtlMs = metaOverlayCacheTtlMs();
121+
const overlayCacheEpoch = overlayCacheTtlMs > 0 ? readOverlayCacheGeneration(this.engine) : undefined;
122+
const overlayCacheNow = Date.now();
123+
const cachedRecords = readMetaOverlayCache(
124+
this.engine, overlayCacheKey, overlayCacheEpoch, overlayCacheTtlMs, overlayCacheNow,
125+
);
126+
127+
let records: any[];
128+
if (cachedRecords !== undefined) {
129+
records = cachedRecords as any[];
130+
} else {
131+
records = await queryActive();
132+
// ⭐ An EMPTY row set is cached too, and that is the main point
133+
// rather than an edge case: the empty result is what triggers the
134+
// alt-type retry above, so "no overlay rows for this type" is the
135+
// answer whose caching removes BOTH reads. #11633 §1 measured that
136+
// an app whose objects are all code-authored pays the doubled read
137+
// on every request; this is the line that stops it.
138+
writeMetaOverlayCache(
139+
this.engine, overlayCacheKey, overlayCacheEpoch, records, overlayCacheTtlMs, overlayCacheNow,
140+
);
141+
}
142+
return records;
143+
}
144+
145+
/** The body of `ObjectStackProtocolImplementation#storedOverlayEntries`; the method's docblock there is its contract. */
146+
export function storedOverlayEntries(
147+
this: Pick<StoredOverlayReadHost, 'convertStoredItem'>,
148+
request: { type: string },
149+
records: any[],
150+
): StoredOverlayEntry[] {
151+
// Parse each overlay body once — replaying the stored-row
152+
// conversion chain (#3903) so every consumer of this list sees
153+
// the canonical protocol shape — and surface its persisted
154+
// software-package binding so the sidebar package filter and
155+
// provenance classification see overlay rows the way they see
156+
// registry items.
157+
return records.map((record) => {
158+
const data = this.convertStoredItem(
159+
String(record.type ?? request.type),
160+
typeof record.metadata === 'string'
161+
? JSON.parse(record.metadata)
162+
: record.metadata,
163+
) as any;
164+
const recPkg = (record as { package_id?: string | null }).package_id ?? undefined;
165+
if (recPkg && data && typeof data === 'object' && (data as any)._packageId === undefined) {
166+
(data as any)._packageId = recPkg;
167+
}
168+
// [ADR-0131 D6/D7] Every row is the environment's: the family
169+
// declares no organization column, and the reads that feed this
170+
// ({@link readActiveOverlayRows}) leave a legacy organization's
171+
// rows out off the physical column. (The form doors' legacy layer
172+
// that once needed the row's own scope here is deleted.)
173+
return {
174+
name: String(record.name), data, packageId: recPkg, organizationId: null,
175+
type: String(record.type ?? request.type),
176+
};
177+
});
178+
}
179+
180+
/** The body of `ObjectStackProtocolImplementation#namesWithOwnStoredRow`; the method's docblock there is its contract. */
181+
export function namesWithOwnStoredRow(records: readonly any[], packageId?: string): ReadonlySet<string> {
182+
const packages = addressPackages(packageId);
183+
const names = new Set<string>();
184+
for (const record of records) {
185+
if (typeof record?.name !== 'string') continue;
186+
if (packages.some((pkg) => pkg === undefined || (record?.package_id ?? null) === pkg)) names.add(record.name);
187+
}
188+
return names;
189+
}
190+
191+
/** The body of `ObjectStackProtocolImplementation#readItemActiveRows`; the method's docblock there is its contract. */
192+
export async function readItemActiveRows(
193+
this: Pick<StoredOverlayReadHost, 'engine' | 'findEnvironmentRows'>,
194+
type: string, name: string, cached = false,
195+
): Promise<StoredOverlayRow[]> {
196+
const other = PLURAL_TO_SINGULAR[type] ?? SINGULAR_TO_PLURAL[type];
197+
const read = async (): Promise<StoredOverlayRow[]> => {
198+
// [#21911] The explicit system opt-in — see findServedOverlayRow.
199+
const rows: StoredOverlayRow[] | null | undefined = await this.findEnvironmentRows('sys_metadata', {
200+
where: { type: other === undefined ? type : { $in: [type, other] }, name, state: 'active' },
201+
context: { isSystem: true },
202+
});
203+
return rows ?? [];
204+
};
205+
if (!cached) return read();
206+
// ⭐ The epoch is read BEFORE the read and stored with its rows, as the
207+
// list read's is (see `readActiveOverlayRows`): a write landing while
208+
// this read is in flight leaves the entry dead on arrival.
209+
const key: MetaOverlayCacheKey = { type, name };
210+
const ttlMs = metaOverlayCacheTtlMs();
211+
const epoch = ttlMs > 0 ? readOverlayCacheGeneration(this.engine) : undefined;
212+
const now = Date.now();
213+
const hit = readMetaOverlayCache(this.engine, key, epoch, ttlMs, now);
214+
if (hit !== undefined) return hit as StoredOverlayRow[];
215+
const rows = await read();
216+
writeMetaOverlayCache(this.engine, key, epoch, rows, ttlMs, now);
217+
return rows;
218+
}

0 commit comments

Comments
 (0)