Skip to content

Commit 59d9181

Browse files
refactor(objectql): move the realtime publish bodies out of engine.ts into engine-realtime-publish.ts (#25522 cut 4) (#25948)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DMYdQaNcHT9ZVNXcm3pkCpX)_ Part of #25522 Clause-②: no Metadata-protocol: none Ruling-ref: 6348891160 Cut 4 of the `packages/objectql/src/engine.ts` split (#24586 ruling A, recorded in `6270097840`). The claim is `6408069065`. The bodies of the two private `ObjectQL` realtime publish methods move unedited into a new module, `packages/objectql/src/engine-realtime-publish.ts`: `publishDataEvent` and `publishBulkDataEvent`. The ten module-level event helpers that only those two call move with them, byte for byte. #25522 stays open for the later cuts. Branch `claude/project-thread-6ikbqy` (platform-assigned to this card thread), cut from `origin/main` at `f00e8d5c8`. Since then `main` has moved to `2a728ba2a`; no commit in that range touches `packages/objectql/**` or the docs page this PR edits. The branch has not been merged forward, and CI builds the merge ref. **Serial with the file's in-flight PRs (A1).** Before the first edit, none of the 7 open PRs touched `packages/objectql/src/engine.ts`, `engine-data-events.test.ts`, `federated-injected-column-readers.test.ts` or `realtime-protocol.mdx` (every file list read through the REST API). Seat 1's #25653 edits `withRunAs` in `engine.ts`, a different region. Just before this PR opened I measured again: 16 PRs are open, and one new one touches `engine.ts`. #25935 (opened 04:48Z, after this claim) adds one import line near base line 279 and an aggregate refusal near base line 21954. Neither overlaps this diff; the nearest hunk here ends at base line 263. For each of those PRs, whichever lands second merges `main`. ## What moves, re-measured on `origin/main` `f00e8d5c8` The cut-3 dev proposed this area in PR #25686's body. I measured it again before moving anything: - **Both methods are private with explicit return types** (a `Promise` of `void`). The stubs keep both signatures and docblocks, so the `.d.ts` keeps its bytes (measured below). - **18 lines from the last 30 days** in the two bodies (16 in `publishDataEvent`, 2 in `publishBulkDataEvent`; `git blame --since=2026-09-26T04:00Z` at `f00e8d5c8`, after deepening history to 2026-07-19). The moved helpers carry 71 recent lines out of 279. Control: the whole file has 7,261 recent lines on the same blame. - **No `this.FIELD.delete/insert/update(` site and no static read.** The bodies read three members only: `realtimeService`, `logger` and `_registry`. - **No spy and no instance double** on either method or on `realtimeService` anywhere in the repo's tests. The callers still call the methods on the engine. - **The helpers.** The bodies call ten module-level functions in `engine.ts`: `generateEventUuid`, `eventRecordId`, `eventRecordBody`, `redactEventMetadataBody`, `projectEventRecordBody`, `eventUserId`, `eventOrganizationId`, `eventOrganizationValue`, `bulkEventOrganizationId` and `eventMatchedCount`. No other code in `engine.ts` calls any of them, measured by name. They move with the bodies, so the new module needs no runtime import of `engine.ts`. `accessibleOrganizationsOf` sat between them, but the tenant wall reads it, so it stays. The cut-3 proposal priced this cut at about −95 lines and listed no path reader. Both readings are corrected here. The helpers move too, so the cut is larger. And three readers read the moved code by path; each is migrated below. ## The shape (#23050 shape B, as cuts 1 to 3 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 4]`. - `RealtimePublishHost` is new. It types each member as the class member itself (`ObjectQL['realtimeService']`, `ObjectQL['logger']`, `ObjectQL['_registry']`). 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. - **All three members go as getters.** Each read happens when the body makes it, so a service installed later by `setRealtimeService` is seen as before. `publishBulkDataEvent`'s host has no `_registry`, because its body never reads it. - **Imports.** The module imports `ObjectQL` as a type only, so there is no module cycle at run time. It imports what the moved code reads, with the specifiers `engine.ts` used. `engine.ts` drops these imported names, whose only reader was the moved code: `redactStoredMetadataBody`, `STORED_METADATA_BODY_COLUMN` and `STORED_METADATA_TYPE_COLUMN` (`@objectstack/spec/kernel`), `RealtimeEventPayload` (`@objectstack/spec/contracts`), the whole `@objectstack/spec/api` import (`BulkDataEventSchema`, `DataEventSchema`, `BulkDataEvent`, `DataEvent`), and `omitInternalFieldsFromWriteResponse` (`@objectstack/core`; its three other mentions are prose). The `[#21120]` and `omitInternalFieldsFromWriteResponse` import comments move with their imports. Nothing re-exports the module. ## Net lines (the seat's rule: an area moves only when `engine.ts` shrinks) | | base `f00e8d5c8` | head | |---|---|---| | `engine.ts` (`wc -l`) | 23,820 | 23,430 (−390) | | `engine-realtime-publish.ts` | — | 507 | Diff: +538 / −420 over 5 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`; the diff touches none of it). Then `pnpm --filter @objectstack/objectql build` with `engine.ts` restored from `f00e8d5c8` (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 `readRealtimeService` 4 times and the base copy 0 times, so each build is the one it claims to be. I ran the pair twice (once for the declarations, once for the export lists), with the same result. All six declaration files are **byte-identical**, and the raw diff of each is 0 lines. The util chunk name, `util-BuVJ7CNx`, is the same at both ends. | file | raw sha256, base = head | |---|---| | `index.d.ts` | `6f76e7978f0fc72c6bbcf129f97c753188123ed605cf01ee38d1774ea8f0e69a` | | `index.d.mts` | `21fdbb330b86ada1f46e2431bae853a8ccb8d64f08469e3e2b41254a50605145` | | `core.d.ts` | `2cd3ea741f5c01f5de3274d5728254ead8bacb8dcd57886bede9f39c648bb2f1` | | `core.d.mts` | `47c1c62d395f5a63d64842bd99ecf6fbeadbf5a34c057b7a9f4b005377c11c5a` | | `util-BuVJ7CNx.d.ts` and `.d.mts` | `05835759410068e3f1002223baf35eca63064ee4d090b1eb5fd5276b2da3feee` | - **Normaliser.** I ran the ruling's one-liner verbatim (double-quoted unions) on every file. Normalised = raw at both ends, because this package prints single-quoted literals. This cut does not depend on it: the raw bytes are identical. - **Positive control**, in the same run, on a copy of the head `util-BuVJ7CNx.d.ts`. Line 7157 `private publishDataEvent;` made public (`publishDataEvent;`, raw diff 4 lines) gives raw = normalised `a379ca74f62d51b31185fa0119cfcd24e1a5c920645e135390c257e372f4192d`, which differs from `05835759…`. - **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 cuts 2 and 3. No moved name is exported. No changeset: `skip-changeset`, per ruling `6348891160` item 4. ## Proof of move-only The proof uses AST tokens: the recursive `getChildren` leaves of the TypeScript parse. JSDoc leaves are compared with leading indentation stripped per line, because the moved code is one level shallower. - **Bodies.** For each moved function, the parameters (without the added `this`), the return type and the body have the same token sequence as the base method: `publishDataEvent` 371 tokens and `publishBulkDataEvent` 287 tokens, both SAME. Every body line is the base line less two leading spaces (60 and 57 lines, braces included). - **Helpers.** All ten are byte-identical to base, full text with the docblock (`getFullText`, trimmed): 741 tokens in all. - **Negative controls.** Changing one token in the moved `publishBulkDataEvent` (`matched === 0` to `matched === 1`, in memory) makes the same check report DIFFER. Changing one in the moved helper `eventRecordId` also reports DIFFER. - **Class side.** The class has 318 members at base and at head, in the same order. The 316 that did not move are byte-identical (full text, docblocks included). The 2 stubs keep their docblocks, modifiers and signatures byte-identical. - **Top level of `engine.ts`.** 222 of the 237 base statements are byte-identical at head. The other 15 are the 10 helpers, the class, and four imports: three narrowed by the names listed above, and the `@objectstack/spec/api` import, gone whole. The new statements are the import of the new module, the three narrowed imports, and the class. - **The tests reach the moved code** through the stubs: `engine-data-events.test.ts` (42 cases), `engine-realtime-credential-mask.test.ts` and `engine-realtime-stored-metadata-body.test.ts`. All pass at head (below). ## Readers of the moved lines, migrated in this PR - **`engine-data-events.test.ts`'s source pin** ("BulkDataEventSchema.parse at the publish site stays the validator…") read `engine.ts` for `private async publishBulkDataEvent(` and `function bulkEventOrganizationId(`. It now reads `engine-realtime-publish.ts` for `export async function publishBulkDataEvent(`, and the body ends at the module's own closing brace. Every assertion is unchanged. **Proof that it can still fail:** `scripts/ablation-replace.mjs` added an `eventOrganizationId(` call into the moved bulk body (anchor 1 → 0, blob `02e17ff9f831` → `87ab8ae01da1`). The pin went red: 1 failed / 41 passed. Restored, the blob is HEAD's again and `git diff HEAD` is empty. - **`federated-injected-column-readers.test.ts`** keys each seam use as `FILE#FUNCTION :: SEAM`. The row for `eventOrganizationId :: resolveTenantFieldName()` moves from `engine.ts#` to `engine-realtime-publish.ts#`. Its disposition and reason are unchanged. **Proof:** putting the old key back (blob `17bd6adaf9c5` → `6ea31c760521`) makes "has a row for every seam use in the source" fail and name the new key as unlisted. Restored the same way. - **`content/docs/protocol/kernel/realtime-protocol.mdx`** pointed at `publishDataEvent` in `packages/objectql/src/engine.ts` for what `changes` carries. It now names `engine-realtime-publish.ts`. - **Liveness ledgers.** No ledger names a moved symbol; `pnpm --filter @objectstack/spec check:liveness` exits 0 at head. - **ADR anchors (A2).** `scripts/adr-anchors/packages__objectql__src__engine.ts.json`'s one paragraph is about the transaction seam, which does not move. No anchor entry describes a moved body, so none is added. `check:adr-anchors` exits 0 at both ends: 137 decision numbers, and 58,841 citations at both ends (8,253 files at base, 8,254 at head: the new module). **The decision-letter count on the anchored surface goes from 1,847 at base to 1,842 at head.** The five citations are ADR-0076 D2, ADR-0131 D8, ADR-0095 D1, ADR-0105 D2 and ADR-0131 D7. They sit in the moved helper docblocks, and the new module is not an anchored file. They are still valid letters. A probe entry for the new module, made only to measure and then deleted, put the count back to 1,847 with the gate at exit 0. Whether the module should get its own entry is in Acceptance notes. - **Other path readers.** `check-adr-symbol-anchors`: 2,474 anchors resolve. `check-platform-checklist --changed-from origin/main`: 1 anchored file in the change set, 14 anchors judged, 0 lost. ## Direct dependants (A3, narrowed and declared) No dependant was type-checked locally. Both moved methods are private, so no dependant can reach them, and the six published declaration files are byte-identical. CI's TypeScript Type Check lanes cover the dependants. ## For the next dispatch (cut 5) From cut 3's list, the areas still waiting are these: Roll-up summaries (spec-coupled, see cut 1); the Datasource registry (U = 3); Related-record expansion (`expandRelatedRecords` reads the private static `MAX_EXPAND_DEPTH`). I did not measure a fresh seam table for cut 5. The next dispatch should measure one. ## Acceptance notes - **An ADR anchor entry for `engine-realtime-publish.ts`?** Five decision-letter citations left the anchored surface with the moved helpers (above). By card ask 3, no entry is owed, because no anchor paragraph describes a moved body. This claim's file surface also does not include `scripts/adr-anchors/**`, so I did not add one. If the seat wants those five letters checked again, a one-file entry that names ADR-0131 would do it. The paragraph would cover `bulkEventOrganizationId`, which reads the recorded wall verdict alone (D8). The probe above shows that entry passes. Carrier: the seat. ## Verification All at head `2108463930`. - `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): 467 files, 1,903 suites, 8,718 tests, 8,718 passed, 0 failed, 0 skipped. - `pnpm lint` (`eslint . --no-inline-config`, repo-wide, as the seat asked): exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 91 families on the real diff at `2108463930` (stderr names the repo and the commit). All 91 were run, and each exit code was recorded before any pipe. 89 exited 0 on the first pass. `check:skill-examples` exited 3 until I built the `@objectstack/client-react...` closure, then exited 0 (262 prose examples type-check). `check-engine-split-ratio --days 90` exited 0, ratio 97.8%, with history deepened to 2026-07-19. `--ran` reconciles: 91 accounted for, 90 run, 1 NOT MEASURED: - `check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET: the other packages have no `dist/`. The export lists above answer its question for this package, and CI runs it. - Hand gates, all exit 0: `check-adr-anchors` (base and head), `check-adr-symbol-anchors`, `check-platform-checklist --changed-from origin/main`, `check-commit-card-trailers --range origin/main..HEAD`. - `test:repo` was not run locally; CI's `Test Core` runs it. --- _Generated by [Claude Code](https://claude.ai/code/session_017kEHq65soeg6DRE9DDutT2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3b957f9 commit 59d9181

6 files changed

Lines changed: 545 additions & 420 deletions

File tree

‎content/docs/protocol/kernel/realtime-protocol.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ They are camelCase, and the record body travels **inside `payload`**, never at t
432432
and `payload`, so a client written from the old fences reads `msg.subscription_id` and `msg.data`
433433
and gets `undefined` on both. `changes` is real but sits **one level down**, on the `DataEvent`
434434
inside `payload`, and it carries the write's own patch — a flat `{ field: newValue }` map
435-
(`publishDataEvent`, `packages/objectql/src/engine.ts`), not the `old` / `new` pairs this page
435+
(`publishDataEvent`, `packages/objectql/src/engine-realtime-publish.ts`), not the `old` / `new` pairs this page
436436
used to show; read the post-state from `payload.after`. `reason` is declared nowhere in the
437437
protocol at all — see **Subscribe to Query Results** below.
438438
</Callout>

‎packages/objectql/src/engine-data-events.test.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,11 +1009,12 @@ describe('#15225 / #15813 — a published BulkDataEvent names the organization t
10091009
// emitted INSIDE the `parse` call — a stamp added onto the envelope after
10101010
// validation would pass every pin above while bypassing the contract.
10111011
const testPath = expect.getState().testPath;
1012-
if (!testPath) throw new Error('vitest did not report a testPath — cannot locate engine.ts');
1013-
const src = readFileSync(join(dirname(testPath), 'engine.ts'), 'utf8');
1014-
const start = src.indexOf('private async publishBulkDataEvent(');
1012+
if (!testPath) throw new Error('vitest did not report a testPath — cannot locate engine-realtime-publish.ts');
1013+
// [#25522 cut 4] The body and the resolver moved out of `engine.ts`, whole.
1014+
const src = readFileSync(join(dirname(testPath), 'engine-realtime-publish.ts'), 'utf8');
1015+
const start = src.indexOf('export async function publishBulkDataEvent(');
10151016
expect(start).toBeGreaterThan(-1);
1016-
const body = src.slice(start, src.indexOf('\n }\n', start));
1017+
const body = src.slice(start, src.indexOf('\n}\n', start));
10171018
const parseCalls = body.match(/BulkDataEventSchema\.parse\(\{[\s\S]*?\n\s*\}\);/g) ?? [];
10181019
expect(parseCalls).toHaveLength(1);
10191020
expect(parseCalls[0]).toContain('organizationId');

0 commit comments

Comments
 (0)