import { expect, test, type Page } from "@playwright/test"; import { resultSnapshot, prepareApp } from "./fixtures"; async function probePage(page: Page) { await prepareApp(page); await page.route("**/result-persistence-probe", route => route.fulfill({ contentType: "text/html", body: "Persistence probe" })); await page.goto("/result-persistence-probe"); } test("eight-branch shaped results save atomically with few writes and restore every number", async ({ page }) => { await probePage(page); const saved = await page.evaluate(async template => { // @ts-expect-error browser-side Vite module const api = await import("/src/resultPersistence.ts"); const series: Record = { emptyFirst: [] }; for (let column = 0; column < 1785; column++) { series[column === 0 ? "time" : `output.${column}`] = Array.from({ length: 1002 }, (_, row) => column * 100000 + row + 0.125); } series.emptyLast = []; series["output.17"][31] = -0; const snapshot = { ...template, result: { ...template.result, series } }; const originalTransaction = IDBDatabase.prototype.transaction; const originalPut = IDBObjectStore.prototype.put; let writes = 0; let transactions = 0; let pointerBeforeCommit: string | null | undefined; let pointerAtCommit: string | null | undefined; IDBDatabase.prototype.transaction = function (...args: Parameters) { const transaction = originalTransaction.apply(this, args); if (this.name === "system-simulation-results" && transaction.mode === "readwrite") { transactions++; pointerBeforeCommit = sessionStorage.getItem(api.RESULT_SNAPSHOT_KEY); transaction.addEventListener("complete", () => { pointerAtCommit = sessionStorage.getItem(api.RESULT_SNAPSHOT_KEY); }); } return transaction; }; IDBObjectStore.prototype.put = function (...args: Parameters) { writes++; return originalPut.apply(this, args); }; const start = performance.now(); await api.storeResultSnapshot(snapshot); const saveMs = performance.now() - start; IDBDatabase.prototype.transaction = originalTransaction; IDBObjectStore.prototype.put = originalPut; const readStart = performance.now(); const restored = await api.loadStoredResultSnapshot(); return { writes, transactions, saveMs, readMs: performance.now() - readStart, pointerBeforeCommit, pointerAtCommit, hasPointer: Boolean(sessionStorage.getItem(api.RESULT_SNAPSHOT_KEY)), pending: api.resultSavePending(), exact: Object.entries(series).every(([key, values]) => values.length === restored.result.series[key].length && values.every((value, i) => Object.is(value, restored.result.series[key][i]))) }; }, resultSnapshot); expect(saved).toMatchObject({ transactions: 1, pointerBeforeCommit: null, pointerAtCommit: null, hasPointer: true, pending: false, exact: true }); // Thousands of short output columns must not cause thousands of writes or // one transaction per small batch; the workload bound is independent of packing size. expect(saved.writes).toBeLessThan(16); console.log(JSON.stringify({ probe: "eight-branch-packed-persistence", ...saved })); await page.reload(); const restored = await page.evaluate(async () => { // @ts-expect-error browser-side Vite module const { loadStoredResultSnapshot } = await import("/src/resultPersistence.ts"); const snapshot = await loadStoredResultSnapshot(); return { columns: Object.keys(snapshot.result.series).length, last: snapshot.result.series["output.1784"][1001], empty: snapshot.result.series.emptyFirst }; }); expect(restored).toEqual({ columns: 1787, last: 178401001.125, empty: [] }); }); test("legacy session snapshots and old column chunks remain readable; gaps and overlaps fail", async ({ page }) => { await probePage(page); const result = await page.evaluate(async template => { // @ts-expect-error browser-side Vite module const api = await import("/src/resultPersistence.ts"); sessionStorage.setItem(api.RESULT_SNAPSHOT_KEY, JSON.stringify(template)); const sessionLegacy = (await api.loadStoredResultSnapshot()).id === template.id; // Create legacy records independently of the new writer and its format. const db = await new Promise((resolve, reject) => { const request = indexedDB.open("system-simulation-results", 1); request.onupgradeneeded = () => { request.result.createObjectStore("headers"); request.result.createObjectStore("chunks"); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); async function legacy(chunks: [number, number[]][]) { await new Promise((resolve, reject) => { const tx = db.transaction(["headers", "chunks"], "readwrite"); tx.oncomplete = () => resolve(); tx.onabort = () => reject(tx.error); tx.objectStore("headers").put({ snapshot: { ...template, result: { ...template.result, series: {} } }, lengths: { time: 6, empty: 0 } }, "legacy"); tx.objectStore("chunks").clear(); for (const [offset, values] of chunks) tx.objectStore("chunks").put(new Float64Array(values), ["legacy", "time", offset]); }); sessionStorage.setItem(api.RESULT_SNAPSHOT_KEY, JSON.stringify({ storage: "indexeddb", version: 1, cacheId: "legacy" })); try { return { values: (await api.loadStoredResultSnapshot()).result.series.time, rejected: false }; } catch { return { rejected: true }; } } const complete = await legacy([[0, [1, 2, 3]], [3, [4, 5, 6]]]); const gap = await legacy([[0, [1, 2]], [3, [4, 5, 6]]]); const overlap = await legacy([[0, [1, 2, 3, 4]], [2, [3, 4]]]); db.close(); return { sessionLegacy, complete, gap, overlap }; }, resultSnapshot); expect(result).toEqual({ sessionLegacy: true, complete: { values: [1, 2, 3, 4, 5, 6], rejected: false }, gap: { rejected: true }, overlap: { rejected: true } }); }); test("in-flight superseded commits and failed writes cannot replace the latest complete pointer", async ({ page }) => { await probePage(page); const result = await page.evaluate(async template => { // @ts-expect-error browser-side Vite module const api = await import("/src/resultPersistence.ts"); await api.storeResultSnapshot(template); const stablePointer = sessionStorage.getItem(api.RESULT_SNAPSHOT_KEY); const originalPut = IDBObjectStore.prototype.put; let puts = 0; IDBObjectStore.prototype.put = function (...args: Parameters) { if (++puts === 2) throw new DOMException("Injected header failure after data write", "QuotaExceededError"); return originalPut.apply(this, args); }; let failed = false; try { await api.storeResultSnapshot({ ...template, id: "failed" }); } catch { failed = true; } IDBObjectStore.prototype.put = originalPut; const preserved = sessionStorage.getItem(api.RESULT_SNAPSHOT_KEY) === stablePointer; const originalTransaction = IDBDatabase.prototype.transaction; let latest: Promise | undefined; let armed = true; IDBDatabase.prototype.transaction = function (...args: Parameters) { const transaction = originalTransaction.apply(this, args); if (armed && this.name === "system-simulation-results" && transaction.mode === "readwrite") { armed = false; // Replace an older save after its transaction starts but before commit. queueMicrotask(() => { latest = api.storeResultSnapshot({ ...template, id: "newest" }); }); } return transaction; }; const old = await api.storeResultSnapshot({ ...template, id: "older" }); const newer = await latest; IDBDatabase.prototype.transaction = originalTransaction; const restored = await api.loadStoredResultSnapshot(); return { failed, preserved, old, newer, id: restored.id, pending: api.resultSavePending() }; }, resultSnapshot); expect(result).toEqual({ failed: true, preserved: true, old: false, newer: true, id: "newest", pending: false }); }); test("saving in another page preserves its borrowed cache and corrupt packed records are rejected", async ({ page, context }) => { await probePage(page); const pointer = await page.evaluate(async template => { // @ts-expect-error browser-side Vite module const api = await import("/src/resultPersistence.ts"); await api.storeResultSnapshot(template); return sessionStorage.getItem(api.RESULT_SNAPSHOT_KEY)!; }, resultSnapshot); const other = await context.newPage(); await probePage(other); await other.evaluate(async ({ template, pointer }) => { // @ts-expect-error browser-side Vite module const api = await import("/src/resultPersistence.ts"); sessionStorage.setItem(api.RESULT_SNAPSHOT_KEY, pointer); await api.loadStoredResultSnapshot(); await api.storeResultSnapshot({ ...template, id: "other-page" }); }, { template: resultSnapshot, pointer }); const outcome = await page.evaluate(async () => { // @ts-expect-error browser-side Vite module const api = await import("/src/resultPersistence.ts"); const before = await api.loadStoredResultSnapshot(); const marker = JSON.parse(sessionStorage.getItem(api.RESULT_SNAPSHOT_KEY)!); const db = await new Promise((resolve, reject) => { const request = indexedDB.open("system-simulation-results", 1); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); await new Promise((resolve, reject) => { const tx = db.transaction("chunks", "readwrite"); tx.oncomplete = () => resolve(); tx.onabort = () => reject(tx.error); const cursor = tx.objectStore("chunks").openCursor(IDBKeyRange.bound([marker.cacheId], [marker.cacheId, []])); cursor.onsuccess = () => { const entry = cursor.result; if (entry) entry.update({ ...entry.value, offset: 1 }); }; }); db.close(); let rejected = false; try { await api.loadStoredResultSnapshot(); } catch { rejected = true; } return { originalId: before.id, rejected }; }); expect(outcome).toEqual({ originalId: resultSnapshot.id, rejected: true }); await other.close(); });