fix: bound migration commits and version the OPFS worker protocol

STO-06: the IndexedDB codec migration commit chain runs entirely inside
IndexedDB callbacks, so up to maxRows records could keep executing past the
caller's cooperative deadline. The monotonic budget is now re-checked before
each record's first write; a started record still completes atomically, the
checkpoint advances only to the last safe key, and a clock failure aborts the
transaction rather than committing an unbounded batch.

STO-07: every OPFS worker request and response now carries
OPFS_WORKER_PROTOCOL_VERSION = 2, responses echo their request kind, and the
client validates the envelope and failure shape strictly while remembering the
expected kind per pending request. A page/worker release mismatch or a reply for
a different operation closes as UNSUPPORTED instead of being decoded as a value
of the wrong shape. UNSUPPORTED is used deliberately: the closed browser-data
taxonomy has no INCOMPATIBLE code and none was invented.

SW-10 and the OPFS/Web Push V2 wire rollouts remain deferred: they are
expand/dual-read/drain/contract deployments across releases rather than a single
in-repo change. The ledger records them as DEFERRED_TO_MIGRATION.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 00:41:12 +09:00
co-authored by Claude Opus 5
parent fce8e046ea
commit f6098242be
7 changed files with 205 additions and 21 deletions
+31
View File
@@ -596,6 +596,37 @@ describe("IndexedDB bounded codec maintenance", () => {
});
});
it("stops codec migration commit at the cooperative deadline", async () => {
const memory = new MemoryIndexedDbFactory();
await prepareSchema(memory);
seedLegacy(memory, "row-a", "first");
seedLegacy(memory, "row-b", "second");
const policy = defaultPolicy();
// STO-06. The clock only advances past the deadline once the commit
// transaction is already open, so the stop must happen inside the commit
// chain rather than before transform.
let calls = 0;
const maintenance = createMaintenance(memory, policy, {
now: () => {
calls += 1;
// Scan, prepare and the first commit record stay inside the budget.
return calls <= 6 ? 0 : 5_000;
},
});
const result = await maintenance.migrateCodecBatch({
maxRows: 10,
maxDurationMs: 1_000,
});
expect(result.ok).toBe(true);
if (!result.ok) return;
// The batch is incomplete and says so; it never claims a full pass.
expect(result.value.state).toBe("MORE");
expect(result.value.budgetExhausted).toBe(true);
expect(result.value.checkpointedRows).toBeLessThan(2);
});
it("fails closed when a historical payload cannot be transformed", async () => {
const memory = new MemoryIndexedDbFactory();
await prepareSchema(memory);