Files
clean-architecture-frontend…/src/adapters/storage/opfs/opfs-worker-runtime.ts
T
DongHyeonkaandClaude Opus 5 632b230c82 fix: report OPFS completion honestly and bound public cache staging
A durable write whose journal transaction could not be completed returned
plain success with `SUCCEEDED` telemetry. The payload was committed but
the transaction stayed `COMMITTED`, so the reconcile backlog and its quota
pressure grew while every caller was told the write had settled. That is
now a `RECONCILE` failure with the effect certainty preserved, and an
unfinished delete is observed `DEGRADED` rather than clean.

The worker seam lost causes in both directions. A bootstrap failure
answered every request with kind `CAPABILITIES`, so the gateway read a
kind mismatch and replaced the real `BLOCKED` or `QUOTA_EXCEEDED` with a
generic `UNSUPPORTED`; the envelope's correlation is now captured once at
the listener. On the client, the pending row and its timer were released
before the reply was decoded, so a trap that threw inside the decoder left
the public promise pending with nothing left to time it out, and a
throwing `requestId` getter produced a timeout instead of a prompt
protocol failure.

Public cache staging handed its signal to each `Request` and called that
ownership. A fetch that ignored it held the mutation lock forever, and a
digest that finished after the abort still wrote both the asset and the
activation marker — publishing a release nobody was waiting for. One
terminal owner now covers the whole staging body and every await
re-checks it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:25:26 +09:00

1972 lines
58 KiB
TypeScript

