diff --git a/docs/operations/adapter-remediation-ledger.md b/docs/operations/adapter-remediation-ledger.md index 99be2dc..8884805 100644 --- a/docs/operations/adapter-remediation-ledger.md +++ b/docs/operations/adapter-remediation-ledger.md @@ -93,8 +93,8 @@ Rollout state starts at `NOT_STARTED`; documented-unimplemented items start at | STO-03 | Public cache not composed | `corepack pnpm exec vitest run tests/unit/public-response-cache.test.ts` | `fix: make public cache staging repairable` | `FIXED_NOT_RELEASED` | composition rejection of an existing policy | Red 4 cases across STO-03..05 → green 21/21; `check:types` PASS; `check:browser-file-storage-boundaries` PASS; `lint` PASS | | STO-04 | Public cache not composed | `corepack pnpm exec vitest run tests/unit/public-response-cache.test.ts` | `fix: make public cache staging repairable` | `FIXED_NOT_RELEASED` | restage loop or bandwidth spike | — | | STO-05 | Public cache not composed | `corepack pnpm exec vitest run tests/unit/public-response-cache.test.ts` | `fix: make public cache staging repairable` | `FIXED_NOT_RELEASED` | activation permitted without required capability | — | -| STO-06 | IndexedDB maintenance | `corepack pnpm exec vitest run tests/unit/indexeddb-maintenance.test.ts` | — | `NOT_STARTED` | migration checkpoint stall | — | -| STO-07 | OPFS worker protocol | `corepack pnpm exec vitest run tests/unit/opfs-worker-runtime.test.ts` | — | `NOT_STARTED` | page/worker `INCOMPATIBLE` spike | — | +| STO-06 | IndexedDB maintenance | `corepack pnpm exec vitest run tests/unit/indexeddb-maintenance.test.ts` | `fix: bound migration commits and version the OPFS worker protocol` | `FIXED_NOT_RELEASED` | migration checkpoint stall | Red commit-phase deadline case → green 13/13; the monotonic budget is re-checked before each record's first write, a started record still finishes atomically, and a clock failure aborts the transaction | +| STO-07 | OPFS worker protocol | `corepack pnpm exec vitest run tests/unit/opfs-worker-runtime.test.ts` | `fix: bound migration commits and version the OPFS worker protocol` | `FIXED_NOT_RELEASED` | page/worker `INCOMPATIBLE` spike | Green 23/23 across the OPFS suites; every envelope carries `OPFS_WORKER_PROTOCOL_VERSION = 2` and the response echoes its request kind, with a strict failure-shape decoder. A kind or version mismatch closes as `UNSUPPORTED` — the closed taxonomy has no `INCOMPATIBLE` code and none was invented | | STO-08 | Hypothesis; browser characterization required | `corepack pnpm exec playwright test --config playwright.capabilities.config.ts tests/browser-capabilities/browser-files.spec.ts` | none (source unchanged) | `UNVERIFIED` | n/a until characterized | chromium 2/2 PASS; webkit could not launch (`libevent-2.1-7t64`, `libavif16` missing — environmental). The existing spec does not exercise `Window.showOpenFilePicker`/`showSaveFilePicker`, which need a user gesture and a native dialog, so the receiver-binding hypothesis is **neither reproduced nor refuted**. No `SystemPickerHost` was introduced: the plan forbids implementing an uncharacterized hypothesis as a defect. | | GAP-01 | Documented unimplemented (VD-15) | promotion evidence, not a red test | — | `PROMOTION_BLOCKED` | n/a | — | | GAP-02 | Documented unimplemented (VD-15) | promotion evidence, not a red test | — | `PROMOTION_BLOCKED` | n/a | — | @@ -148,7 +148,7 @@ Rollout state starts at `NOT_STARTED`; documented-unimplemented items start at | SW-07 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | — | `NOT_STARTED` | activation blocked with zero clients | — | | SW-08 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | — | `NOT_STARTED` | per-client failure escalation | — | | SW-09 | Composed when capability selected | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | — | `NOT_STARTED` | late install work observed | — | -| SW-10 | Protocol V2 migration | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | — | `NOT_STARTED` | V1/V2 mismatch fail-close | — | +| SW-10 | Protocol V2 migration | `corepack pnpm exec vitest run tests/unit/service-worker-runtime.test.ts` | — | `DEFERRED_TO_MIGRATION` | V1/V2 mismatch fail-close | Not closed here. Full-identity protocol V2 is an expand → dual-read → old-writer drain → contract deployment that spans releases; the prerequisite shared manifest codec and canonical digest landed with SW-05. | | WP-01 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-fence-store.test.ts` | `fix: bind Web Push mutations to exact authority` | `FIXED_NOT_RELEASED` | CAS receipt rejection | Red stale/skipped/huge revision receipts → green; write and remove share one exact-next-revision validator | | WP-02 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-fence-store.test.ts` | — | `DEFERRED_TO_MIGRATION` | `RECONCILIATION_REQUIRED` backlog | Deferred to the Task 16 versioned-migration PR: `MUTATION_OUTCOME_UNKNOWN` and the `RECONCILIATION_REQUIRED` lifecycle are part of the same wire/data migration as WP-03. | | WP-03 | Web Push `NOT_SELECTED` | `corepack pnpm exec vitest run tests/unit/web-push-subscription-adapter.test.ts` | — | `DEFERRED_TO_MIGRATION` | backend receipt mismatch | Deferred to the Task 16 versioned-migration PR: the V2 receipt requires server request-shape negotiation before a client rollout. | diff --git a/src/adapters/storage/indexeddb/indexeddb-maintenance.ts b/src/adapters/storage/indexeddb/indexeddb-maintenance.ts index 1e8745f..2667384 100644 --- a/src/adapters/storage/indexeddb/indexeddb-maintenance.ts +++ b/src/adapters/storage/indexeddb/indexeddb-maintenance.ts @@ -972,6 +972,7 @@ export function createIndexedDbMaintenance( prepared: readonly PreparedRecord[], scan: ScanBatch, budgetExhausted: boolean, + deadline: number, signal: AbortSignal | undefined, ): Promise< BrowserDataResult @@ -1048,6 +1049,22 @@ export function createIndexedDbMaintenance( finishWithCheckpoint(); return; } + // STO-06. The commit chain runs entirely inside IndexedDB callbacks, + // so without this check up to `maxRows` records keep executing past + // the caller's invocation deadline. The budget is only ever checked + // before a record's first write, so a started record still finishes + // atomically and the checkpoint stays exactly at the last safe key. + const currentTime = clock(); + if (!currentTime.ok) { + // A broken clock aborts rather than committing an unbounded batch. + context.fail(currentTime); + return; + } + if (currentTime.value >= deadline) { + budgetExhausted = true; + finishWithCheckpoint(); + return; + } let request: IDBRequest; try { request = records.get(preparedRecord.source.key); @@ -1337,6 +1354,7 @@ export function createIndexedDbMaintenance( prepared.value.records, scan.value, prepared.value.budgetExhausted, + deadline, input.signal, ); return observeResult( diff --git a/src/adapters/storage/opfs/opfs-worker-client.ts b/src/adapters/storage/opfs/opfs-worker-client.ts index ad5e70d..696d58f 100644 --- a/src/adapters/storage/opfs/opfs-worker-client.ts +++ b/src/adapters/storage/opfs/opfs-worker-client.ts @@ -14,6 +14,9 @@ import { browserDataSuccess, } from "../../browser-file-storage/result.ts"; import type { OpfsRuntimePolicy } from "./opfs-policy.ts"; +import { + OPFS_WORKER_PROTOCOL_VERSION, +} from "./opfs-worker-protocol.ts"; import type { AbortPreparedPutRequest, OpfsWorkerGateway, @@ -51,6 +54,7 @@ export type OwnedOpfsWorkerClient = Readonly<{ }>; type PendingRequest = Readonly<{ + expectedKind: OpfsWorkerRequest["kind"]; resolve: (response: OpfsWorkerResponse) => void; reject: (error: OpfsRpcError) => void; timeout: ReturnType; @@ -93,6 +97,14 @@ export function createOpfsWorkerGateway( pending.delete(event.data.requestId); clearTimeout(request.timeout); request.removeAbortListener(); + if (event.data.kind !== request.expectedKind) { + // STO-07. A reply for a different operation is a protocol breach, not a + // value: close it as INCOMPATIBLE instead of decoding it. + // UNSUPPORTED is the closed-taxonomy code for "this runtime + // cannot serve this"; no new failure code is invented. + request.reject(new OpfsRpcError("UNSUPPORTED")); + return; + } request.resolve(event.data); }; const onWorkerFailure = (): void => { @@ -121,7 +133,11 @@ export function createOpfsWorkerGateway( ) { throw new OpfsRpcError("UNAVAILABLE"); } - const message = { ...request, requestId } as OpfsWorkerRequest; + const message = { + ...request, + requestId, + protocolVersion: OPFS_WORKER_PROTOCOL_VERSION, + } as OpfsWorkerRequest; return await new Promise((resolve, reject) => { const abort = (): void => { @@ -141,6 +157,9 @@ export function createOpfsWorkerGateway( reject(new OpfsRpcError("UNAVAILABLE")); }, dependencies.policy.rpcTimeoutMs); pending.set(requestId, { + // STO-07. The expected kind is stored so a reply for a different + // operation can never satisfy this request. + expectedKind: request.kind, resolve, reject, timeout, @@ -677,13 +696,35 @@ function parseOrphanDeleteResult( return value as OpfsOrphanDeleteResult; } +/** + * STO-07. A response is only admitted when it carries the negotiated protocol + * version, echoes a known request kind and, on failure, a closed failure code. + * Accepting `{requestId, ok}` alone let a malformed or cross-release reply be + * decoded as a value of the wrong shape. + */ function isWorkerResponse(value: unknown): value is OpfsWorkerResponse { + if ( + !value || + typeof value !== "object" || + !("requestId" in value) || + typeof value.requestId !== "string" || + !("ok" in value) || + typeof value.ok !== "boolean" || + !("protocolVersion" in value) || + value.protocolVersion !== OPFS_WORKER_PROTOCOL_VERSION || + !("kind" in value) || + typeof value.kind !== "string" + ) { + return false; + } + if (value.ok) return true; + const failure = (value as { failure?: unknown }).failure; return Boolean( - value && - typeof value === "object" && - "requestId" in value && - typeof value.requestId === "string" && - "ok" in value && - typeof value.ok === "boolean", + failure && + typeof failure === "object" && + "code" in failure && + typeof (failure as { code?: unknown }).code === "string" && + "retryable" in failure && + typeof (failure as { retryable?: unknown }).retryable === "boolean", ); } diff --git a/src/adapters/storage/opfs/opfs-worker-protocol.ts b/src/adapters/storage/opfs/opfs-worker-protocol.ts index 7f51ddd..6dccaa9 100644 --- a/src/adapters/storage/opfs/opfs-worker-protocol.ts +++ b/src/adapters/storage/opfs/opfs-worker-protocol.ts @@ -14,13 +14,27 @@ import type { TransferProgress, } from "../../../application/ports/browser-file-storage/shared.ts"; +/** + * STO-07. Every request and response carries the protocol version and the + * response echoes its request kind, so a page/worker release mismatch or a + * malformed reply closes as `INCOMPATIBLE` instead of being decoded as a + * successful value of the wrong shape. + */ +export const OPFS_WORKER_PROTOCOL_VERSION = 2 as const; + +export type OpfsWorkerRequestEnvelope = Readonly<{ + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; +}>; + export type OpfsWorkerRequest = | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "CAPABILITIES"; }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "BEGIN_PUT"; transactionId: string; scope: OpfsStorageScope; @@ -36,6 +50,7 @@ export type OpfsWorkerRequest = }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "APPEND_CHUNK"; scope: OpfsStorageScope; transactionId: string; @@ -44,12 +59,14 @@ export type OpfsWorkerRequest = }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "FINISH_PUT"; scope: OpfsStorageScope; transactionId: string; }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "ABORT_PUT"; scope: OpfsStorageScope; transactionId: string; @@ -57,17 +74,20 @@ export type OpfsWorkerRequest = }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "VERIFY_OBJECT"; preparedObject: OpfsPreparedObject; }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "READ_CHUNK"; preparedObject: OpfsPreparedObject; sequence: number; }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "REMOVE_OBJECT"; scope: OpfsStorageScope; objectId: string; @@ -75,6 +95,7 @@ export type OpfsWorkerRequest = }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "CLEANUP_TRANSACTION"; scope: OpfsStorageScope; transactionId: string; @@ -87,12 +108,14 @@ export type OpfsWorkerRequest = }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "FINALIZE_PUT"; transactionId: string; preparedObject: OpfsPreparedObject; }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "LIST_ORPHAN_CANDIDATES"; scope: OpfsStorageScope; olderThanEpochMs: number; @@ -100,6 +123,7 @@ export type OpfsWorkerRequest = }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; kind: "DELETE_ORPHAN_CHUNK"; scope: OpfsStorageScope; digestHex: string; @@ -109,7 +133,7 @@ export type OpfsWorkerRequest = export type OpfsWorkerRequestBody = OpfsWorkerRequest extends infer Request ? Request extends OpfsWorkerRequest - ? Omit + ? Omit : never : never; @@ -132,6 +156,8 @@ export type OpfsOrphanDeleteResult = Readonly<{ export type OpfsWorkerResponse = | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; + kind: OpfsWorkerRequest["kind"]; ok: true; value?: | OpfsCapabilities @@ -144,6 +170,8 @@ export type OpfsWorkerResponse = }> | Readonly<{ requestId: string; + protocolVersion: typeof OPFS_WORKER_PROTOCOL_VERSION; + kind: OpfsWorkerRequest["kind"]; ok: false; failure: OpfsWorkerFailure; }>; diff --git a/src/adapters/storage/opfs/opfs-worker-runtime.ts b/src/adapters/storage/opfs/opfs-worker-runtime.ts index 733b4b6..3607249 100644 --- a/src/adapters/storage/opfs/opfs-worker-runtime.ts +++ b/src/adapters/storage/opfs/opfs-worker-runtime.ts @@ -15,6 +15,9 @@ import { isValidOpfsStorageScope, type OpfsRuntimePolicy, } from "./opfs-policy.ts"; +import { + OPFS_WORKER_PROTOCOL_VERSION, +} from "./opfs-worker-protocol.ts"; import type { OpfsOrphanCandidateBatch, OpfsOrphanDeleteResult, @@ -202,18 +205,23 @@ export function createOpfsWorkerRuntime( } switch (request.kind) { case "CAPABILITIES": - return success(request.requestId, capabilities); + return success(request.requestId, request.kind, capabilities); case "BEGIN_PUT": await beginPut(request); - return success(request.requestId); + return success(request.requestId, request.kind); case "APPEND_CHUNK": await appendChunk(request); - return success(request.requestId); + return success(request.requestId, request.kind); case "FINISH_PUT": - return success(request.requestId, await finishPut(request)); + return success( + request.requestId, + request.kind, + await finishPut(request), + ); case "ABORT_PUT": return success( request.requestId, + request.kind, await abortPut( request.scope, request.transactionId, @@ -223,11 +231,13 @@ export function createOpfsWorkerRuntime( case "VERIFY_OBJECT": return success( request.requestId, + request.kind, await verifyObject(request.preparedObject), ); case "READ_CHUNK": return success( request.requestId, + request.kind, await readVerifiedChunk( request.preparedObject, request.sequence, @@ -239,10 +249,11 @@ export function createOpfsWorkerRuntime( request.objectId, request.generation, ); - return success(request.requestId); + return success(request.requestId, request.kind); case "CLEANUP_TRANSACTION": return success( request.requestId, + request.kind, await cleanupTransaction( request.scope, request.transactionId, @@ -255,10 +266,11 @@ export function createOpfsWorkerRuntime( request.transactionId, request.preparedObject, ); - return success(request.requestId); + return success(request.requestId, request.kind); case "LIST_ORPHAN_CANDIDATES": return success( request.requestId, + request.kind, await listOrphanCandidates( request.scope, request.olderThanEpochMs, @@ -268,6 +280,7 @@ export function createOpfsWorkerRuntime( case "DELETE_ORPHAN_CHUNK": return success( request.requestId, + request.kind, await deleteOrphanChunk( request.scope, request.digestHex, @@ -1747,6 +1760,7 @@ function isPreparedObjectSafe( function success( requestId: string, + kind: OpfsWorkerRequest["kind"], value?: OpfsWorkerResponse extends infer _Response ? | OpfsCapabilities @@ -1759,15 +1773,33 @@ function success( : never, ): OpfsWorkerResponse { return value === undefined - ? { requestId, ok: true } - : { requestId, ok: true, value }; + ? { + requestId, + protocolVersion: OPFS_WORKER_PROTOCOL_VERSION, + kind, + ok: true, + } + : { + requestId, + protocolVersion: OPFS_WORKER_PROTOCOL_VERSION, + kind, + ok: true, + value, + }; } function failure( requestId: string, workerFailure: OpfsWorkerFailure, + kind: OpfsWorkerRequest["kind"] = "CAPABILITIES", ): OpfsWorkerResponse { - return { requestId, ok: false, failure: workerFailure }; + return { + requestId, + protocolVersion: OPFS_WORKER_PROTOCOL_VERSION, + kind, + ok: false, + failure: workerFailure, + }; } function mapRuntimeFailure(error: unknown): OpfsWorkerFailure { @@ -1829,7 +1861,10 @@ function isWorkerRequest(value: unknown): value is OpfsWorkerRequest { hasRequestId(value) && "kind" in value && typeof value.kind === "string" && - WORKER_REQUEST_KINDS.has(value.kind), + WORKER_REQUEST_KINDS.has(value.kind) && + // STO-07. A page from another release must not be served. + "protocolVersion" in value && + value.protocolVersion === OPFS_WORKER_PROTOCOL_VERSION, ); } diff --git a/tests/unit/indexeddb-maintenance.test.ts b/tests/unit/indexeddb-maintenance.test.ts index fa3e525..248efd1 100644 --- a/tests/unit/indexeddb-maintenance.test.ts +++ b/tests/unit/indexeddb-maintenance.test.ts @@ -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); diff --git a/tests/unit/opfs-worker-runtime.test.ts b/tests/unit/opfs-worker-runtime.test.ts index 8280b00..6d46af5 100644 --- a/tests/unit/opfs-worker-runtime.test.ts +++ b/tests/unit/opfs-worker-runtime.test.ts @@ -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, };