chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type {
|
||||
OpfsPreparedObject,
|
||||
OpfsPhysicalGenerationId,
|
||||
OpfsStorageScope,
|
||||
} from "../../src/application/ports/browser-file-storage/opfs-ports.ts";
|
||||
import type {
|
||||
@@ -14,13 +15,16 @@ 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,
|
||||
} from "../../src/adapters/storage/opfs/opfs-worker-protocol.ts";
|
||||
import {
|
||||
createOpfsWorkerRuntime,
|
||||
startBrowserOpfsDedicatedWorker,
|
||||
type OpfsMutationLeaseManager,
|
||||
type OpfsWorkerMessageHost,
|
||||
} from "../../src/adapters/storage/opfs/opfs-worker-runtime.ts";
|
||||
|
||||
const scopeA: OpfsStorageScope = Object.freeze({
|
||||
@@ -170,18 +174,23 @@ function notFound(): DOMException {
|
||||
return new DOMException("Entry was not found.", "NotFoundError");
|
||||
}
|
||||
|
||||
const PHYSICAL_GENERATION_A = "a".repeat(32) as OpfsPhysicalGenerationId;
|
||||
|
||||
function beginRequest(
|
||||
requestId: string,
|
||||
transactionId: string,
|
||||
scope: OpfsStorageScope,
|
||||
physicalGenerationId: OpfsPhysicalGenerationId = PHYSICAL_GENERATION_A,
|
||||
): OpfsWorkerRequest {
|
||||
return {
|
||||
requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "BEGIN_PUT",
|
||||
transactionId,
|
||||
scope,
|
||||
objectId: "object_12345678",
|
||||
generation: 1,
|
||||
physicalGenerationId,
|
||||
declaredByteLength: 1,
|
||||
mediaType: "application/octet-stream",
|
||||
createdAtEpochMs: 100,
|
||||
@@ -263,6 +272,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",
|
||||
@@ -314,6 +324,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",
|
||||
@@ -323,6 +334,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",
|
||||
@@ -369,6 +381,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,
|
||||
@@ -381,6 +394,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,
|
||||
@@ -388,6 +402,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,
|
||||
@@ -397,11 +412,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,
|
||||
});
|
||||
@@ -430,6 +448,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,
|
||||
@@ -439,6 +458,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,
|
||||
@@ -447,6 +467,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,
|
||||
@@ -459,6 +480,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,
|
||||
@@ -468,6 +490,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,
|
||||
@@ -478,22 +501,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,
|
||||
});
|
||||
@@ -511,10 +540,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,
|
||||
@@ -564,6 +596,8 @@ describe("OPFS worker client lifecycle", () => {
|
||||
listener?.({
|
||||
data: {
|
||||
requestId: "request_collision_1234",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "CAPABILITIES",
|
||||
ok: true,
|
||||
value: {
|
||||
available: true,
|
||||
@@ -628,11 +662,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,
|
||||
};
|
||||
@@ -728,3 +766,591 @@ describe("OPFS worker client lifecycle", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* STO-RR-01. A Web Lock is not reentrant. Any path that re-acquires the origin
|
||||
* mutation lease while already holding it stops making progress forever, and a
|
||||
* lock the runtime waits on cannot be observed by a fake that hands out an
|
||||
* unlimited number of leases.
|
||||
*/
|
||||
function strictNonReentrantLeases(
|
||||
counters: { acquires: number; releases: number },
|
||||
): OpfsMutationLeaseManager {
|
||||
let held = false;
|
||||
return {
|
||||
async acquire() {
|
||||
if (held) {
|
||||
// A second holder waits for the first to release. Nothing here ever
|
||||
// does, which is exactly what a deadlock looks like.
|
||||
return await new Promise<never>(() => {});
|
||||
}
|
||||
held = true;
|
||||
counters.acquires += 1;
|
||||
let released = false;
|
||||
return {
|
||||
release() {
|
||||
if (released) return;
|
||||
released = true;
|
||||
held = false;
|
||||
counters.releases += 1;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withTimeout<Value>(
|
||||
operation: Promise<Value>,
|
||||
label: string,
|
||||
ms = 200,
|
||||
): Promise<Value> {
|
||||
return Promise.race([
|
||||
operation,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
setTimeout(() => reject(new Error(`${label} did not settle`)), ms);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
describe("STO-RR-01 OPFS finalization under a non-reentrant lock", () => {
|
||||
async function completedPut(
|
||||
runtime: ReturnType<typeof createOpfsWorkerRuntime>,
|
||||
transactionId: string,
|
||||
): Promise<OpfsPreparedObject> {
|
||||
expect(
|
||||
await runtime.handleRequest(
|
||||
beginRequest(`request_begin_${transactionId}`, transactionId, scopeA),
|
||||
),
|
||||
).toMatchObject({ ok: true });
|
||||
expect(
|
||||
await runtime.handleRequest({
|
||||
requestId: `request_append_${transactionId}`,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "APPEND_CHUNK",
|
||||
scope: scopeA,
|
||||
transactionId,
|
||||
sequence: 0,
|
||||
bytes: new Uint8Array([9]).buffer,
|
||||
}),
|
||||
).toMatchObject({ ok: true });
|
||||
const finished = await runtime.handleRequest({
|
||||
requestId: `request_finish_${transactionId}`,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "FINISH_PUT",
|
||||
scope: scopeA,
|
||||
transactionId,
|
||||
});
|
||||
expect(finished).toMatchObject({ ok: true });
|
||||
return preparedValue(finished);
|
||||
}
|
||||
|
||||
it("finalizes a normal PUT with exactly one lock acquisition", async () => {
|
||||
const root = new MemoryDirectory();
|
||||
const counters = { acquires: 0, releases: 0 };
|
||||
const runtime = createOpfsWorkerRuntime({
|
||||
root: root as unknown as FileSystemDirectoryHandle,
|
||||
crypto: globalThis.crypto,
|
||||
policy: runtimePolicy,
|
||||
leaseManager: strictNonReentrantLeases(counters),
|
||||
dedicatedWorker: true,
|
||||
supportsSynchronousAccessHandles: false,
|
||||
});
|
||||
const transactionId = "transaction_final_0001";
|
||||
const prepared = await completedPut(runtime, transactionId);
|
||||
const before = counters.acquires;
|
||||
|
||||
const finalized = await withTimeout(
|
||||
runtime.handleRequest({
|
||||
requestId: "request_finalize_0001",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "FINALIZE_PUT",
|
||||
transactionId,
|
||||
preparedObject: prepared,
|
||||
}),
|
||||
"FINALIZE_PUT",
|
||||
);
|
||||
|
||||
expect(finalized).toMatchObject({ ok: true });
|
||||
expect(counters.acquires - before).toBe(1);
|
||||
expect(counters.acquires).toBe(counters.releases);
|
||||
expect(
|
||||
root.has([
|
||||
"authorities",
|
||||
scopeA.authorityToken,
|
||||
scopeA.namespaceToken,
|
||||
scopeA.partitionToken,
|
||||
"staging",
|
||||
transactionId,
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves the lock free for the next mutation after a finalized PUT", async () => {
|
||||
const root = new MemoryDirectory();
|
||||
const counters = { acquires: 0, releases: 0 };
|
||||
const runtime = createOpfsWorkerRuntime({
|
||||
root: root as unknown as FileSystemDirectoryHandle,
|
||||
crypto: globalThis.crypto,
|
||||
policy: runtimePolicy,
|
||||
leaseManager: strictNonReentrantLeases(counters),
|
||||
dedicatedWorker: true,
|
||||
supportsSynchronousAccessHandles: false,
|
||||
});
|
||||
const first = "transaction_final_0002";
|
||||
const prepared = await completedPut(runtime, first);
|
||||
await withTimeout(
|
||||
runtime.handleRequest({
|
||||
requestId: "request_finalize_0002",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "FINALIZE_PUT",
|
||||
transactionId: first,
|
||||
preparedObject: prepared,
|
||||
}),
|
||||
"first FINALIZE_PUT",
|
||||
);
|
||||
|
||||
const second = "transaction_final_0003";
|
||||
await expect(
|
||||
withTimeout(completedPut(runtime, second), "second PUT"),
|
||||
).resolves.toMatchObject({ descriptor: { objectId: "object_12345678" } });
|
||||
expect(counters.acquires).toBe(counters.releases);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* STO-RR-02. A failure raised while serving a validated request must answer
|
||||
* that request. Defaulting the response kind to `CAPABILITIES` made the client's
|
||||
* own expected-kind check reject it as a protocol breach, so a quota or
|
||||
* integrity failure reached the caller as `UNSUPPORTED`.
|
||||
*/
|
||||
describe("STO-RR-02 worker failure responses echo the request kind", () => {
|
||||
const failingRoot = {
|
||||
async getDirectoryHandle(): Promise<FileSystemDirectoryHandle> {
|
||||
throw new DOMException("Out of room", "QuotaExceededError");
|
||||
},
|
||||
async getFileHandle(): Promise<FileSystemFileHandle> {
|
||||
throw new DOMException("Out of room", "QuotaExceededError");
|
||||
},
|
||||
async removeEntry(): Promise<void> {
|
||||
throw new DOMException("Out of room", "QuotaExceededError");
|
||||
},
|
||||
async *entries(): AsyncIterableIterator<never> {},
|
||||
} as unknown as FileSystemDirectoryHandle;
|
||||
|
||||
it("keeps the validated kind on every failure path", async () => {
|
||||
const counters = { acquires: 0, releases: 0 };
|
||||
const runtime = createOpfsWorkerRuntime({
|
||||
root: failingRoot,
|
||||
crypto: globalThis.crypto,
|
||||
policy: runtimePolicy,
|
||||
leaseManager: strictNonReentrantLeases(counters),
|
||||
dedicatedWorker: true,
|
||||
supportsSynchronousAccessHandles: false,
|
||||
});
|
||||
|
||||
const requests: readonly OpfsWorkerRequest[] = [
|
||||
beginRequest("request_kind_begin", "transaction_kind_0001", scopeA),
|
||||
{
|
||||
requestId: "request_kind_remove",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "REMOVE_OBJECT",
|
||||
scope: scopeA,
|
||||
objectId: "object_12345678",
|
||||
generation: 1,
|
||||
},
|
||||
{
|
||||
requestId: "request_kind_cleanup",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "CLEANUP_TRANSACTION",
|
||||
scope: scopeA,
|
||||
transactionId: "transaction_kind_0001",
|
||||
},
|
||||
{
|
||||
requestId: "request_kind_orphans",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "LIST_ORPHAN_CANDIDATES",
|
||||
scope: scopeA,
|
||||
olderThanEpochMs: 1,
|
||||
maxEntries: 1,
|
||||
},
|
||||
];
|
||||
|
||||
for (const request of requests) {
|
||||
const response = await runtime.handleRequest(request);
|
||||
expect(response).toMatchObject({
|
||||
ok: false,
|
||||
kind: request.kind,
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("still reports a protocol-level failure for an unreadable envelope", async () => {
|
||||
const counters = { acquires: 0, releases: 0 };
|
||||
const runtime = createOpfsWorkerRuntime({
|
||||
root: failingRoot,
|
||||
crypto: globalThis.crypto,
|
||||
policy: runtimePolicy,
|
||||
leaseManager: strictNonReentrantLeases(counters),
|
||||
dedicatedWorker: true,
|
||||
supportsSynchronousAccessHandles: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
await runtime.handleRequest({
|
||||
requestId: "request_kind_broken",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "NOT_A_KIND",
|
||||
}),
|
||||
).toMatchObject({ ok: false, kind: "CAPABILITIES" });
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* STO-RR-03. The client decoder is the trust boundary for anything a worker
|
||||
* says. A `code` that is merely a string lets an arbitrary value escape the
|
||||
* closed `BrowserDataFailure` taxonomy into application code.
|
||||
*/
|
||||
describe("STO-RR-03 worker responses are decoded against closed sets", () => {
|
||||
function respondingWorker(
|
||||
reply: (request: OpfsWorkerRequest) => unknown,
|
||||
): OpfsWorkerLike {
|
||||
const listeners = new Set<(event: MessageEvent<unknown>) => void>();
|
||||
return {
|
||||
postMessage(message: unknown) {
|
||||
const response = reply(message as OpfsWorkerRequest);
|
||||
queueMicrotask(() => {
|
||||
for (const listener of listeners) {
|
||||
listener({ data: response } as MessageEvent<unknown>);
|
||||
}
|
||||
});
|
||||
},
|
||||
addEventListener(_type: "message", listener: (event: MessageEvent<unknown>) => void) {
|
||||
listeners.add(listener);
|
||||
},
|
||||
removeEventListener(_type: "message", listener: (event: MessageEvent<unknown>) => void) {
|
||||
listeners.delete(listener);
|
||||
},
|
||||
} as unknown as OpfsWorkerLike;
|
||||
}
|
||||
|
||||
const hostileReplies: readonly (readonly [string, (request: OpfsWorkerRequest) => unknown])[] = [
|
||||
[
|
||||
"unknown failure code",
|
||||
(request) => ({
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: false,
|
||||
failure: { code: "EVIL", retryable: false },
|
||||
}),
|
||||
],
|
||||
[
|
||||
"unknown request kind",
|
||||
(request) => ({
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: "NOT_A_KIND",
|
||||
ok: false,
|
||||
failure: { code: "UNAVAILABLE", retryable: false },
|
||||
}),
|
||||
],
|
||||
[
|
||||
"non-boolean retryable",
|
||||
(request) => ({
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: false,
|
||||
failure: { code: "UNAVAILABLE", retryable: "yes" },
|
||||
}),
|
||||
],
|
||||
[
|
||||
"inherited failure fields",
|
||||
(request) => ({
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: false,
|
||||
failure: Object.create({ code: "UNAVAILABLE", retryable: false }) as object,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"throwing getter",
|
||||
(request) => {
|
||||
const response: Record<string, unknown> = {
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
ok: false,
|
||||
failure: { code: "UNAVAILABLE", retryable: false },
|
||||
};
|
||||
Object.defineProperty(response, "kind", {
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
throw new TypeError("hostile getter");
|
||||
},
|
||||
});
|
||||
return response;
|
||||
},
|
||||
],
|
||||
[
|
||||
"extra own field",
|
||||
(request) => ({
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: false,
|
||||
failure: { code: "UNAVAILABLE", retryable: false, injected: 1 },
|
||||
}),
|
||||
],
|
||||
];
|
||||
|
||||
for (const [label, reply] of hostileReplies) {
|
||||
it(`closes a ${label} as UNSUPPORTED without rejecting`, async () => {
|
||||
const gateway = createOpfsWorkerGateway({
|
||||
worker: respondingWorker(reply),
|
||||
policy: runtimePolicy,
|
||||
createRequestId: () => `request_hostile_${label.replace(/\W/gu, "")}`,
|
||||
});
|
||||
const result = await withTimeout(gateway.capabilities(), label);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.ok ? null : result.error.code).toBe("UNSUPPORTED");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* NS-06. Correlation was read separately and the pending row and its timer
|
||||
* were removed before the reply was decoded. A trap that threw inside the
|
||||
* decoder therefore left the public promise pending forever, and a throwing
|
||||
* `requestId` getter produced a timeout instead of a prompt protocol failure.
|
||||
*/
|
||||
const uncorrelatableReplies: readonly (readonly [
|
||||
string,
|
||||
(request: OpfsWorkerRequest) => unknown,
|
||||
])[] = [
|
||||
[
|
||||
"throwing requestId getter",
|
||||
(request) =>
|
||||
Object.defineProperty(
|
||||
{
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: true,
|
||||
value: {},
|
||||
},
|
||||
"requestId",
|
||||
{
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
throw new TypeError("hostile requestId getter");
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
[
|
||||
"throwing ownKeys trap",
|
||||
(request) =>
|
||||
new Proxy(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: true,
|
||||
value: {},
|
||||
},
|
||||
{
|
||||
ownKeys() {
|
||||
throw new TypeError("hostile ownKeys trap");
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
[
|
||||
"descriptor trap that throws after correlation",
|
||||
(request) => {
|
||||
let reads = 0;
|
||||
return new Proxy(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: true,
|
||||
value: {},
|
||||
},
|
||||
{
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
reads += 1;
|
||||
if (reads > 1) {
|
||||
throw new TypeError("stateful descriptor trap");
|
||||
}
|
||||
return Reflect.getOwnPropertyDescriptor(target, key);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
],
|
||||
[
|
||||
"symbol-keyed field",
|
||||
(request) => ({
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: true,
|
||||
value: {},
|
||||
[Symbol.for("injected")]: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"non-enumerable own field",
|
||||
(request) =>
|
||||
Object.defineProperty(
|
||||
{
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: true,
|
||||
value: {},
|
||||
},
|
||||
"injected",
|
||||
{ enumerable: false, value: true },
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
for (const [label, reply] of uncorrelatableReplies) {
|
||||
it(`closes a ${label} promptly as UNSUPPORTED`, async () => {
|
||||
const gateway = createOpfsWorkerGateway({
|
||||
worker: respondingWorker(reply),
|
||||
policy: { ...runtimePolicy, rpcTimeoutMs: 60_000 },
|
||||
createRequestId: () => `request_uncorr_${label.replace(/\W/gu, "")}`,
|
||||
});
|
||||
// The RPC timeout is far beyond the test budget, so a pass here means the
|
||||
// reply itself closed the request rather than the timer.
|
||||
const result = await withTimeout(gateway.capabilities(), label);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.ok ? null : result.error.code).toBe("UNSUPPORTED");
|
||||
});
|
||||
}
|
||||
|
||||
it("keeps serving requests after a malformed reply", async () => {
|
||||
let replies = 0;
|
||||
const gateway = createOpfsWorkerGateway({
|
||||
worker: respondingWorker((request) => {
|
||||
replies += 1;
|
||||
if (replies === 1) return { requestId: request.requestId };
|
||||
return {
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: false,
|
||||
failure: { code: "QUOTA_EXCEEDED", retryable: true },
|
||||
};
|
||||
}),
|
||||
policy: { ...runtimePolicy, rpcTimeoutMs: 60_000 },
|
||||
createRequestId: () => `request_sequence_${replies}`,
|
||||
});
|
||||
|
||||
const first = await withTimeout(gateway.capabilities(), "first");
|
||||
expect(first.ok ? null : first.error.code).toBe("UNSUPPORTED");
|
||||
const second = await withTimeout(gateway.capabilities(), "second");
|
||||
expect(second.ok ? null : second.error.code).toBe("QUOTA_EXCEEDED");
|
||||
});
|
||||
|
||||
it("still admits a well-formed closed failure", async () => {
|
||||
const gateway = createOpfsWorkerGateway({
|
||||
worker: respondingWorker((request) => ({
|
||||
requestId: request.requestId,
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind: request.kind,
|
||||
ok: false,
|
||||
failure: { code: "QUOTA_EXCEEDED", retryable: true },
|
||||
})),
|
||||
policy: runtimePolicy,
|
||||
createRequestId: () => "request_wellformed_1234",
|
||||
});
|
||||
const result = await withTimeout(gateway.capabilities(), "well-formed");
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.ok ? null : result.error.code).toBe("QUOTA_EXCEEDED");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* NS-05. When the runtime never came up, the message host answered with a
|
||||
* default `CAPABILITIES` kind. The gateway saw that as an expected-kind
|
||||
* mismatch and replaced the real cause — `BLOCKED`, `QUOTA_EXCEEDED` — with a
|
||||
* generic `UNSUPPORTED` protocol breach, so the outage was misreported.
|
||||
*/
|
||||
describe("NS-05 a bootstrap failure answers the request it belongs to", () => {
|
||||
function hostFor(): Readonly<{
|
||||
host: OpfsWorkerMessageHost;
|
||||
posted: OpfsWorkerResponse[];
|
||||
deliver(message: unknown): void;
|
||||
}> {
|
||||
const listeners: ((event: MessageEvent<unknown>) => void)[] = [];
|
||||
const posted: OpfsWorkerResponse[] = [];
|
||||
return {
|
||||
host: {
|
||||
addEventListener(_type, listener) {
|
||||
listeners.push(listener);
|
||||
},
|
||||
postMessage(message) {
|
||||
posted.push(message);
|
||||
},
|
||||
},
|
||||
posted,
|
||||
deliver(message: unknown) {
|
||||
for (const listener of listeners) {
|
||||
listener({ data: message } as MessageEvent<unknown>);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const requestKinds = [
|
||||
"CAPABILITIES",
|
||||
"BEGIN_PUT",
|
||||
"APPEND_CHUNK",
|
||||
"FINISH_PUT",
|
||||
"ABORT_PUT",
|
||||
"VERIFY_OBJECT",
|
||||
"READ_CHUNK",
|
||||
"REMOVE_OBJECT",
|
||||
"CLEANUP_TRANSACTION",
|
||||
"FINALIZE_PUT",
|
||||
"LIST_ORPHAN_CANDIDATES",
|
||||
"DELETE_ORPHAN_CHUNK",
|
||||
] as const;
|
||||
|
||||
for (const kind of requestKinds) {
|
||||
it(`preserves the ${kind} correlation when bootstrap fails`, async () => {
|
||||
const { host, posted, deliver } = hostFor();
|
||||
const start = startBrowserOpfsDedicatedWorker(host, {
|
||||
storageManager: {
|
||||
getDirectory: () =>
|
||||
Promise.reject(
|
||||
new DOMException("blocked", "SecurityError"),
|
||||
),
|
||||
} as unknown as StorageManager,
|
||||
crypto: globalThis.crypto,
|
||||
});
|
||||
// The bootstrap rejection must not escape the worker entry point either.
|
||||
await expect(start).rejects.toBeInstanceOf(Error);
|
||||
|
||||
deliver({
|
||||
requestId: "request_bootstrap_failure",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(posted).toHaveLength(1);
|
||||
expect(posted[0]).toMatchObject({
|
||||
requestId: "request_bootstrap_failure",
|
||||
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
|
||||
kind,
|
||||
ok: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user