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);
+31
View File
@@ -15,6 +15,7 @@ import {
createOpfsWorkerGateway,
type OpfsWorkerLike,
} from "../../src/adapters/storage/opfs/opfs-worker-client.ts";
import { OPFS_WORKER_PROTOCOL_VERSION } from "../../src/adapters/storage/opfs/opfs-worker-protocol.ts";
import type {
OpfsWorkerRequest,
OpfsWorkerResponse,
@@ -181,6 +182,7 @@ function beginRequest(
): OpfsWorkerRequest {
return {
requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "BEGIN_PUT",
transactionId,
scope,
@@ -268,6 +270,7 @@ describe("OPFS dedicated worker runtime", () => {
await Promise.resolve();
const aborted = await runtime.handleRequest({
requestId: "request_abort_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "ABORT_PUT",
scope: scopeA,
transactionId: "transaction_12345678",
@@ -319,6 +322,7 @@ describe("OPFS dedicated worker runtime", () => {
const appending = runtime.handleRequest({
requestId: "request_append_5678",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "APPEND_CHUNK",
scope: scopeA,
transactionId: "transaction_56785678",
@@ -328,6 +332,7 @@ describe("OPFS dedicated worker runtime", () => {
await Promise.resolve();
const aborting = runtime.handleRequest({
requestId: "request_abort_5678",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "ABORT_PUT",
scope: scopeA,
transactionId: "transaction_56785678",
@@ -374,6 +379,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: `request_append_iso_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "APPEND_CHUNK",
scope: targetScope,
transactionId,
@@ -386,6 +392,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_abort_iso_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "ABORT_PUT",
scope: scopeA,
transactionId,
@@ -393,6 +400,7 @@ describe("OPFS dedicated worker runtime", () => {
).toMatchObject({ ok: true });
const finished = await runtime.handleRequest({
requestId: "request_finish_iso_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINISH_PUT",
scope: scopeB,
transactionId,
@@ -402,11 +410,14 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_verify_iso_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "VERIFY_OBJECT",
preparedObject,
}),
).toEqual({
requestId: "request_verify_iso_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: true,
});
@@ -435,6 +446,7 @@ describe("OPFS dedicated worker runtime", () => {
);
await runtime.handleRequest({
requestId: `request_gc_append_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "APPEND_CHUNK",
scope: targetScope,
transactionId,
@@ -444,6 +456,7 @@ describe("OPFS dedicated worker runtime", () => {
const object = preparedValue(
await runtime.handleRequest({
requestId: `request_gc_finish_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINISH_PUT",
scope: targetScope,
transactionId,
@@ -452,6 +465,7 @@ describe("OPFS dedicated worker runtime", () => {
preparedByScope.set(targetScope.authorityToken, object);
await runtime.handleRequest({
requestId: `request_gc_finalize_${index}`,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "FINALIZE_PUT",
transactionId,
preparedObject: object,
@@ -464,6 +478,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_gc_list_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "LIST_ORPHAN_CANDIDATES",
scope: scopeA,
olderThanEpochMs: cutoff,
@@ -473,6 +488,7 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_gc_delete_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "DELETE_ORPHAN_CHUNK",
scope: scopeA,
digestHex,
@@ -483,22 +499,28 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_gc_verify_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "VERIFY_OBJECT",
preparedObject: preparedByScope.get(scopeA.authorityToken)!,
}),
).toEqual({
requestId: "request_gc_verify_a",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: false,
});
expect(
await runtime.handleRequest({
requestId: "request_gc_verify_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "VERIFY_OBJECT",
preparedObject: preparedByScope.get(scopeB.authorityToken)!,
}),
).toEqual({
requestId: "request_gc_verify_b",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: true,
});
@@ -516,10 +538,13 @@ describe("OPFS dedicated worker runtime", () => {
expect(
await runtime.handleRequest({
requestId: "request_caps_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "CAPABILITIES",
}),
).toEqual({
requestId: "request_caps_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: expect.any(String),
ok: true,
value: {
available: false,
@@ -569,6 +594,8 @@ describe("OPFS worker client lifecycle", () => {
listener?.({
data: {
requestId: "request_collision_1234",
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: "CAPABILITIES",
ok: true,
value: {
available: true,
@@ -633,11 +660,15 @@ describe("OPFS worker client lifecycle", () => {
message.kind === "VERIFY_OBJECT"
? {
requestId: message.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: message.kind,
ok: true,
value: true,
}
: {
requestId: message.requestId,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind: message.kind,
ok: true,
value: new Uint8Array([4, 2]).buffer,
};