/** * Does a live `useShape` read reach the DEPLOYED broker, and does deleting from the set it * returns reach the document? * * Standalone, and deliberately outside `e2e/run.ts`: what it measures is the PUBLISHED * `useShape` - the collection an application reads off the `data` member of what the ORM * hook hands it, and a `.delete()` on that collection reaching the document. `e2e/run.ts` exercises the other shape read, * `useShapeQuery`, which since `1.0.0-dev.6` reads through the SDK's shape query too. Two * surfaces, two runs. * * Run: `bun run e2e/orm-live-read.ts` from `packages/polyfill`. * * -- What it decides ---------------------------------------- * The verdict is readable in one number: does `readyPromise` settle. It does on one path * only — the arrival of an initial payload from the engine * (`GraphOrmSubscription.ts:handleInitialResponse`) — so a refused start leaves it pending * for ever, which is exactly why the failure is silent and why a bound, not an await, is * what turns it into a report. Nothing here infers a verdict from a version string. * * -- What it found, and the row that went with the answer ---------------- * On 2026-09-16 the read did not start: the engine answered `Deserialization error of * config`, and a sixth row of this run established WHERE that `config` sits - position 4, * before the callback, where the five-argument client of the day landed its callback. The * deployment was AHEAD of the pinned client packages, not behind them. * * Updating them answered it: `@ng-org/web@0.1.2-alpha.14` declares six arguments with * `config` at index 4 and `@ng-org/orm@0.1.2-alpha.21` sends six to match, and the run * measured `6 passed, 0 failed` with the read ready in 16ms and the delete reaching the * document. The characterisation row was retired with the question it answered: there is * nothing left for it to find out, and a row that always prints the same sentence teaches * whoever reads this report to skip it. */ import * as fs from "node:fs"; import * as path from "node:path"; import { execSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { armSuiteDeadline, closeContext, closeQuietly, launchWatchedContext, newPage, serveOnEphemeralPort, setupBrokerPage, within, type BrowserContext, type Frame, type Page, } from "ng-e2e-helpers"; import { WALLET, mintBatchWallet } from "./harness-page"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ENTRY = path.resolve(__dirname, "orm-live-entry.ts"); const BUNDLE_OUT = path.resolve(__dirname, ".dist", "orm-live-entry.js"); // ── bounds ────────────────────────────────────────────────────────────────── /** `bun build` is a local bundle; a minute is already ten times what it takes. */ const BUILD_MS = 60_000; /** * The whole measurement. Five checks against one fresh wallet, where the expensive parts * are the broker login and one document creation — the 42-check suite pays both and * measures 3.4 min in total, so eight minutes here is the point past which this has * stopped being a measurement of anything. */ const SUITE_DEADLINE_MS = 8 * 60 * 1000; /** One `__sdk` bridge call. `frame.evaluate()` has no deadline of its own. */ const BRIDGE_MS = 2 * 60 * 1000; /** * How long a live read may take to deliver its initial payload. * * NOT yet computed from a measured latency of this path, and it cannot be while the start * is refused: nothing has ever settled here to measure. It is sized against the duration * the step before it is allowed — the broker login, bounded at 45s in `ng-e2e-helpers` and * measured at a few seconds — so a read that has not answered in 60s has not answered * because it was slow. The first run that settles prints `ready in Nms`; set this to a * generous multiple of that figure then, and say so here. */ const READY_MS = 60_000; /** * How long the delete may take to reach the document. Same status, and the write it waits * on is the one `e2e/run.ts` measures elsewhere at a second or two against this broker. */ const DELETE_MS = 30_000; // ── what this run reports ─────────────────────────────────────────────────── /** * Every check, declared here so the run's total is known BEFORE the browser launches — the * convention `e2e/notebook.ts` states at length: a run that dies in its setup still has a * total to be compared against, so a difference between two runs is always a real one. */ const CHECKS: readonly string[] = [ "the document accepted the seed (two triples for one subject)", "the live read STARTS: `useShape` answers and the subscription reports ready", "the initial payload carries the seeded object, with its `@id` and `@graph`", "`.delete()` on that set is accepted", "the deletion reached the DOCUMENT: the seeded subject's triples are gone", ]; type Check = { name: string; ok: boolean; detail?: string }; const results: Check[] = []; function check(name: string, ok: boolean, detail?: string): void { results.push({ name, ok, detail }); console.log(` [${ok ? "PASS" : "FAIL"}] ${name}${detail ? " — " + detail : ""}`); } /** The bridge as the browser side of `frame.evaluate` sees it — no `any`, and no import * the serialized callback could not carry across. */ type Bridge = Record unknown>; declare global { interface Window { __sdk?: Bridge; } } function sdk(frame: Frame, method: string, ...args: unknown[]): Promise { return within(`__sdk.${method}() in the broker iframe`, BRIDGE_MS, () => frame.evaluate( ([m, a]) => window.__sdk![m as string]!(...(a as unknown[])), [method, args] as const, ), ) as Promise; } function buildBundle(): void { fs.mkdirSync(path.dirname(BUNDLE_OUT), { recursive: true }); execSync(`bun build ${ENTRY} --outfile ${BUNDLE_OUT} --bundle --format=esm`, { stdio: "pipe", cwd: path.resolve(__dirname, ".."), timeout: BUILD_MS, }); } function serve(): Promise<{ url: string; close: () => void }> { const bundle = fs.readFileSync(BUNDLE_OUT, "utf-8"); const html = `ng-eventually ORM live read
`; return serveOnEphemeralPort((req, res) => { if (req.url === "/orm-live-entry.js") { res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" }); res.end(bundle); } else { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(html); } }); } async function main(): Promise { armSuiteDeadline("the ORM live-read measurement", SUITE_DEADLINE_MS); const started = Date.now(); console.log("[orm-live] building the page bundle..."); try { buildBundle(); } catch (e) { console.error( "[orm-live] the bundle did not build. `@ng-org/orm` is an OPTIONAL PEER of this " + "package, so an application that does not name it gets nothing - but THIS " + "checkout declares it in packages/polyfill devDependencies, and `pnpm install` " + "does fetch that. So the ORM is not missing by design here: run `pnpm install` " + "at the repository root. NOTHING was measured, so nothing is concluded.", ); console.error(String((e as { stderr?: Buffer })?.stderr ?? e)); process.exit(1); } console.log("[orm-live] minting this run's wallet..."); const wallet = await mintBatchWallet("the ORM live-read measurement (e2e/orm-live-read.ts)"); const { url, close: closeServer } = await serve(); console.log(`[orm-live] page served at ${url}`); let ctx: BrowserContext | null = null; let page: Page | null = null; try { ctx = await launchWatchedContext("orm-live", wallet.dir); page = await newPage("the ORM live-read page", ctx); page.on("pageerror", (e) => console.error("[iframe error]", e.message)); page.on("console", (m) => { if (m.type() === "error") console.error("[iframe console]", m.text()); }); const frame = await setupBrokerPage(page, url, WALLET.password); await frame.waitForFunction(() => window.__sdk !== undefined, { timeout: 30_000 }); await frame.waitForFunction(() => window.__sdk!.status!() === "connected", { timeout: 60_000, }); const start = await sdk<{ doc: string; subject: string; seeded: number; startError: string | null; }>(frame, "liveStart"); check(CHECKS[0]!, start.seeded === 2, `doc=${start.doc} triples=${start.seeded}`); // The verdict. A refused start never settles `readyPromise`, so this wait IS the // measurement — and the bound is what turns "for ever" into a reported failure. let readyMs: number | null = null; try { await frame.waitForFunction(() => window.__sdk!.liveReadyMs!() !== null, { timeout: READY_MS, }); readyMs = await sdk(frame, "liveReadyMs"); } catch { readyMs = null; } check( CHECKS[1]!, start.startError === null && readyMs !== null, start.startError !== null ? `useShape threw: ${start.startError}` : readyMs === null ? `readyPromise still pending after ${READY_MS / 1000}s — the engine refused the ` + "start; the next row says what it expected instead" : `ready in ${readyMs}ms`, ); const items = await sdk>(frame, "liveItems"); const seen = items[0]; check( CHECKS[2]!, items.length === 1 && seen?.id === start.subject && seen?.graph === start.doc, `items=${JSON.stringify(items)}`, ); const del = await sdk<{ deleted: boolean; error: string | null }>(frame, "liveDelete"); check(CHECKS[3]!, del.deleted === true && del.error === null, `deleted=${del.deleted} error=${del.error}`); let remaining = await sdk(frame, "liveSeededTriples"); try { await within("the deletion reaching the document", DELETE_MS, async () => { const deadline = Date.now() + DELETE_MS; while (remaining > 0 && Date.now() < deadline) { await page!.waitForTimeout(1000); remaining = await sdk(frame, "liveSeededTriples"); } }); } catch { /* the count below is the verdict either way */ } check(CHECKS[4]!, remaining === 0, `triples left for the seeded subject: ${remaining}`); } finally { if (page) await closeQuietly("the ORM live-read page", () => page!.close()); if (ctx) await closeContext("orm-live", ctx); closeServer(); wallet.discard(); } const passed = results.filter((r) => r.ok).length; const failed = results.length - passed; console.log( `\n══ ORM live-read summary: ${passed} passed, ${failed} failed, ${CHECKS.length} declared ` + `— run ${((Date.now() - started) / 60000).toFixed(1)} min ══`, ); if (failed > 0 || results.length !== CHECKS.length) { for (const r of results.filter((x) => !x.ok)) console.log(` - ${r.name}: ${r.detail ?? ""}`); process.exit(1); } } main().catch((e) => { console.error("[orm-live] fatal:", e); process.exit(1); });