import type {
OpfsCapabilities,
OpfsChunkReference,
OpfsCleanupEffect,
OpfsPhysicalGenerationId,
OpfsPreparedObject,
OpfsStorageScope,
} from "../../../application/ports/browser-file-storage/opfs-ports.ts";
import {
assertValidStoragePolicy,
type BrowserDataFailureCode,
} from "../../../application/ports/browser-file-storage/shared.ts";
import {
resolveOpfsRuntimePolicy,
isValidOpfsStorageScope,
type OpfsRuntimePolicy,
} from "./opfs-policy.ts";
import {
OPFS_WORKER_PROTOCOL_VERSION,
} from "./opfs-worker-protocol.ts";
import type {
OpfsOrphanCandidateBatch,
OpfsOrphanDeleteResult,
OpfsWorkerFailure,
OpfsWorkerRequest,
OpfsWorkerResponse,
} from "./opfs-worker-protocol.ts";
export interface OpfsMutationLease {
release(): void;
}
export interface OpfsMutationLeaseManager {
acquire(signal?: AbortSignal): Promise<OpfsMutationLease>;
}
export interface OpfsWorkerMessageHost {
addEventListener(
type: "message",
listener: (event: MessageEvent<unknown>) => void,
): void;
postMessage(
message: OpfsWorkerResponse,
transfer?: readonly Transferable[],
): void;
}
export type BrowserOpfsWorkerDependencies = Readonly<{
storageManager: StorageManager;
lockManager?: LockManager;
crypto: Crypto;
dedicatedWorker: boolean;
supportsSynchronousAccessHandles: boolean;
policy?: Partial<OpfsRuntimePolicy>;
}>;
type ActivePut = {
readonly transactionId: string;
readonly scope: OpfsPreparedObject["descriptor"]["scope"];
readonly objectId: string;
readonly generation: number;
readonly physicalGenerationId: OpfsPhysicalGenerationId;
readonly declaredByteLength: number;
readonly mediaType: string;
readonly createdAtEpochMs: number;
readonly storagePolicy: OpfsPreparedObject["descriptor"]["storagePolicy"];
readonly chunkSizeBytes: number;
readonly lease: OpfsMutationLease;
readonly abortController: AbortController;
readonly chunks: OpfsChunkReference[];
operationTail: Promise<void>;
state: "ACTIVE" | "FINISHING" | "ABORTED";
totalBytes: number;
sawShortChunk: boolean;
};
type SyncAccessHandleLike = {
write(data: ArrayBufferView, options?: { at?: number }): number;
truncate(newSize: number): void;
flush(): void;
close(): void;
};
type SyncCapableFileHandle = FileSystemFileHandle & {
createSyncAccessHandle?: () => Promise<SyncAccessHandleLike>;
};
type LockManagerLike = {
request<Value>(
name: string,
options: Readonly<{ mode: "exclusive"; signal?: AbortSignal }>,
callback: (lock: unknown) => Promise<Value>,
): Promise<Value>;
};
class OpfsRuntimeFailure extends Error {
readonly code: BrowserDataFailureCode;
readonly retryable: boolean;
constructor(code: BrowserDataFailureCode, retryable = false) {
super("OPFS operation failed.");
this.name = "OpfsRuntimeFailure";
this.code = code;
this.retryable = retryable;
}
}
export interface OpfsWorkerRuntime {
handleRequest(request: unknown): Promise<OpfsWorkerResponse | null>;
}
export async function createBrowserOpfsWorkerRuntime(
dependencies: BrowserOpfsWorkerDependencies,
): Promise<OpfsWorkerRuntime> {
const policy = resolveOpfsRuntimePolicy(dependencies.policy);
const originRoot = await dependencies.storageManager.getDirectory();
const root = await originRoot.getDirectoryHandle(policy.rootDirectoryName, {
create: true,
});
const leaseManager = dependencies.lockManager
? createWebLockLeaseManager(
dependencies.lockManager as unknown as LockManagerLike,
policy.mutationLockName,
)
: null;
return createOpfsWorkerRuntime({
root,
crypto: dependencies.crypto,
policy,
leaseManager,
dedicatedWorker: dependencies.dedicatedWorker,
supportsSynchronousAccessHandles:
dependencies.supportsSynchronousAccessHandles,
});
}
export async function startBrowserOpfsDedicatedWorker(
host: OpfsWorkerMessageHost,
dependencies: Readonly<{
storageManager: StorageManager;
lockManager?: LockManager;
crypto: Crypto;
policy?: Partial<OpfsRuntimePolicy>;
}>,
): Promise<void> {
const supportsSynchronousAccessHandles =
typeof FileSystemFileHandle !== "undefined" &&
"createSyncAccessHandle" in FileSystemFileHandle.prototype;
const runtimePromise = createBrowserOpfsWorkerRuntime({
...dependencies,
dedicatedWorker: true,
supportsSynchronousAccessHandles,
});
// Install before awaiting OPFS initialization. Worker messages posted while
// getDirectory() is pending must not be dropped during bootstrap.
host.addEventListener("message", (event) => {
if (!hasRequestId(event.data)) return;
const requestData = event.data;
// NS-05. The envelope's correlation is captured once, up front. Answering a
// bootstrap failure with a default `CAPABILITIES` kind made the client see
// an expected-kind mismatch and overwrite the real cause — a `BLOCKED` or
// `QUOTA_EXCEEDED` outage was reported to operators as `UNSUPPORTED`.
const correlation = requestCorrelation(requestData);
void runtimePromise
.then((runtime) => runtime.handleRequest(requestData))
.then((response) => postWorkerResponse(host, response))
.catch((error: unknown) => {
host.postMessage(
failure(
correlation.requestId,
mapRuntimeFailure(error),
correlation.kind,
),
);
});
});
await runtimePromise;
}
export function createOpfsWorkerRuntime(
dependencies: Readonly<{
root: FileSystemDirectoryHandle;
crypto: Crypto;
policy: OpfsRuntimePolicy;
leaseManager: OpfsMutationLeaseManager | null;
dedicatedWorker: boolean;
supportsSynchronousAccessHandles: boolean;
}>,
): OpfsWorkerRuntime {
const activePuts = new Map<string, ActivePut>();
const pendingBegins = new Map<string, AbortController>();
const cancellationTombstones = new Set<string>();
const requiredCapabilitiesAvailable =
dependencies.dedicatedWorker &&
dependencies.leaseManager !== null &&
(dependencies.supportsSynchronousAccessHandles ||
dependencies.policy.allowAsyncWritableChunkFallback);
const capabilities: OpfsCapabilities = Object.freeze({
available: requiredCapabilitiesAvailable,
dedicatedWorkerRequired: true,
crossContextMutationLockAvailable: dependencies.leaseManager !== null,
synchronousAccessHandleAvailable:
dependencies.dedicatedWorker &&
dependencies.supportsSynchronousAccessHandles,
});
return Object.freeze({
async handleRequest(request: unknown) {
if (!hasRequestId(request)) return null;
if (!isWorkerRequest(request)) {
// STO-RR-02. Only an envelope this runtime could not read produces a
// protocol-level failure. Everything below answers its own request.
return failure(
request.requestId,
mapRuntimeFailure(new OpfsRuntimeFailure("INVALID_INPUT")),
);
}
try {
switch (request.kind) {
case "CAPABILITIES":
return success(request.requestId, request.kind, capabilities);
case "BEGIN_PUT":
await beginPut(request);
return success(request.requestId, request.kind);
case "APPEND_CHUNK":
await appendChunk(request);
return success(request.requestId, request.kind);
case "FINISH_PUT":
return success(
request.requestId,
request.kind,
await finishPut(request),
);
case "ABORT_PUT":
return success(
request.requestId,
request.kind,
await abortPut(
request.scope,
request.transactionId,
request.physicalGenerationId,
),
);
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,
),
);
case "REMOVE_OBJECT":
await removeObject(
request.scope,
request.objectId,
request.generation,
);
return success(request.requestId, request.kind);
case "CLEANUP_TRANSACTION":
return success(
request.requestId,
request.kind,
await cleanupTransaction(
request.scope,
request.transactionId,
true,
request.physicalGenerationId,
),
);
case "FINALIZE_PUT":
await finalizePut(
request.transactionId,
request.preparedObject,
);
return success(request.requestId, request.kind);
case "LIST_ORPHAN_CANDIDATES":
return success(
request.requestId,
request.kind,
await listOrphanCandidates(
request.scope,
request.olderThanEpochMs,
request.maxEntries,
),
);
case "DELETE_ORPHAN_CHUNK":
return success(
request.requestId,
request.kind,
await deleteOrphanChunk(
request.scope,
request.digestHex,
request.olderThanEpochMs,
),
);
}
} catch (error) {
// STO-RR-02. The kind travels with the failure so the client's
// expected-kind check cannot mistake a quota, integrity or abort
// failure for a protocol breach.
return failure(
request.requestId,
mapRuntimeFailure(error),
request.kind,
);
}
},
});
async function beginPut(
request: Extract<OpfsWorkerRequest, { kind: "BEGIN_PUT" }>,
): Promise<void> {
assertWorkerAvailable();
if (
!SAFE_TRANSACTION_ID.test(request.transactionId) ||
!isValidOpfsStorageScope(request.scope) ||
!dependencies.policy.isObjectIdAllowed(request.objectId) ||
!Number.isSafeInteger(request.generation) ||
request.generation < 1 ||
!isPhysicalGenerationId(request.physicalGenerationId) ||
!Number.isSafeInteger(request.declaredByteLength) ||
request.declaredByteLength < 0 ||
request.declaredByteLength > dependencies.policy.maxObjectBytes ||
!dependencies.policy.isMediaTypeAllowed(request.mediaType) ||
!Number.isSafeInteger(request.createdAtEpochMs) ||
request.createdAtEpochMs < 0 ||
request.chunkSizeBytes !== dependencies.policy.chunkSizeBytes
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
try {
assertValidStoragePolicy(request.storagePolicy);
} catch {
throw new OpfsRuntimeFailure("POLICY_REJECTED");
}
if (request.storagePolicy.namespace !== request.scope.namespace) {
throw new OpfsRuntimeFailure("POLICY_REJECTED");
}
const transactionKey = scopedTransactionKey(
request.scope,
request.transactionId,
);
if (activePuts.has(transactionKey)) {
throw new OpfsRuntimeFailure("CONFLICT");
}
if (cancellationTombstones.has(transactionKey)) {
throw new OpfsRuntimeFailure("ABORTED");
}
const beginAbort = new AbortController();
pendingBegins.set(transactionKey, beginAbort);
let lease: OpfsMutationLease;
try {
lease = await dependencies.leaseManager!.acquire(beginAbort.signal);
} finally {
pendingBegins.delete(transactionKey);
}
if (
beginAbort.signal.aborted ||
cancellationTombstones.has(transactionKey)
) {
lease.release();
throw new OpfsRuntimeFailure("ABORTED");
}
const put: ActivePut = {
transactionId: request.transactionId,
scope: request.scope,
objectId: request.objectId,
generation: request.generation,
physicalGenerationId: request.physicalGenerationId,
declaredByteLength: request.declaredByteLength,
mediaType: request.mediaType,
createdAtEpochMs: request.createdAtEpochMs,
storagePolicy: request.storagePolicy,
chunkSizeBytes: request.chunkSizeBytes,
lease,
abortController: beginAbort,
chunks: [],
operationTail: Promise.resolve(),
state: "ACTIVE",
totalBytes: 0,
sawShortChunk: false,
};
activePuts.set(transactionKey, put);
try {
await writeReceipt(put);
if (
beginAbort.signal.aborted ||
cancellationTombstones.has(transactionKey)
) {
if (activePuts.get(transactionKey) === put) {
activePuts.delete(transactionKey);
lease.release();
}
await cleanupTransaction(request.scope, request.transactionId);
throw new OpfsRuntimeFailure("ABORTED");
}
} catch (error) {
if (activePuts.get(transactionKey) === put) {
activePuts.delete(transactionKey);
lease.release();
}
throw error;
}
}
async function appendChunk(
request: Extract<OpfsWorkerRequest, { kind: "APPEND_CHUNK" }>,
): Promise<void> {
if (!isValidOpfsStorageScope(request.scope)) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const transactionKey = scopedTransactionKey(
request.scope,
request.transactionId,
);
const put = activePuts.get(transactionKey);
if (!put) throw new OpfsRuntimeFailure("INVALID_INPUT");
try {
await runActivePutOperation(transactionKey, put, async () => {
if (
request.sequence !== put.chunks.length ||
!(request.bytes instanceof ArrayBuffer) ||
request.bytes.byteLength === 0 ||
request.bytes.byteLength > put.chunkSizeBytes ||
put.sawShortChunk ||
put.chunks.length >= dependencies.policy.maxChunkCount
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const nextTotal = put.totalBytes + request.bytes.byteLength;
if (
!Number.isSafeInteger(nextTotal) ||
nextTotal > put.declaredByteLength ||
nextTotal > dependencies.policy.maxObjectBytes
) {
throw new OpfsRuntimeFailure("LIMIT_EXCEEDED");
}
const bytes = new Uint8Array(request.bytes);
const digestHex = await sha256Hex(dependencies.crypto, bytes);
assertActivePut(transactionKey, put);
await writeImmutableChunk(put.scope, digestHex, bytes);
assertActivePut(transactionKey, put);
put.chunks.push(
Object.freeze({
sequence: request.sequence,
byteLength: bytes.byteLength,
digestHex,
}),
);
put.totalBytes = nextTotal;
put.sawShortChunk = bytes.byteLength < put.chunkSizeBytes;
await writeReceipt(put);
assertActivePut(transactionKey, put);
});
} catch (error) {
if (put.state !== "ABORTED") {
await failActivePut(transactionKey, put);
}
throw error;
}
}
async function finishPut(
request: Extract<OpfsWorkerRequest, { kind: "FINISH_PUT" }>,
): Promise<OpfsPreparedObject> {
if (!isValidOpfsStorageScope(request.scope)) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const transactionKey = scopedTransactionKey(
request.scope,
request.transactionId,
);
const put = activePuts.get(transactionKey);
if (!put) throw new OpfsRuntimeFailure("INVALID_INPUT");
try {
const preparedObject = await runActivePutOperation(
transactionKey,
put,
async () => {
if (put.state !== "ACTIVE") {
throw new OpfsRuntimeFailure("CONFLICT");
}
put.state = "FINISHING";
if (
put.totalBytes !== put.declaredByteLength ||
put.chunks.length !==
Math.ceil(put.declaredByteLength / put.chunkSizeBytes)
) {
throw new OpfsRuntimeFailure("INTEGRITY_FAILED");
}
const rootDigestHex = await treeDigestHex(
dependencies.crypto,
put.chunkSizeBytes,
put.declaredByteLength,
put.chunks,
);
assertActivePut(transactionKey, put, true);
const prepared: OpfsPreparedObject = Object.freeze({
physicalSchemaVersion: 2,
physicalGenerationId: put.physicalGenerationId,
descriptor: Object.freeze({
objectId: put.objectId,
scope: put.scope,
generation: put.generation,
byteLength: put.declaredByteLength,
mediaType: put.mediaType,
createdAtEpochMs: put.createdAtEpochMs,
integrity: Object.freeze({
algorithm: "SHA-256-TREE-V1",
rootDigestHex,
chunkSizeBytes: put.chunkSizeBytes,
}),
storagePolicy: put.storagePolicy,
}),
chunks: Object.freeze([...put.chunks]),
});
await writeJsonAtomic(manifestPath(prepared), prepared);
assertActivePut(transactionKey, put, true);
await writeJsonAtomic(receiptPath(put.scope, put.transactionId), {
schemaVersion: 1,
phase: "FILES_READY",
preparedObject: prepared,
});
assertActivePut(transactionKey, put, true);
return prepared;
},
);
if (activePuts.get(transactionKey) === put) {
activePuts.delete(transactionKey);
put.lease.release();
}
return preparedObject;
} catch (error) {
if (put.state !== "ABORTED") {
await failActivePut(transactionKey, put);
}
throw error;
}
}
async function abortPut(
scope: OpfsPreparedObject["descriptor"]["scope"],
transactionId: string,
physicalGenerationId?: OpfsPhysicalGenerationId,
): Promise<OpfsCleanupEffect> {
if (
!isValidOpfsStorageScope(scope) ||
!SAFE_TRANSACTION_ID.test(transactionId) ||
(physicalGenerationId !== undefined &&
!isPhysicalGenerationId(physicalGenerationId))
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const transactionKey = scopedTransactionKey(scope, transactionId);
rememberCancellation(transactionKey);
pendingBegins.get(transactionKey)?.abort();
const active = activePuts.get(transactionKey);
if (active) {
active.state = "ABORTED";
active.abortController.abort();
await active.operationTail;
if (activePuts.get(transactionKey) === active) {
activePuts.delete(transactionKey);
}
try {
// STO-01. The mutation lease is held through the physical delete and
// the staging cleanup; releasing it earlier would let a new transaction
// race this compensation.
await removePhysicalGeneration(
active.scope,
active.objectId,
active.generation,
active.physicalGenerationId,
);
return await cleanupTransactionLocked(
scope,
transactionId,
true,
physicalGenerationId ?? active.physicalGenerationId,
);
} finally {
active.lease.release();
}
}
return await cleanupTransaction(
scope,
transactionId,
true,
physicalGenerationId,
);
}
async function runActivePutOperation<Value>(
transactionKey: string,
put: ActivePut,
operation: () => Promise<Value>,
): Promise<Value> {
const previous = put.operationTail;
let release: (() => void) | undefined;
put.operationTail = new Promise<void>((resolve) => {
release = resolve;
});
await previous;
try {
assertActivePut(transactionKey, put, true);
return await operation();
} finally {
release?.();
}
}
function assertActivePut(
transactionKey: string,
put: ActivePut,
allowFinishing = false,
): void {
if (
activePuts.get(transactionKey) !== put ||
put.abortController.signal.aborted ||
put.state === "ABORTED" ||
(!allowFinishing && put.state !== "ACTIVE")
) {
throw new OpfsRuntimeFailure("ABORTED");
}
}
async function failActivePut(
transactionKey: string,
put: ActivePut,
): Promise<void> {
put.state = "ABORTED";
put.abortController.abort();
if (activePuts.get(transactionKey) === put) {
activePuts.delete(transactionKey);
}
try {
await removePhysicalGeneration(
put.scope,
put.objectId,
put.generation,
put.physicalGenerationId,
);
await cleanupTransactionLocked(
put.scope,
put.transactionId,
true,
put.physicalGenerationId,
);
} finally {
put.lease.release();
}
}
async function removePhysicalGeneration(
scope: OpfsStorageScope,
objectId: string,
generation: number,
physicalGenerationId: OpfsPhysicalGenerationId | undefined,
): Promise<void> {
try {
const objectDirectory = await getDirectory(
dependencies.root,
[
...scopeRootPath(scope),
"objects",
objectId.slice(0, 2),
objectId,
],
false,
);
await removeEntryIfPresent(
objectDirectory,
generationSegmentFor(generation, physicalGenerationId),
true,
);
} catch (error) {
if (!isNotFound(error)) throw error;
}
}
async function verifyObject(
expected: OpfsPreparedObject,
): Promise<boolean> {
if (!isPreparedObjectSafe(expected, dependencies.policy)) return false;
let stored: unknown;
try {
stored = await readJson(manifestPath(expected));
} catch (error) {
if (isNotFound(error)) return false;
throw error;
}
if (
!isPreparedObjectSafe(stored, dependencies.policy) ||
stableJson(stored) !== stableJson(expected)
) {
return false;
}
const rootDigestHex = await treeDigestHex(
dependencies.crypto,
expected.descriptor.integrity.chunkSizeBytes,
expected.descriptor.byteLength,
expected.chunks,
);
if (rootDigestHex !== expected.descriptor.integrity.rootDigestHex) {
return false;
}
for (const chunk of expected.chunks) {
let bytes: Uint8Array;
try {
bytes = await readFile(
chunkPath(expected.descriptor.scope, chunk.digestHex),
);
} catch (error) {
if (isNotFound(error)) return false;
throw error;
}
if (
bytes.byteLength !== chunk.byteLength ||
(await sha256Hex(dependencies.crypto, bytes)) !== chunk.digestHex
) {
return false;
}
}
return true;
}
async function readVerifiedChunk(
expected: OpfsPreparedObject,
sequence: number,
): Promise<ArrayBuffer> {
if (
!isPreparedObjectSafe(expected, dependencies.policy) ||
!Number.isSafeInteger(sequence) ||
sequence < 0
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const reference = expected.chunks[sequence];
if (!reference || reference.sequence !== sequence) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const bytes = await readFile(
chunkPath(expected.descriptor.scope, reference.digestHex),
);
if (
bytes.byteLength !== reference.byteLength ||
(await sha256Hex(dependencies.crypto, bytes)) !== reference.digestHex
) {
throw new OpfsRuntimeFailure("INTEGRITY_FAILED");
}
const copy = Uint8Array.from(bytes);
return copy.buffer as ArrayBuffer;
}
async function removeObject(
scope: OpfsPreparedObject["descriptor"]["scope"],
objectId: string,
generation: number,
): Promise<void> {
assertWorkerAvailable();
if (
!isValidOpfsStorageScope(scope) ||
!dependencies.policy.isObjectIdAllowed(objectId) ||
!Number.isSafeInteger(generation) ||
generation < 1
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const lease = await dependencies.leaseManager!.acquire();
try {
const prefixDirectory = await getDirectory(
dependencies.root,
[
...scopeRootPath(scope),
"objects",
objectId.slice(0, 2),
],
false,
);
await removeEntryIfPresent(prefixDirectory, objectId, true);
} catch (error) {
if (!isNotFound(error)) throw error;
} finally {
lease.release();
}
}
/**
* STO-01. Cleanup deletes exactly one transaction's physical generation while
* holding the origin mutation lease, and reports whether the effect actually
* happened. `ALREADY_CLEAN` means there was nothing left to delete.
*/
async function cleanupTransaction(
scope: OpfsPreparedObject["descriptor"]["scope"],
transactionId: string,
removePreparedGeneration = true,
physicalGenerationId?: OpfsPhysicalGenerationId,
): Promise<OpfsCleanupEffect> {
if (
!isValidOpfsStorageScope(scope) ||
!SAFE_TRANSACTION_ID.test(transactionId)
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
// Probe before locking. A transaction that never reached staging has
// nothing to delete, and waiting for the mutation lease here would deadlock
// against the very BEGIN this compensation is cancelling.
try {
await getDirectory(
dependencies.root,
[...scopeRootPath(scope), "staging", transactionId],
false,
);
} catch (error) {
if (isNotFound(error)) return CLEANUP_ALREADY_CLEAN;
throw error;
}
// Every destructive step below runs while the lease is held.
const lease = await dependencies.leaseManager!.acquire();
try {
return await cleanupTransactionLocked(
scope,
transactionId,
removePreparedGeneration,
physicalGenerationId,
);
} finally {
lease.release();
}
}
/**
* Callers that already hold the origin mutation lease use this directly, so
* an abort never releases the lease between fencing and physical deletion.
*/
async function cleanupTransactionLocked(
scope: OpfsPreparedObject["descriptor"]["scope"],
transactionId: string,
removePreparedGeneration = true,
physicalGenerationId?: OpfsPhysicalGenerationId,
): Promise<OpfsCleanupEffect> {
if (
!isValidOpfsStorageScope(scope) ||
!SAFE_TRANSACTION_ID.test(transactionId) ||
(physicalGenerationId !== undefined &&
!isPhysicalGenerationId(physicalGenerationId))
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
let staging: FileSystemDirectoryHandle;
try {
staging = await getDirectory(
dependencies.root,
[...scopeRootPath(scope), "staging"],
false,
);
} catch (error) {
if (isNotFound(error)) return CLEANUP_ALREADY_CLEAN;
throw error;
}
{
if (removePreparedGeneration) {
let receipt: unknown;
try {
receipt = await readJson(receiptPath(scope, transactionId));
} catch (error) {
if (isNotFound(error)) {
await removeEntryIfPresent(staging, transactionId, true);
return CLEANUP_ALREADY_CLEAN;
}
// Keep unreadable staging in place so orphan GC fails closed.
throw error;
}
const target = extractReceiptPhysicalTarget(receipt, scope);
if (!target) {
throw new OpfsRuntimeFailure("CORRUPT_DATA");
}
// A caller-supplied token wins: a stale compensation must not widen its
// target to whatever the receipt now says.
await removePhysicalGeneration(
scope,
target.objectId,
target.generation,
physicalGenerationId ?? target.physicalGenerationId,
);
}
await removeEntryIfPresent(staging, transactionId, true);
return CLEANUP_CLEANED;
}
}
async function finalizePut(
transactionId: string,
preparedObject: OpfsPreparedObject,
): Promise<void> {
assertWorkerAvailable();
if (
!SAFE_TRANSACTION_ID.test(transactionId) ||
!isPreparedObjectSafe(preparedObject, dependencies.policy)
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const lease = await dependencies.leaseManager!.acquire();
try {
const descriptor = preparedObject.descriptor;
const objectDirectory = await getDirectory(
dependencies.root,
[
...scopeRootPath(descriptor.scope),
"objects",
descriptor.objectId.slice(0, 2),
descriptor.objectId,
],
false,
);
for await (const [name, handle] of objectDirectory.entries()) {
if (
handle.kind === "directory" &&
isGenerationSegment(name) &&
name !== preparedGenerationSegment(preparedObject)
) {
await objectDirectory.removeEntry(name, { recursive: true });
}
}
// STO-RR-01. This path already holds the origin mutation lease, and a
// Web Lock is not reentrant: asking for it again here never returns, so
// an ordinary PUT would stop for good at FINALIZE.
await cleanupTransactionLocked(descriptor.scope, transactionId, false);
} catch (error) {
if (!isNotFound(error)) throw error;
await cleanupTransactionLocked(
preparedObject.descriptor.scope,
transactionId,
false,
);
} finally {
lease.release();
}
}
async function listOrphanCandidates(
scope: OpfsPreparedObject["descriptor"]["scope"],
olderThanEpochMs: number,
maxEntries: number,
): Promise<OpfsOrphanCandidateBatch> {
assertWorkerAvailable();
if (
!isValidOpfsStorageScope(scope) ||
!Number.isSafeInteger(olderThanEpochMs) ||
olderThanEpochMs < 0 ||
!Number.isSafeInteger(maxEntries) ||
maxEntries < 1 ||
maxEntries > dependencies.policy.orphanGcBatchSize
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const lease = await dependencies.leaseManager!.acquire();
try {
const staged = await stagedChunkDigests(scope);
if (!staged.safeToSweep) {
return Object.freeze({
safeToSweep: false,
digests: Object.freeze([]),
moreAvailable: false,
});
}
let shaRoot: FileSystemDirectoryHandle;
try {
shaRoot = await getDirectory(
dependencies.root,
[...scopeRootPath(scope), "chunks", "sha256"],
false,
);
} catch (error) {
if (isNotFound(error)) {
return Object.freeze({
safeToSweep: true,
digests: Object.freeze([]),
moreAvailable: false,
});
}
throw error;
}
const candidates: string[] = [];
outer: for await (const [, prefixHandle] of shaRoot.entries()) {
if (prefixHandle.kind !== "directory") continue;
const prefixDirectory =
prefixHandle as FileSystemDirectoryHandle;
for await (const [name, handle] of prefixDirectory.entries()) {
if (handle.kind !== "file") continue;
const digestHex = name.endsWith(".bin")
? name.slice(0, -4)
: "";
if (
!SHA256_HEX.test(digestHex) ||
staged.digests.has(digestHex)
) {
continue;
}
const file = await (
handle as FileSystemFileHandle
).getFile();
if (file.lastModified > olderThanEpochMs) continue;
candidates.push(digestHex);
if (candidates.length > maxEntries) break outer;
}
}
return Object.freeze({
safeToSweep: true,
digests: Object.freeze(candidates.slice(0, maxEntries)),
moreAvailable: candidates.length > maxEntries,
});
} finally {
lease.release();
}
}
async function deleteOrphanChunk(
scope: OpfsPreparedObject["descriptor"]["scope"],
digestHex: string,
olderThanEpochMs: number,
): Promise<OpfsOrphanDeleteResult> {
assertWorkerAvailable();
if (
!isValidOpfsStorageScope(scope) ||
!SHA256_HEX.test(digestHex) ||
!Number.isSafeInteger(olderThanEpochMs) ||
olderThanEpochMs < 0
) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const lease = await dependencies.leaseManager!.acquire();
try {
const staged = await stagedChunkDigests(scope);
if (!staged.safeToSweep) {
return Object.freeze({ deleted: false, skippedUnsafe: true });
}
if (staged.digests.has(digestHex)) {
return Object.freeze({ deleted: false, skippedUnsafe: false });
}
const path = chunkPath(scope, digestHex);
try {
const { directory, fileName } = await resolveFileParent(
dependencies.root,
path,
false,
);
const handle = await directory.getFileHandle(fileName);
const file = await handle.getFile();
if (file.lastModified > olderThanEpochMs) {
return Object.freeze({ deleted: false, skippedUnsafe: false });
}
await directory.removeEntry(fileName);
return Object.freeze({ deleted: true, skippedUnsafe: false });
} catch (error) {
if (isNotFound(error)) {
return Object.freeze({ deleted: false, skippedUnsafe: false });
}
throw error;
}
} finally {
lease.release();
}
}
async function stagedChunkDigests(
scope: OpfsPreparedObject["descriptor"]["scope"],
): Promise<Readonly<{
safeToSweep: boolean;
digests: ReadonlySet<string>;
}>> {
const digests = new Set<string>();
let staging: FileSystemDirectoryHandle;
try {
staging = await getDirectory(
dependencies.root,
[...scopeRootPath(scope), "staging"],
false,
);
} catch (error) {
if (isNotFound(error)) {
return { safeToSweep: true, digests };
}
throw error;
}
for await (const [transactionId, handle] of staging.entries()) {
if (
handle.kind !== "directory" ||
!SAFE_TRANSACTION_ID.test(transactionId)
) {
return { safeToSweep: false, digests };
}
let receipt: unknown;
try {
receipt = await readJson(receiptPath(scope, transactionId));
} catch {
return { safeToSweep: false, digests };
}
if (!receiptBelongsToScope(receipt, scope)) {
return { safeToSweep: false, digests };
}
const receiptDigests = extractReceiptDigests(receipt);
if (!receiptDigests) {
return { safeToSweep: false, digests };
}
for (const digest of receiptDigests) digests.add(digest);
}
return { safeToSweep: true, digests };
}
function extractReceiptPhysicalTarget(
receipt: unknown,
scope: OpfsStorageScope,
): Readonly<{
objectId: string;
generation: number;
physicalGenerationId: OpfsPhysicalGenerationId | undefined;
}> | null {
if (!receiptBelongsToScope(receipt, scope)) return null;
const record = receipt as Record<string, unknown>;
if (record.phase === "PREPARING") {
return typeof record.objectId === "string" &&
dependencies.policy.isObjectIdAllowed(record.objectId) &&
typeof record.generation === "number" &&
Number.isSafeInteger(record.generation) &&
record.generation > 0
? {
objectId: record.objectId,
generation: record.generation,
physicalGenerationId: isPhysicalGenerationId(
record.physicalGenerationId,
)
? record.physicalGenerationId
: undefined,
}
: null;
}
if (
record.phase === "FILES_READY" &&
isPreparedObjectSafe(record.preparedObject, dependencies.policy) &&
sameScope(record.preparedObject.descriptor.scope, scope)
) {
const prepared = record.preparedObject;
return {
objectId: prepared.descriptor.objectId,
generation: prepared.descriptor.generation,
physicalGenerationId:
prepared.physicalSchemaVersion === 2
? prepared.physicalGenerationId
: undefined,
};
}
return null;
}
function rememberCancellation(transactionId: string): void {
cancellationTombstones.add(transactionId);
while (
cancellationTombstones.size >
dependencies.policy.maxCancellationTombstones
) {
const oldest = cancellationTombstones.values().next().value;
if (typeof oldest !== "string") break;
cancellationTombstones.delete(oldest);
}
}
async function writeReceipt(put: ActivePut): Promise<void> {
await writeJsonAtomic(receiptPath(put.scope, put.transactionId), {
schemaVersion: 2,
phase: "PREPARING",
scope: put.scope,
objectId: put.objectId,
generation: put.generation,
physicalGenerationId: put.physicalGenerationId,
declaredByteLength: put.declaredByteLength,
chunks: put.chunks,
});
}
async function writeImmutableChunk(
scope: OpfsPreparedObject["descriptor"]["scope"],
digestHex: string,
bytes: Uint8Array,
): Promise<void> {
const path = chunkPath(scope, digestHex);
try {
const existing = await readFile(path);
if (
existing.byteLength !== bytes.byteLength ||
(await sha256Hex(dependencies.crypto, existing)) !== digestHex
) {
const { directory, fileName } = await resolveFileParent(
dependencies.root,
path,
false,
);
await directory.removeEntry(fileName);
} else {
return;
}
} catch (error) {
if (!isNotFound(error)) throw error;
}
const { directory, fileName } = await resolveFileParent(
dependencies.root,
path,
true,
);
const fileHandle = (await directory.getFileHandle(fileName, {
create: true,
})) as SyncCapableFileHandle;
if (
dependencies.dedicatedWorker &&
typeof fileHandle.createSyncAccessHandle === "function"
) {
try {
await writeWithSyncAccessHandle(fileHandle, bytes);
return;
} catch (error) {
if (
!dependencies.policy.allowAsyncWritableChunkFallback ||
!isSyncUnsupported(error)
) {
throw error;
}
}
}
if (!dependencies.policy.allowAsyncWritableChunkFallback) {
throw new OpfsRuntimeFailure("UNSUPPORTED");
}
await writeWithAtomicWritable(fileHandle, bytes);
}
async function writeJsonAtomic(
path: readonly string[],
value: unknown,
): Promise<void> {
const bytes = new TextEncoder().encode(JSON.stringify(value));
const { directory, fileName } = await resolveFileParent(
dependencies.root,
path,
true,
);
const handle = await directory.getFileHandle(fileName, { create: true });
await writeWithAtomicWritable(handle, bytes);
}
async function readJson(path: readonly string[]): Promise<unknown> {
const bytes = await readFile(path);
if (bytes.byteLength > MAX_MANIFEST_BYTES) {
throw new OpfsRuntimeFailure("CORRUPT_DATA");
}
try {
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
} catch {
throw new OpfsRuntimeFailure("CORRUPT_DATA");
}
}
async function readFile(path: readonly string[]): Promise<Uint8Array> {
const { directory, fileName } = await resolveFileParent(
dependencies.root,
path,
false,
);
const handle = await directory.getFileHandle(fileName);
const file = await handle.getFile();
return new Uint8Array(await file.arrayBuffer());
}
function assertWorkerAvailable(): void {
if (!requiredCapabilitiesAvailable) {
throw new OpfsRuntimeFailure("UNSUPPORTED");
}
}
}
export function installOpfsWorkerMessageHandler(
host: OpfsWorkerMessageHost,
runtime: OpfsWorkerRuntime,
): void {
host.addEventListener("message", (event) => {
void runtime.handleRequest(event.data).then((response) => {
postWorkerResponse(host, response);
});
});
}
function postWorkerResponse(
host: OpfsWorkerMessageHost,
response: OpfsWorkerResponse | null,
): void {
if (!response) return;
if (
response.ok &&
response.value instanceof ArrayBuffer
) {
host.postMessage(response, [response.value]);
return;
}
host.postMessage(response);
}
export function createWebLockLeaseManager(
lockManager: LockManagerLike,
lockName: string,
): OpfsMutationLeaseManager {
if (lockName.length === 0) {
throw new TypeError("OPFS mutation lock name is required.");
}
return Object.freeze({
async acquire(signal?: AbortSignal) {
if (signal?.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
let releaseHold: (() => void) | undefined;
let released = false;
const hold = new Promise<void>((resolve) => {
releaseHold = resolve;
});
let acquiredResolve: ((lease: OpfsMutationLease) => void) | undefined;
let acquiredReject: ((error: unknown) => void) | undefined;
const acquired = new Promise<OpfsMutationLease>((resolve, reject) => {
acquiredResolve = resolve;
acquiredReject = reject;
});
void lockManager
.request(lockName, { mode: "exclusive", signal }, async (lock) => {
if (!lock) throw new OpfsRuntimeFailure("BLOCKED", true);
const lease: OpfsMutationLease = Object.freeze({
release() {
if (released) return;
released = true;
releaseHold?.();
},
});
acquiredResolve?.(lease);
await hold;
})
.catch((error: unknown) => acquiredReject?.(error));
return await acquired;
},
});
}
/**
* Exported for a focused lifecycle test. Partial writes are retried and every
* acquired handle is closed, including failure paths.
*/
export async function writeWithSyncAccessHandle(
fileHandle: SyncCapableFileHandle,
bytes: Uint8Array,
): Promise<void> {
if (typeof fileHandle.createSyncAccessHandle !== "function") {
throw new OpfsRuntimeFailure("UNSUPPORTED");
}
let accessHandle: SyncAccessHandleLike | undefined;
try {
accessHandle = await fileHandle.createSyncAccessHandle();
accessHandle.truncate(0);
let offset = 0;
while (offset < bytes.byteLength) {
const written = accessHandle.write(bytes.subarray(offset), { at: offset });
if (
!Number.isSafeInteger(written) ||
written <= 0 ||
written > bytes.byteLength - offset
) {
throw new OpfsRuntimeFailure("NOT_READABLE", true);
}
offset += written;
}
accessHandle.truncate(bytes.byteLength);
accessHandle.flush();
} finally {
accessHandle?.close();
}
}
async function writeWithAtomicWritable(
fileHandle: FileSystemFileHandle,
bytes: Uint8Array,
): Promise<void> {
const writable = await fileHandle.createWritable({ keepExistingData: false });
let closed = false;
try {
const copy = Uint8Array.from(bytes);
await writable.write(copy);
await writable.close();
closed = true;
} finally {
if (!closed) {
try {
await writable.abort();
} catch {
// Preserve the original write error.
}
}
}
}
async function resolveFileParent(
root: FileSystemDirectoryHandle,
path: readonly string[],
create: boolean,
): Promise<Readonly<{
directory: FileSystemDirectoryHandle;
fileName: string;
}>> {
if (path.length < 1 || path.some((segment) => !SAFE_PATH_SEGMENT.test(segment))) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
const fileName = path.at(-1)!;
const directory = await getDirectory(root, path.slice(0, -1), create);
return { directory, fileName };
}
async function getDirectory(
root: FileSystemDirectoryHandle,
path: readonly string[],
create: boolean,
): Promise<FileSystemDirectoryHandle> {
let current = root;
for (const segment of path) {
if (!SAFE_PATH_SEGMENT.test(segment)) {
throw new OpfsRuntimeFailure("INVALID_INPUT");
}
current = await current.getDirectoryHandle(segment, { create });
}
return current;
}
async function removeEntryIfPresent(
directory: FileSystemDirectoryHandle,
name: string,
recursive: boolean,
): Promise<void> {
try {
await directory.removeEntry(name, { recursive });
} catch (error) {
if (!isNotFound(error)) throw error;
}
}
const CLEANUP_CLEANED: OpfsCleanupEffect = Object.freeze({ kind: "CLEANED" });
const CLEANUP_ALREADY_CLEAN: OpfsCleanupEffect = Object.freeze({
kind: "ALREADY_CLEAN",
});
const PHYSICAL_GENERATION_ID = /^[0-9a-f]{32}$/u;
const V1_GENERATION_SEGMENT = /^\d+$/u;
const V2_GENERATION_SEGMENT = /^g\d+-[0-9a-f]{32}$/u;
export function isPhysicalGenerationId(
value: unknown,
): value is OpfsPhysicalGenerationId {
return typeof value === "string" && PHYSICAL_GENERATION_ID.test(value);
}
/**
* STO-01. v1 wrote `objects/<prefix>/<id>/<generation>/`, which two
* transactions can legitimately share. v2 writes
* `objects/<prefix>/<id>/g<generation>-<token>/` so a late compensation can
* only ever delete its own transaction's directory. v1 segments stay readable
* through the rollback window.
*/
function generationSegmentFor(
generation: number,
physicalGenerationId: OpfsPhysicalGenerationId | undefined,
): string {
return physicalGenerationId === undefined
? String(generation)
: `g${generation}-${physicalGenerationId}`;
}
function preparedGenerationSegment(
preparedObject: OpfsPreparedObject,
): string {
return generationSegmentFor(
preparedObject.descriptor.generation,
preparedObject.physicalSchemaVersion === 2
? preparedObject.physicalGenerationId
: undefined,
);
}
function isGenerationSegment(name: string): boolean {
return V1_GENERATION_SEGMENT.test(name) || V2_GENERATION_SEGMENT.test(name);
}
function manifestPath(preparedObject: OpfsPreparedObject): readonly string[] {
const descriptor = preparedObject.descriptor;
return [
...scopeRootPath(descriptor.scope),
"objects",
descriptor.objectId.slice(0, 2),
descriptor.objectId,
preparedGenerationSegment(preparedObject),
"manifest.json",
];
}
function chunkPath(
scope: OpfsStorageScope,
digestHex: string,
): readonly string[] {
return [
...scopeRootPath(scope),
"chunks",
"sha256",
digestHex.slice(0, 2),
`${digestHex}.bin`,
];
}
function receiptPath(
scope: OpfsStorageScope,
transactionId: string,
): readonly string[] {
return [
...scopeRootPath(scope),
"staging",
transactionId,
"receipt.json",
];
}
function scopeRootPath(scope: OpfsStorageScope): readonly string[] {
return [
"authorities",
scope.authorityToken,
scope.namespaceToken,
scope.partitionToken,
];
}
function scopedTransactionKey(
scope: OpfsStorageScope,
transactionId: string,
): string {
return [
scope.authorityToken,
scope.namespaceToken,
scope.partitionToken,
transactionId,
].join("|");
}
function sameScope(
left: OpfsStorageScope,
right: OpfsStorageScope,
): boolean {
return (
left.namespace === right.namespace &&
left.authorityToken === right.authorityToken &&
left.namespaceToken === right.namespaceToken &&
left.partitionToken === right.partitionToken
);
}
function receiptBelongsToScope(
receipt: unknown,
scope: OpfsStorageScope,
): boolean {
if (!receipt || typeof receipt !== "object") return false;
const record = receipt as Record<string, unknown>;
if (
// v1 receipts stay readable through the rollback window.
(record.schemaVersion !== 1 && record.schemaVersion !== 2) ||
(record.phase !== "PREPARING" && record.phase !== "FILES_READY")
) {
return false;
}
if (record.phase === "PREPARING") {
return Boolean(
record.scope &&
typeof record.scope === "object" &&
isValidOpfsStorageScope(record.scope as OpfsStorageScope) &&
sameScope(record.scope as OpfsStorageScope, scope),
);
}
if (
!record.preparedObject ||
typeof record.preparedObject !== "object" ||
!("descriptor" in record.preparedObject) ||
!record.preparedObject.descriptor ||
typeof record.preparedObject.descriptor !== "object" ||
!("scope" in record.preparedObject.descriptor)
) {
return false;
}
const preparedScope = record.preparedObject.descriptor.scope;
return Boolean(
preparedScope &&
typeof preparedScope === "object" &&
isValidOpfsStorageScope(preparedScope as OpfsStorageScope) &&
sameScope(preparedScope as OpfsStorageScope, scope),
);
}
async function sha256Hex(
crypto: Crypto,
bytes: Uint8Array,
): Promise<string> {
const copy = Uint8Array.from(bytes);
const digest = await crypto.subtle.digest("SHA-256", copy);
return bytesToHex(new Uint8Array(digest));
}
async function treeDigestHex(
crypto: Crypto,
chunkSizeBytes: number,
byteLength: number,
chunks: readonly OpfsChunkReference[],
): Promise<string> {
const canonical = [
"sha-256-tree-v1",
`chunk-size:${chunkSizeBytes}`,
`byte-length:${byteLength}`,
...chunks.map(
(chunk) =>
`${chunk.sequence}:${chunk.byteLength}:${chunk.digestHex}`,
),
"",
].join("\n");
return await sha256Hex(crypto, new TextEncoder().encode(canonical));
}
function bytesToHex(bytes: Uint8Array): string {
let hex = "";
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
return hex;
}
function stableJson(value: unknown): string {
if (
value === null ||
typeof value === "string" ||
typeof value === "boolean"
) {
return JSON.stringify(value);
}
if (typeof value === "number") {
if (!Number.isFinite(value)) throw new OpfsRuntimeFailure("CORRUPT_DATA");
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(",")}]`;
}
if (typeof value === "object") {
const record = value as Record<string, unknown>;
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
.join(",")}}`;
}
throw new OpfsRuntimeFailure("CORRUPT_DATA");
}
/**
* STO-01 expand phase. v1 prepared objects stay readable through the rollback
* window; v2 additionally carries a transaction-unique physical fencing token.
*/
function isSupportedPhysicalSchema(value: object): boolean {
const record = value as Record<string, unknown>;
if (record.physicalSchemaVersion === 1) return true;
return (
record.physicalSchemaVersion === 2 &&
typeof record.physicalGenerationId === "string" &&
/^[0-9a-f]{32}$/u.test(record.physicalGenerationId)
);
}
function isPreparedObjectSafe(
value: unknown,
policy: OpfsRuntimePolicy,
): value is OpfsPreparedObject {
if (
!value ||
typeof value !== "object" ||
!("physicalSchemaVersion" in value) ||
!isSupportedPhysicalSchema(value) ||
!("descriptor" in value) ||
!value.descriptor ||
typeof value.descriptor !== "object" ||
!("chunks" in value) ||
!Array.isArray(value.chunks)
) {
return false;
}
const descriptor = value.descriptor as Record<string, unknown>;
const integrity =
descriptor.integrity && typeof descriptor.integrity === "object"
? (descriptor.integrity as Record<string, unknown>)
: null;
if (
typeof descriptor.objectId !== "string" ||
!policy.isObjectIdAllowed(descriptor.objectId) ||
!("scope" in descriptor) ||
!descriptor.scope ||
typeof descriptor.scope !== "object" ||
!isValidOpfsStorageScope(
descriptor.scope as OpfsPreparedObject["descriptor"]["scope"],
) ||
(descriptor.scope as OpfsPreparedObject["descriptor"]["scope"])
.namespace !==
(descriptor.storagePolicy as
| OpfsPreparedObject["descriptor"]["storagePolicy"]
| undefined)?.namespace ||
typeof descriptor.generation !== "number" ||
!Number.isSafeInteger(descriptor.generation) ||
descriptor.generation < 1 ||
typeof descriptor.byteLength !== "number" ||
!Number.isSafeInteger(descriptor.byteLength) ||
descriptor.byteLength < 0 ||
descriptor.byteLength > policy.maxObjectBytes ||
typeof descriptor.mediaType !== "string" ||
!policy.isMediaTypeAllowed(descriptor.mediaType) ||
typeof descriptor.createdAtEpochMs !== "number" ||
!Number.isSafeInteger(descriptor.createdAtEpochMs) ||
descriptor.createdAtEpochMs < 0 ||
!integrity ||
integrity.algorithm !== "SHA-256-TREE-V1" ||
typeof integrity.rootDigestHex !== "string" ||
!SHA256_HEX.test(integrity.rootDigestHex) ||
integrity.chunkSizeBytes !== policy.chunkSizeBytes ||
!("storagePolicy" in descriptor) ||
!descriptor.storagePolicy ||
typeof descriptor.storagePolicy !== "object" ||
value.chunks.length > policy.maxChunkCount
) {
return false;
}
try {
assertValidStoragePolicy(
descriptor.storagePolicy as OpfsPreparedObject["descriptor"]["storagePolicy"],
);
} catch {
return false;
}
let total = 0;
for (let index = 0; index < value.chunks.length; index += 1) {
const chunk = value.chunks[index] as unknown;
if (
!chunk ||
typeof chunk !== "object" ||
!("sequence" in chunk) ||
chunk.sequence !== index ||
!("byteLength" in chunk) ||
typeof chunk.byteLength !== "number" ||
!Number.isSafeInteger(chunk.byteLength) ||
chunk.byteLength < 1 ||
chunk.byteLength > policy.chunkSizeBytes ||
!("digestHex" in chunk) ||
typeof chunk.digestHex !== "string" ||
!SHA256_HEX.test(chunk.digestHex)
) {
return false;
}
if (index < value.chunks.length - 1 && chunk.byteLength !== policy.chunkSizeBytes) {
return false;
}
total += chunk.byteLength;
}
return (
total === descriptor.byteLength &&
value.chunks.length ===
Math.ceil(descriptor.byteLength / policy.chunkSizeBytes)
);
}
function success(
requestId: string,
kind: OpfsWorkerRequest["kind"],
value?: OpfsWorkerResponse extends infer _Response
?
| OpfsCapabilities
| OpfsCleanupEffect
| OpfsPreparedObject
| ArrayBuffer
| boolean
| OpfsOrphanCandidateBatch
| OpfsOrphanDeleteResult
: never,
): OpfsWorkerResponse {
return value === undefined
? {
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,
protocolVersion: OPFS_WORKER_PROTOCOL_VERSION,
kind,
ok: false,
failure: workerFailure,
};
}
function mapRuntimeFailure(error: unknown): OpfsWorkerFailure {
if (error instanceof OpfsRuntimeFailure) {
return { code: error.code, retryable: error.retryable };
}
if (error instanceof DOMException) {
if (error.name === "QuotaExceededError") {
return { code: "QUOTA_EXCEEDED", retryable: true };
}
if (error.name === "NotFoundError") {
return { code: "NOT_FOUND", retryable: false };
}
if (
error.name === "NoModificationAllowedError" ||
error.name === "InvalidStateError"
) {
return { code: "BLOCKED", retryable: true };
}
if (error.name === "NotAllowedError" || error.name === "SecurityError") {
return { code: "PERMISSION_DENIED", retryable: false };
}
if (error.name === "AbortError") {
return { code: "ABORTED", retryable: false };
}
if (error.name === "NotSupportedError") {
return { code: "UNSUPPORTED", retryable: false };
}
}
return { code: "UNAVAILABLE", retryable: true };
}
function isSyncUnsupported(error: unknown): boolean {
return (
error instanceof TypeError ||
(error instanceof DOMException && error.name === "NotSupportedError")
);
}
function isNotFound(error: unknown): boolean {
return error instanceof DOMException && error.name === "NotFoundError";
}
function hasRequestId(
value: unknown,
): value is Readonly<{ requestId: string }> {
return Boolean(
value &&
typeof value === "object" &&
"requestId" in value &&
typeof value.requestId === "string" &&
value.requestId.length > 0 &&
value.requestId.length <= 128,
);
}
/**
* NS-05. Reads a request envelope's correlation exactly once, through its own
* data descriptors, so a reply can name the request it answers even when the
* runtime that would have handled it never came up. An envelope whose kind
* cannot be read stays a protocol-level failure rather than borrowing an
* unrelated kind.
*/
function requestCorrelation(
value: Readonly<{ requestId: string }>,
): Readonly<{ requestId: string; kind: OpfsWorkerRequest["kind"] }> {
let kind: unknown;
try {
kind = Object.getOwnPropertyDescriptor(value, "kind")?.value;
} catch {
kind = undefined;
}
return {
requestId: value.requestId,
kind:
typeof kind === "string" && WORKER_REQUEST_KINDS.has(kind)
? (kind as OpfsWorkerRequest["kind"])
: "CAPABILITIES",
};
}
function isWorkerRequest(value: unknown): value is OpfsWorkerRequest {
return Boolean(
hasRequestId(value) &&
"kind" in value &&
typeof value.kind === "string" &&
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,
);
}
const MAX_MANIFEST_BYTES = 1024 * 1024;
const SAFE_TRANSACTION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
const SAFE_PATH_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u;
const SHA256_HEX = /^[a-f0-9]{64}$/u;
const WORKER_REQUEST_KINDS = new Set<string>([
"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",
]);
function extractReceiptDigests(
receipt: unknown,
): readonly string[] | null {
if (!receipt || typeof receipt !== "object") return null;
const record = receipt as Record<string, unknown>;
let chunks: unknown;
if (Array.isArray(record.chunks)) {
chunks = record.chunks;
} else if (
record.preparedObject &&
typeof record.preparedObject === "object" &&
"chunks" in record.preparedObject
) {
chunks = record.preparedObject.chunks;
} else {
return null;
}
if (!Array.isArray(chunks)) return null;
const digests: string[] = [];
for (const chunk of chunks) {
if (
!chunk ||
typeof chunk !== "object" ||
!("digestHex" in chunk) ||
typeof chunk.digestHex !== "string" ||
!SHA256_HEX.test(chunk.digestHex)
) {
return null;
}
digests.push(chunk.digestHex);
}
return digests;
}