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:
co-authored by
Claude Opus 5
parent
fce8e046ea
commit
f6098242be
@@ -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. |
|
||||
|
||||
@@ -972,6 +972,7 @@ export function createIndexedDbMaintenance<WireValue>(
|
||||
prepared: readonly PreparedRecord<WireValue>[],
|
||||
scan: ScanBatch,
|
||||
budgetExhausted: boolean,
|
||||
deadline: number,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<
|
||||
BrowserDataResult<IndexedDbMaintenanceBatchReceipt>
|
||||
@@ -1048,6 +1049,22 @@ export function createIndexedDbMaintenance<WireValue>(
|
||||
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<unknown>;
|
||||
try {
|
||||
request = records.get(preparedRecord.source.key);
|
||||
@@ -1337,6 +1354,7 @@ export function createIndexedDbMaintenance<WireValue>(
|
||||
prepared.value.records,
|
||||
scan.value,
|
||||
prepared.value.budgetExhausted,
|
||||
deadline,
|
||||
input.signal,
|
||||
);
|
||||
return observeResult(
|
||||
|
||||
@@ -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<typeof setTimeout>;
|
||||
@@ -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<OpfsWorkerResponse>((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",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Request, "requestId">
|
||||
? Omit<Request, "requestId" | "protocolVersion">
|
||||
: 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;
|
||||
}>;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user