优化原生结果编码传输与浏览器缓存,记录八路性能基线
原生结果series通过字节索引直传,C端使用Ryu精确回读编码和64 KiB批量写出;网页采用Float64缓存和CSV工作线程,减少结果处理与保存等待。 补充八路AME曲线核查、全流程分阶段计时、独立编码基准和复现工具,固定后续优化采用修正八路及rtol=1e-8。C写出1.1808→0.1638 s,点击到可查看8.0100→6.9756 s。 验证:最终10项编码专项、29项相关后端回归通过;8份原生结果逐位一致,16次网页结果/CSV/刷新恢复通过。前端构建及缓存/CSV专项在本轮结果处理工作中通过。环境、原始大结果与临时构建不纳入Git。
This commit is contained in:
1 parent
808c484f5b
commit
3bc4be3c06
55 files changed
+20522
-141
No files matched your search
@@ -0,0 +1,208 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { buildResultCsvBlob } from "../../src/resultCsvEncoding";
|
||||
import { prepareApp, resultSnapshot } from "./fixtures";
|
||||
|
||||
test("CSV preserves BOM, CRLF, escaped raw keys and every binary64 value", async () => {
|
||||
const numbers = [-0, 0, Number.MIN_VALUE, -Number.MIN_VALUE, Number.MAX_VALUE, -Number.MAX_VALUE,
|
||||
1 + Number.EPSILON, 1e21, 1e-7, Math.PI, 403499745569.73364];
|
||||
const keys = ["time", '力,"端口"\r\nN'];
|
||||
const blob = buildResultCsvBlob(keys, numbers.length, new Float64Array([...numbers, ...numbers]));
|
||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
expect([...bytes.slice(0, 3)]).toEqual([0xef, 0xbb, 0xbf]);
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
const header = 'time,"力,""端口""\r\nN"\r\n';
|
||||
expect(text.startsWith(header)).toBe(true);
|
||||
const rows = text.slice(header.length).split("\r\n");
|
||||
expect(rows.pop()).toBe("");
|
||||
expect(rows).toHaveLength(numbers.length);
|
||||
rows.forEach((row, i) => row.split(",").forEach((value) => expect(Object.is(Number(value), numbers[i])).toBe(true)));
|
||||
expect(blob.type).toBe("text/csv;charset=utf-8");
|
||||
expect(() => buildResultCsvBlob(["time"], 1, new Float64Array([Infinity]))).toThrow("non-finite");
|
||||
expect(() => buildResultCsvBlob(["time"], 2, new Float64Array([0]))).toThrow("incomplete");
|
||||
});
|
||||
|
||||
async function probePage(page: Page) {
|
||||
await page.route("**/csv-worker-probe", (route) => route.fulfill({ contentType: "text/html", body: "<title>CSV worker probe</title>" }));
|
||||
await page.goto("/csv-worker-probe");
|
||||
}
|
||||
|
||||
test("real Worker exports over one million raw cells exactly in bounded transfers", async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
await probePage(page);
|
||||
let csvRequests = 0;
|
||||
page.on("request", (request) => { if (request.url().includes("/api/simulation-results/csv")) csvRequests++; });
|
||||
const result = await page.evaluate(async () => {
|
||||
// @ts-expect-error Vite serves this browser module.
|
||||
const { exportResultCsv } = await import("/src/resultCsvExport.ts");
|
||||
const OriginalWorker = Worker;
|
||||
let workerCount = 0, terminated = 0, maxTransferBytes = 0, transferCount = 0;
|
||||
window.Worker = class extends OriginalWorker {
|
||||
constructor(url: string | URL, options?: WorkerOptions) { super(url, options); workerCount++; }
|
||||
postMessage(message: any, transfer?: any) {
|
||||
if (message.type === "chunk") { maxTransferBytes = Math.max(maxTransferBytes, message.values.byteLength); transferCount++; }
|
||||
super.postMessage(message, transfer);
|
||||
}
|
||||
terminate() { terminated++; super.terminate(); }
|
||||
};
|
||||
const count = 4097;
|
||||
const variables = Array.from({ length: 256 }, (_, i) => ({ key: `component_${255-i}.pressure`, unit: "Pa", order: i }));
|
||||
const series: Record<string, number[]> = { time: Array.from({ length: count }, (_, i) => i / 1000) };
|
||||
series.time[1] = 0; // Duplicate and event samples are data, not rows to remove.
|
||||
series.time[2100] = 2.099000001;
|
||||
for (let column = variables.length - 1; column >= 0; column--) {
|
||||
series[variables[column].key] = Array.from({ length: count }, (_, i) => Math.sin(i / 17) * (column + 1));
|
||||
}
|
||||
const extremes = [-0, Number.MIN_VALUE, Number.MAX_VALUE, 1 + Number.EPSILON, 403499745569.73364];
|
||||
extremes.forEach((value, i) => { series[variables[0].key][i] = value; });
|
||||
let ticks = 0;
|
||||
const timer = setInterval(() => ticks++, 0);
|
||||
const started = performance.now();
|
||||
const blob = await exportResultCsv({ variables, series });
|
||||
const elapsedMs = performance.now() - started;
|
||||
clearInterval(timer);
|
||||
window.Worker = OriginalWorker;
|
||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
const lines = text.split("\r\n");
|
||||
const expectedKeys = ["time", ...variables.map((v) => v.key)];
|
||||
const ordered = lines.shift() === expectedKeys.join(",");
|
||||
const trailingNewline = lines.pop() === "";
|
||||
let checked = 0, exact = true;
|
||||
lines.forEach((line, row) => line.split(",").forEach((value, column) => {
|
||||
exact &&= Object.is(Number(value), series[expectedKeys[column]][row]); checked++;
|
||||
}));
|
||||
return { ordered, trailingNewline, exact, checked, rowCount: lines.length,
|
||||
bom: [...bytes.slice(0,3)], workerCount, terminated, maxTransferBytes, transferCount, ticks, elapsedMs };
|
||||
});
|
||||
expect(result).toMatchObject({ ordered: true, trailingNewline: true, exact: true, checked: 257 * 4097,
|
||||
rowCount: 4097, bom: [239,187,191], workerCount: 1, terminated: 1 });
|
||||
expect(result.maxTransferBytes).toBeLessThanOrEqual(1024 * 1024);
|
||||
expect(result.transferCount).toBeGreaterThan(1);
|
||||
expect(result.ticks).toBeGreaterThan(1);
|
||||
expect(csvRequests).toBe(0);
|
||||
console.log(JSON.stringify({ probe: "csv-worker-roundtrip", ...result }));
|
||||
});
|
||||
|
||||
test("CSV rejects bad metadata/data and releases Workers after failure or cancellation", async ({ page }) => {
|
||||
await probePage(page);
|
||||
const result = await page.evaluate(async () => {
|
||||
// @ts-expect-error Vite serves this browser module.
|
||||
const { exportResultCsv, resultCsvFilename } = await import("/src/resultCsvExport.ts");
|
||||
const OriginalWorker = Worker;
|
||||
let created = 0, terminated = 0;
|
||||
window.Worker = class extends OriginalWorker {
|
||||
constructor(url: string | URL, options?: WorkerOptions) { super(url, options); created++; }
|
||||
terminate() { terminated++; super.terminate(); }
|
||||
};
|
||||
const good = () => ({ variables: [{ key: "p" }], series: { time: [0,1], p: [1,2] } });
|
||||
const errors: string[] = [];
|
||||
for (const input of [
|
||||
{ variables: [{ key: "p" }], series: { p: [1] } },
|
||||
{ variables: [], series: { time: [0] } },
|
||||
{ variables: [{ key: "p" }, { key: "p" }], series: { time: [0], p: [1] } },
|
||||
{ variables: [{ key: "p" }], series: { time: [0], extra: [1] } },
|
||||
{ variables: [{ key: "p" }], series: { time: [0], p: [1,2] } },
|
||||
{ variables: [{ key: "p" }], series: { time: [0], p: [Infinity] } },
|
||||
{ variables: [{ key: "p" }], series: { time: [0], p: [null] } },
|
||||
]) {
|
||||
try { await exportResultCsv(input); errors.push("unexpected success"); }
|
||||
catch (error) { errors.push((error as Error).message); }
|
||||
}
|
||||
const aborted = new AbortController(); aborted.abort();
|
||||
const names: string[] = [];
|
||||
try { await exportResultCsv(good(), aborted.signal); } catch (error) { names.push((error as Error).name); }
|
||||
const running = new AbortController();
|
||||
const task = exportResultCsv(good(), running.signal);
|
||||
running.abort();
|
||||
try { await task; } catch (error) { names.push((error as Error).name); }
|
||||
await exportResultCsv(good()); // Failure and abort must not poison a retry.
|
||||
window.Worker = OriginalWorker;
|
||||
return { errors, names, created, terminated,
|
||||
filename: resultCsvFilename(' .储气:/系统. '), unicodeLength: resultCsvFilename("😀".repeat(90)).split("-results")[0].length };
|
||||
});
|
||||
expect(result.errors[0]).toContain("non-empty time");
|
||||
expect(result.errors[1]).toContain("exportable variables");
|
||||
expect(result.errors[2]).toContain("duplicate");
|
||||
expect(result.errors[3]).toContain("missing p; unmapped extra");
|
||||
expect(result.errors[4]).toContain("inconsistent length");
|
||||
expect(result.errors[5]).toContain("non-finite");
|
||||
expect(result.errors[6]).toContain("non-numeric");
|
||||
expect(result.names).toEqual(["AbortError", "AbortError"]);
|
||||
expect(result.created).toBe(4);
|
||||
expect(result.terminated).toBe(result.created);
|
||||
expect(result.filename).toBe("储气__系统-results.csv");
|
||||
expect(result.unicodeLength).toBe(160);
|
||||
});
|
||||
|
||||
async function openResults(page: Page) {
|
||||
await prepareApp(page);
|
||||
await page.addInitScript((snapshot) => {
|
||||
sessionStorage.setItem("system-simulation-flow:latest-result", JSON.stringify(snapshot));
|
||||
}, resultSnapshot);
|
||||
await page.goto("/");
|
||||
await page.getByRole("tab", { name: "结果", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "下载结果 CSV", exact: true })).toBeEnabled();
|
||||
}
|
||||
|
||||
test("CSV toolbar downloads current raw data locally without HTTP", async ({ page }) => {
|
||||
await openResults(page);
|
||||
let requests = 0;
|
||||
page.on("request", (request) => { if (request.url().includes("/api/simulation-results/csv")) requests++; });
|
||||
const downloading = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "下载结果 CSV", exact: true }).click();
|
||||
const download = await downloading;
|
||||
const stream = await download.createReadStream();
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of stream!) chunks.push(Buffer.from(chunk));
|
||||
expect(Buffer.concat(chunks).toString("utf8")).toBe("\ufefftime,generic_sensor_1.value\r\n0,0\r\n5,0.5\r\n10,1\r\n");
|
||||
await expect(page.getByRole("button", { name: "下载结果 CSV", exact: true })).toBeEnabled();
|
||||
expect(requests).toBe(0);
|
||||
});
|
||||
|
||||
async function installHeldWorker(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
const original = Worker;
|
||||
(window as any).__csvWorkers = [];
|
||||
window.Worker = function(url: string | URL, options?: WorkerOptions) {
|
||||
if (!String(url).includes("resultCsv.worker")) return new original(url, options);
|
||||
const worker = { onmessage: null, onerror: null, onmessageerror: null, terminated: 0,
|
||||
postMessage() {}, terminate() { this.terminated++; } };
|
||||
(window as any).__csvWorkers.push(worker);
|
||||
return worker;
|
||||
} as any;
|
||||
});
|
||||
}
|
||||
|
||||
test("CSV toolbar blocks duplicate clicks and cancels a download when leaving results", async ({ page }) => {
|
||||
await openResults(page);
|
||||
await installHeldWorker(page);
|
||||
const button = page.getByRole("button", { name: "下载结果 CSV", exact: true });
|
||||
let downloads = 0;
|
||||
page.on("download", () => downloads++);
|
||||
await button.evaluate((element: HTMLButtonElement) => { element.click(); element.click(); });
|
||||
await expect(button).toBeDisabled();
|
||||
expect(await page.evaluate(() => (window as any).__csvWorkers.length)).toBe(1);
|
||||
await page.getByRole("tab", { name: "建模", exact: true }).click();
|
||||
expect(await page.evaluate(() => (window as any).__csvWorkers[0].terminated)).toBe(1);
|
||||
await page.evaluate(() => (window as any).__csvWorkers[0].onmessage({ data: { type: "complete", blob: new Blob(["stale"]) } }));
|
||||
expect(downloads).toBe(0);
|
||||
await page.getByRole("tab", { name: "结果", exact: true }).click();
|
||||
await expect(button).toBeEnabled();
|
||||
});
|
||||
|
||||
test("CSV Worker errors release resources and allow the toolbar to retry", async ({ page }) => {
|
||||
await openResults(page);
|
||||
await installHeldWorker(page);
|
||||
const button = page.getByRole("button", { name: "下载结果 CSV", exact: true });
|
||||
await button.click();
|
||||
const dialog = page.waitForEvent("dialog");
|
||||
const fail = page.evaluate(() => (window as any).__csvWorkers[0].onerror({ message: "injected worker error", preventDefault() {} }));
|
||||
const alert = await dialog;
|
||||
expect(alert.message()).toBe("CSV 下载失败:injected worker error");
|
||||
await alert.accept();
|
||||
await fail;
|
||||
await expect(button).toBeEnabled();
|
||||
expect(await page.evaluate(() => (window as any).__csvWorkers[0].terminated)).toBe(1);
|
||||
await button.click();
|
||||
expect(await page.evaluate(() => (window as any).__csvWorkers.length)).toBe(2);
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
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: "<title>Persistence probe</title>" }));
|
||||
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<string, number[]> = { 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<typeof originalTransaction>) {
|
||||
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<typeof originalPut>) {
|
||||
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<IDBDatabase>((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<void>((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<typeof originalPut>) {
|
||||
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<boolean> | undefined;
|
||||
let armed = true;
|
||||
IDBDatabase.prototype.transaction = function (...args: Parameters<typeof originalTransaction>) {
|
||||
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<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open("system-simulation-results", 1);
|
||||
request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
|
||||
});
|
||||
await new Promise<void>((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();
|
||||
});
|
||||
Reference in new issue
Block a user