Files
clean-architecture-frontend…/src/adapters/storage/opfs/opfs-byte-store-adapter.ts
T
DongHyeonkaandClaude Opus 5 6a8281a941 fix: make OPFS finalization and public cache repair failure-atomic
STO-RR-01. finalizePut re-acquired the origin mutation lease it was already
holding. A Web Lock is not reentrant, so an ordinary PUT stopped for good at
FINALIZE; it now calls the locked cleanup directly. A strict non-reentrant fake
lease manager pins one acquire and one release per finalization. The adapter no
longer reports a failed finalization as a plain write success either: the
journal row stays COMMITTED for reconciliation, but the caller is told the
write did not settle.

STO-RR-02. A failure raised while serving a validated request now carries that
request's kind. Defaulting every catch to CAPABILITIES made the client's own
expected-kind check reject genuine quota, integrity and abort failures as
protocol breaches and report them as UNSUPPORTED. Only an envelope the runtime
could not read still answers at protocol level.

STO-RR-03. The worker client decodes a response instead of adopting it: exact
own-data descriptors, the negotiated protocol version, the exact awaited kind,
a code inside the closed BrowserDataFailure set and a boolean retryable. An
accessor, a proxy trap, an inherited or extra field and an unknown code all
close the call as UNSUPPORTED rather than leaving it to time out.

STO-RR-04. A marker read that fails transiently is unknown, not damaged, so it
no longer deletes the candidate that may be serving traffic. Only a confirmed
corrupt or missing marker enters the repair path.

STO-RR-05. Staging never deletes a candidate it did not create. A repair
replaces exact entries in place, so a failed fetch leaves every healthy asset
and the active release usable; a candidate this call created is still removed
on failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 14:00:41 +09:00

1618 lines
49 KiB
TypeScript

import type {
DurableObjectDescriptor,
DurableObjectMaintenancePort,
DurableObjectStorePort,
OpfsJournalPort,
OpfsJournalTransaction,
OpfsPolicyMaintenanceReport,
OpfsPreparedObject,
OpfsSensitiveMaintenanceReason,
OpfsReconciliationReport,
OpfsStorageScope,
PutDurableObjectRequest,
OpfsPhysicalGenerationId,
} from "../../../application/ports/browser-file-storage/opfs-ports.ts";
import {
type BrowserDataFailure,
type BrowserDataOperation,
type BrowserDataResult,
type BrowserStoragePolicy,
} from "../../../application/ports/browser-file-storage/shared.ts";
import {
abortedResult,
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
import {
byteBucket,
observeOpfsSafely,
resolveOpfsRuntimePolicy,
transactionBucket,
snapshotOpfsStoragePolicy,
snapshotOpfsStorageScope,
validateObjectWriteInput,
type OpfsRuntimePolicy,
type OpfsSafeObserver,
} from "./opfs-policy.ts";
import type { OpfsWorkerGateway } from "./opfs-worker-protocol.ts";
type BrowserFailureResult = Readonly<{
ok: false;
error: BrowserDataFailure;
}>;
export type OpfsByteStore = Readonly<{
objects: DurableObjectStorePort;
maintenance: DurableObjectMaintenancePort;
}>;
export type OpfsMaintenanceAuthorityRequest = Readonly<{
reason: OpfsSensitiveMaintenanceReason;
scope: OpfsStorageScope;
storagePolicy: BrowserStoragePolicy;
signal?: AbortSignal;
}>;
export type OpfsMaintenanceAuthorityDecision =
| Readonly<{ authorized: false }>
| Readonly<{
authorized: true;
/**
* Opaque, short-lived proof issued for this exact action and scope.
* It is passed directly to the composition-owned consumer and is never
* persisted or returned through the application port.
*/
proofToken: string;
expiresAtEpochMs: number;
}>;
export type OpfsMaintenanceAuthorityProvider = (
request: OpfsMaintenanceAuthorityRequest,
) =>
| OpfsMaintenanceAuthorityDecision
| Promise<OpfsMaintenanceAuthorityDecision>;
export type OpfsMaintenanceAuthorityConsumer = (
request: Readonly<{
reason: OpfsSensitiveMaintenanceReason;
proofToken: string;
expiresAtEpochMs: number;
scope: OpfsStorageScope;
storagePolicy: BrowserStoragePolicy;
signal?: AbortSignal;
}>,
) => boolean | Promise<boolean>;
export type OpfsByteStoreDependencies = Readonly<{
journal: OpfsJournalPort;
worker: OpfsWorkerGateway;
scope: OpfsStorageScope;
storagePolicy: BrowserStoragePolicy;
policy?: Partial<OpfsRuntimePolicy>;
createTransactionId?: () => string;
createPhysicalGenerationId?: () => OpfsPhysicalGenerationId;
/**
* Composition-owned bounded signal for compensating cleanup. It is
* deliberately separate from any caller signal.
*/
compensationSignal?: AbortSignal;
now?: () => number;
observer?: OpfsSafeObserver;
/**
* Obtains a new proof for every sensitive maintenance invocation. The
* provider must bind it to the exact reason, scope and policy.
*/
requestMaintenanceAuthority?: OpfsMaintenanceAuthorityProvider;
/**
* Atomically validates and consumes the issued proof. It must reject replay,
* scope/action/policy mismatch and expiry. The runtime also performs shape
* and short-expiry checks before invoking it.
*/
consumeMaintenanceAuthority?: OpfsMaintenanceAuthorityConsumer;
}>;
export function createOpfsByteStoreAdapter(
inputDependencies: OpfsByteStoreDependencies,
): OpfsByteStore {
const policy = resolveOpfsRuntimePolicy(inputDependencies.policy);
const scope = snapshotOpfsStorageScope(inputDependencies.scope);
const storagePolicy = snapshotOpfsStoragePolicy(
inputDependencies.storagePolicy,
);
if (
storagePolicy.namespace !== scope.namespace
) {
throw new TypeError(
"OPFS storage policy must match the bound scope.",
);
}
const dependencies: OpfsByteStoreDependencies = Object.freeze({
...inputDependencies,
journal: snapshotOpfsJournal(inputDependencies.journal),
worker: snapshotOpfsWorker(inputDependencies.worker),
scope,
storagePolicy,
policy,
});
const createTransactionId =
dependencies.createTransactionId ??
(() => globalThis.crypto.randomUUID());
const createPhysicalGenerationId =
dependencies.createPhysicalGenerationId ??
(() => {
const bytes = new Uint8Array(16);
globalThis.crypto.getRandomValues(bytes);
let hex = "";
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
return hex as OpfsPhysicalGenerationId;
});
/**
* STO-01. Compensation must not inherit the caller's already aborted signal;
* a cleanup that never starts cannot justify releasing the journal row.
*/
const compensationSignal = dependencies.compensationSignal;
const now = dependencies.now ?? Date.now;
const objects: DurableObjectStorePort = Object.freeze({
async capabilities() {
return await dependencies.worker.capabilities();
},
async put(sourceRequest: PutDurableObjectRequest) {
const requestSnapshot = snapshotPutRequest(sourceRequest);
if (!requestSnapshot.ok) return requestSnapshot;
const request = requestSnapshot.value;
const earlyFailure = validatePut(
request,
policy,
dependencies.scope,
dependencies.storagePolicy,
);
if (earlyFailure) return earlyFailure;
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_WRITE",
outcome: "STARTED",
byteBucket: byteBucket(request.source.byteLength!),
});
notifyProgress(request, "VALIDATING", 0);
const currentResult = await dependencies.journal.getCommittedObject(
dependencies.scope,
request.objectId,
);
if (!currentResult.ok) {
return observeFailure(
rebaseFailure(currentResult.error, "OBJECT_WRITE"),
dependencies.observer,
request.source.byteLength!,
);
}
const current = currentResult.value;
if (
(current === null && request.expectedGeneration !== null) ||
(current !== null &&
request.expectedGeneration !== current.descriptor.generation)
) {
return observeFailure(
browserDataFailure("CONFLICT", "OBJECT_WRITE", {
recovery: "REOPEN",
}),
dependencies.observer,
request.source.byteLength!,
);
}
const targetGeneration = (current?.descriptor.generation ?? 0) + 1;
const transactionId = createTransactionId();
// STO-01. The logical generation is reused across transactions; this
// token makes the physical target unique so a late compensation can never
// delete a newer transaction's directory.
const physicalGenerationId = createPhysicalGenerationId();
notifyProgress(request, "PREPARING", 0);
const begun = await dependencies.journal.begin({
transactionId,
mutation: "PUT",
scope: dependencies.scope,
objectId: request.objectId,
expectedGeneration: request.expectedGeneration,
targetGeneration,
targetByteLength: request.source.byteLength!,
targetStoragePolicy: dependencies.storagePolicy,
startedAtEpochMs: now(),
});
if (!begun.ok) {
return observeFailure(
rebaseFailure(begun.error, "OBJECT_WRITE"),
dependencies.observer,
request.source.byteLength!,
);
}
const descriptor: Omit<DurableObjectDescriptor, "integrity"> =
Object.freeze({
objectId: request.objectId,
scope: dependencies.scope,
generation: targetGeneration,
byteLength: request.source.byteLength!,
mediaType: request.mediaType,
createdAtEpochMs: now(),
storagePolicy: dependencies.storagePolicy,
});
const prepared = await dependencies.worker.preparePut({
transactionId,
physicalGenerationId,
descriptor,
source: request.source,
signal: request.signal,
onProgress: request.onProgress,
});
if (!prepared.ok) {
const compensated = await compensatePreparedPut(
begun.value,
physicalGenerationId,
);
if (!compensated.ok) {
return observeFailure(
compensated,
dependencies.observer,
request.source.byteLength!,
);
}
return observeFailure(
prepared,
dependencies.observer,
request.source.byteLength!,
);
}
const filesReady = await dependencies.journal.markFilesReady(
transactionId,
begun.value.fencingToken,
prepared.value,
);
if (!filesReady.ok) {
const compensated = await compensatePreparedPut(
begun.value,
physicalGenerationId,
);
if (!compensated.ok) {
return observeFailure(
compensated,
dependencies.observer,
request.source.byteLength!,
);
}
return observeFailure(
rebaseFailure(filesReady.error, "OBJECT_WRITE"),
dependencies.observer,
request.source.byteLength!,
);
}
const committed = await dependencies.journal.commitPut(
transactionId,
begun.value.fencingToken,
);
if (!committed.ok) {
// The commit response can be lost after IndexedDB commits. Reconciliation
// decides from the durable journal; rolling back here would be unsafe.
return observeFailure(
rebaseFailure(committed.error, "OBJECT_WRITE"),
dependencies.observer,
request.source.byteLength!,
);
}
notifyProgress(
request,
"FINALIZING",
prepared.value.descriptor.byteLength,
);
const finalized = await dependencies.worker.finalizePut(
transactionId,
prepared.value,
request.signal,
);
if (!finalized.ok) {
// STO-RR-01. The commit fence already passed, so the payload is durable
// and the journal keeps its COMMITTED record for reconciliation to
// settle. What did not happen is finalization: the previous generation
// and the staging directory are still present. Reporting a plain
// success here would claim a settled state nobody observed, so the
// worker's own failure is surfaced and the record is left recoverable.
return observeFailure(
rebaseFailure(finalized.error, "OBJECT_WRITE"),
dependencies.observer,
request.source.byteLength!,
);
}
await dependencies.journal.complete(
transactionId,
begun.value.fencingToken,
);
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_WRITE",
outcome: "SUCCEEDED",
byteBucket: byteBucket(prepared.value.descriptor.byteLength),
});
return browserDataSuccess(prepared.value.descriptor);
},
async open(
sourceRequest: Parameters<DurableObjectStorePort["open"]>[0],
) {
const requestSnapshot = snapshotOpenRequest(sourceRequest);
if (!requestSnapshot.ok) return requestSnapshot;
const request = requestSnapshot.value;
const aborted = abortedResult(request.signal, "OBJECT_READ");
if (aborted) return aborted;
if (
!policy.isObjectIdAllowed(request.objectId) ||
(request.generation !== undefined &&
(!Number.isSafeInteger(request.generation) ||
request.generation < 1))
) {
return browserDataFailure("INVALID_INPUT", "OBJECT_READ");
}
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_READ",
outcome: "STARTED",
});
const committed = await dependencies.journal.getCommittedObject(
dependencies.scope,
request.objectId,
);
if (!committed.ok) {
return observeFailure(
rebaseFailure(committed.error, "OBJECT_READ"),
dependencies.observer,
);
}
if (
!committed.value ||
(request.generation !== undefined &&
request.generation !== committed.value.descriptor.generation)
) {
return observeFailure(
browserDataFailure("NOT_FOUND", "OBJECT_READ", {
recovery: "REHYDRATE",
}),
dependencies.observer,
);
}
if (isExpired(committed.value.descriptor, now())) {
await objects.remove({
objectId: request.objectId,
expectedGeneration: committed.value.descriptor.generation,
signal: request.signal,
});
return observeFailure(
browserDataFailure("EXPIRED_RESOURCE", "OBJECT_READ", {
recovery: "REHYDRATE",
}),
dependencies.observer,
committed.value.descriptor.byteLength,
);
}
const opened = await dependencies.worker.openObject(
committed.value,
request.signal,
);
if (!opened.ok) {
return observeFailure(
opened,
dependencies.observer,
committed.value.descriptor.byteLength,
);
}
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_READ",
outcome: "SUCCEEDED",
byteBucket: byteBucket(committed.value.descriptor.byteLength),
});
return browserDataSuccess({
descriptor: committed.value.descriptor,
source: opened.value,
});
},
async remove(
sourceRequest: Parameters<DurableObjectStorePort["remove"]>[0],
) {
const requestSnapshot = snapshotRemoveRequest(sourceRequest);
if (!requestSnapshot.ok) return requestSnapshot;
const request = requestSnapshot.value;
const aborted = abortedResult(request.signal, "OBJECT_DELETE");
if (aborted) return aborted;
if (
!policy.isObjectIdAllowed(request.objectId) ||
!Number.isSafeInteger(request.expectedGeneration) ||
request.expectedGeneration < 1
) {
return browserDataFailure("INVALID_INPUT", "OBJECT_DELETE");
}
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_DELETE",
outcome: "STARTED",
});
const current = await dependencies.journal.getCommittedObject(
dependencies.scope,
request.objectId,
);
if (!current.ok) {
return observeFailure(
rebaseFailure(current.error, "OBJECT_DELETE"),
dependencies.observer,
);
}
if (!current.value) {
return observeFailure(
browserDataFailure("NOT_FOUND", "OBJECT_DELETE"),
dependencies.observer,
);
}
if (
current.value.descriptor.generation !== request.expectedGeneration
) {
return observeFailure(
browserDataFailure("CONFLICT", "OBJECT_DELETE", {
recovery: "REOPEN",
}),
dependencies.observer,
);
}
const transactionId = createTransactionId();
const begun = await dependencies.journal.begin({
transactionId,
mutation: "DELETE",
scope: dependencies.scope,
objectId: request.objectId,
expectedGeneration: request.expectedGeneration,
targetGeneration: request.expectedGeneration + 1,
targetByteLength: 0,
targetStoragePolicy: current.value.descriptor.storagePolicy,
startedAtEpochMs: now(),
});
if (!begun.ok) {
return observeFailure(
rebaseFailure(begun.error, "OBJECT_DELETE"),
dependencies.observer,
);
}
const committed = await dependencies.journal.commitDelete(
transactionId,
begun.value.fencingToken,
);
if (!committed.ok) {
return observeFailure(
rebaseFailure(committed.error, "OBJECT_DELETE"),
dependencies.observer,
);
}
const removed = await dependencies.worker.removeObject(
dependencies.scope,
request.objectId,
request.expectedGeneration,
request.signal,
);
if (removed.ok) {
await dependencies.journal.complete(
transactionId,
begun.value.fencingToken,
);
}
// Logical deletion is already committed. Physical cleanup is retryable
// maintenance and must not make the caller repeat a non-idempotent delete.
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_DELETE",
outcome: "SUCCEEDED",
});
return browserDataSuccess(undefined);
},
});
const maintenance: DurableObjectMaintenancePort = Object.freeze({
async reconcile(
sourceRequest: NonNullable<
Parameters<DurableObjectMaintenancePort["reconcile"]>[0]
> = {},
) {
const requestSnapshot = snapshotReconciliationRequest(sourceRequest);
if (!requestSnapshot.ok) return requestSnapshot;
const request = requestSnapshot.value;
const aborted = abortedResult(request.signal, "OBJECT_RECONCILE");
if (aborted) return aborted;
if (
!isValidOptionalPositiveInteger(request.budgetMs) ||
!isValidOptionalPositiveInteger(request.maxTransactions)
) {
return browserDataFailure("INVALID_INPUT", "OBJECT_RECONCILE");
}
if (
exceedsConfiguredCeiling(
request.budgetMs,
policy.reconciliationBudgetMs,
) ||
exceedsConfiguredCeiling(
request.maxTransactions,
policy.reconciliationBatchSize,
)
) {
return browserDataFailure("LIMIT_EXCEEDED", "OBJECT_RECONCILE");
}
const budgetMs =
request.budgetMs ?? policy.reconciliationBudgetMs;
const maxTransactions =
request.maxTransactions ?? policy.reconciliationBatchSize;
const deadline = now() + budgetMs;
const page = await dependencies.journal.listIncomplete(maxTransactions);
if (!page.ok) {
return rebaseFailure(page.error, "OBJECT_RECONCILE");
}
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_RECONCILE",
outcome: "STARTED",
transactionBucket: transactionBucket(page.value.transactions.length),
});
const report: {
inspectedTransactions: number;
committedTransactions: number;
rolledBackTransactions: number;
cleanedTransactions: number;
inspectedOrphanChunks: number;
deletedOrphanChunks: number;
orphanGcStatus:
| "COMPLETED"
| "DEADLINE_REACHED"
| "STAGING_STATE_UNREADABLE";
moreTransactionsAvailable: boolean;
deadlineReached: boolean;
} = {
inspectedTransactions: 0,
committedTransactions: 0,
rolledBackTransactions: 0,
cleanedTransactions: 0,
inspectedOrphanChunks: 0,
deletedOrphanChunks: 0,
orphanGcStatus: "COMPLETED",
moreTransactionsAvailable: page.value.moreAvailable,
deadlineReached: false,
};
for (const transaction of page.value.transactions) {
if (request.signal?.aborted) {
return browserDataFailure("ABORTED", "OBJECT_RECONCILE");
}
if (now() >= deadline) {
report.deadlineReached = true;
report.moreTransactionsAvailable = true;
break;
}
report.inspectedTransactions += 1;
const reconciled = await reconcileTransaction(transaction, request.signal);
if (!reconciled.ok) {
return observeFailure(reconciled, dependencies.observer);
}
report.committedTransactions += reconciled.value.committed;
report.rolledBackTransactions += reconciled.value.rolledBack;
report.cleanedTransactions += reconciled.value.cleaned;
}
if (now() >= deadline) {
report.deadlineReached = true;
report.orphanGcStatus = "DEADLINE_REACHED";
} else {
const gc = await reconcileOrphanChunks(
deadline,
request.signal,
);
if (!gc.ok) return observeFailure(gc, dependencies.observer);
report.inspectedOrphanChunks = gc.value.inspected;
report.deletedOrphanChunks = gc.value.deleted;
report.orphanGcStatus = gc.value.status;
if (gc.value.status === "DEADLINE_REACHED") {
report.deadlineReached = true;
}
}
observeOpfsSafely(dependencies.observer, {
operation: "OBJECT_RECONCILE",
outcome: "SUCCEEDED",
transactionBucket: transactionBucket(report.inspectedTransactions),
});
return browserDataSuccess<OpfsReconciliationReport>(
Object.freeze({ ...report }),
);
},
async enforcePolicies(
sourceRequest: Parameters<
DurableObjectMaintenancePort["enforcePolicies"]
>[0],
) {
const requestSnapshot = snapshotPolicyMaintenanceRequest(
sourceRequest,
);
if (!requestSnapshot.ok) return requestSnapshot;
const request = requestSnapshot.value;
const aborted = abortedResult(request.signal, "OBJECT_RECONCILE");
if (aborted) return aborted;
if (
!isValidOptionalPositiveInteger(request.budgetMs) ||
!isValidOptionalPositiveInteger(request.maxObjects) ||
(request.reason === "PRESSURE" &&
(!Number.isSafeInteger(request.targetBytesToRelease) ||
request.targetBytesToRelease < 1))
) {
return browserDataFailure("INVALID_INPUT", "OBJECT_RECONCILE");
}
if (
exceedsConfiguredCeiling(
request.budgetMs,
policy.reconciliationBudgetMs,
) ||
exceedsConfiguredCeiling(
request.maxObjects,
policy.reconciliationBatchSize,
)
) {
return browserDataFailure("LIMIT_EXCEEDED", "OBJECT_RECONCILE");
}
if (
!maintenanceReasonAllowed(
request.reason,
dependencies.storagePolicy,
)
) {
return browserDataFailure(
"POLICY_REJECTED",
"OBJECT_RECONCILE",
{ recovery: dependencies.storagePolicy.unavailableFallback },
);
}
const budgetMs =
request.budgetMs ?? policy.reconciliationBudgetMs;
const maxObjects =
request.maxObjects ?? policy.reconciliationBatchSize;
const deadline = now() + budgetMs;
const authorized = await authorizePolicyMaintenance(request);
if (!authorized.ok) return authorized;
if (request.signal?.aborted) {
return browserDataFailure("ABORTED", "OBJECT_RECONCILE");
}
if (now() >= deadline) {
// Authority acquisition is part of the maintenance budget. No journal
// read is allowed after the deadline, so remaining work is reported
// conservatively instead of implying that the partition was scanned.
return browserDataSuccess<OpfsPolicyMaintenanceReport>(
Object.freeze({
inspectedObjects: 0,
removedObjects: 0,
releasedBytes: 0,
moreObjectsAvailable: true,
deadlineReached: true,
}),
);
}
let afterObjectId: string | undefined;
let inspectedObjects = 0;
let removedObjects = 0;
let releasedBytes = 0;
let moreObjectsAvailable = false;
let deadlineReached = false;
while (
inspectedObjects < maxObjects &&
now() < deadline
) {
const page = await dependencies.journal.listCommittedObjects({
scope: dependencies.scope,
afterObjectId,
limit: Math.min(
policy.reconciliationBatchSize,
maxObjects - inspectedObjects,
),
});
if (!page.ok) {
return rebaseFailure(page.error, "OBJECT_RECONCILE");
}
const candidates = [...page.value.objects].sort(
policyMaintenanceOrder,
);
for (const candidate of candidates) {
if (
request.signal?.aborted ||
now() >= deadline ||
inspectedObjects >= maxObjects
) {
deadlineReached = now() >= deadline;
moreObjectsAvailable = true;
break;
}
inspectedObjects += 1;
if (!shouldRemoveForPolicy(candidate, request, now())) continue;
const removed = await objects.remove({
objectId: candidate.descriptor.objectId,
expectedGeneration: candidate.descriptor.generation,
signal: request.signal,
});
if (!removed.ok) {
if (removed.error.code === "CONFLICT" || removed.error.code === "NOT_FOUND") {
continue;
}
return rebaseFailure(removed.error, "OBJECT_RECONCILE");
}
removedObjects += 1;
releasedBytes += candidate.descriptor.byteLength;
if (
request.reason === "PRESSURE" &&
releasedBytes >= request.targetBytesToRelease
) {
moreObjectsAvailable = page.value.moreAvailable;
break;
}
}
if (
deadlineReached ||
(request.reason === "PRESSURE" &&
releasedBytes >= request.targetBytesToRelease) ||
!page.value.moreAvailable ||
!page.value.nextObjectId
) {
moreObjectsAvailable ||= page.value.moreAvailable;
break;
}
afterObjectId = page.value.nextObjectId;
moreObjectsAvailable = page.value.moreAvailable;
}
const report: OpfsPolicyMaintenanceReport = Object.freeze({
inspectedObjects,
removedObjects,
releasedBytes,
moreObjectsAvailable,
deadlineReached,
});
return browserDataSuccess(report);
},
});
async function authorizePolicyMaintenance(
request: Parameters<
DurableObjectMaintenancePort["enforcePolicies"]
>[0],
): Promise<BrowserDataResult<void>> {
if (!requiresMaintenanceAuthority(request)) {
return browserDataSuccess(undefined);
}
const requestAuthority = dependencies.requestMaintenanceAuthority;
const consumeAuthority = dependencies.consumeMaintenanceAuthority;
if (!requestAuthority || !consumeAuthority) {
return browserDataFailure(
"POLICY_REJECTED",
"OBJECT_RECONCILE",
{ recovery: "READ_ONLY" },
);
}
let decision: OpfsMaintenanceAuthorityDecision;
try {
decision = await requestAuthority(
Object.freeze({
reason: request.reason,
scope: dependencies.scope,
storagePolicy: dependencies.storagePolicy,
...(request.signal ? { signal: request.signal } : {}),
}),
);
} catch {
return browserDataFailure(
"POLICY_REJECTED",
"OBJECT_RECONCILE",
{ recovery: "READ_ONLY" },
);
}
if (request.signal?.aborted) {
return browserDataFailure("ABORTED", "OBJECT_RECONCILE");
}
if (
!decision ||
decision.authorized !== true ||
typeof decision.proofToken !== "string" ||
!OPAQUE_AUTHORITY_PROOF.test(decision.proofToken) ||
!Number.isSafeInteger(decision.expiresAtEpochMs)
) {
return browserDataFailure(
"POLICY_REJECTED",
"OBJECT_RECONCILE",
{ recovery: "READ_ONLY" },
);
}
let authorizationEpochMs: number;
try {
authorizationEpochMs = now();
} catch {
return browserDataFailure(
"UNAVAILABLE",
"OBJECT_RECONCILE",
{ retryable: true, recovery: "RETRY" },
);
}
if (
!Number.isSafeInteger(authorizationEpochMs) ||
authorizationEpochMs < 0 ||
decision.expiresAtEpochMs <= authorizationEpochMs ||
decision.expiresAtEpochMs - authorizationEpochMs >
MAX_AUTHORITY_PROOF_LIFETIME_MS
) {
return browserDataFailure(
"POLICY_REJECTED",
"OBJECT_RECONCILE",
{ recovery: "READ_ONLY" },
);
}
const proofToken = decision.proofToken;
const expiresAtEpochMs = decision.expiresAtEpochMs;
let consumed: boolean;
try {
consumed = await consumeAuthority(
Object.freeze({
reason: request.reason,
proofToken,
expiresAtEpochMs,
scope: dependencies.scope,
storagePolicy: dependencies.storagePolicy,
...(request.signal ? { signal: request.signal } : {}),
}),
);
} catch {
return browserDataFailure(
"POLICY_REJECTED",
"OBJECT_RECONCILE",
{ recovery: "READ_ONLY" },
);
}
// The proof is intentionally not retained after this invocation. The
// composition consumer owns atomic replay prevention across runtimes.
if (request.signal?.aborted) {
return browserDataFailure("ABORTED", "OBJECT_RECONCILE");
}
return consumed
? browserDataSuccess(undefined)
: browserDataFailure(
"POLICY_REJECTED",
"OBJECT_RECONCILE",
{ recovery: "READ_ONLY" },
);
}
return Object.freeze({ objects, maintenance });
/**
* STO-01. The compensating half of the put saga.
*
* The journal row and its budget reservation are the only durable evidence
* that a physical staging generation may still exist, so they are released
* exactly when the physical effect is confirmed `CLEANED` or
* `ALREADY_CLEAN`. A timeout, crash, malformed response or `EFFECT_UNKNOWN`
* keeps `PREPARING`/`FILES_READY` in place and asks for reconciliation.
*/
async function compensatePreparedPut(
transaction: OpfsJournalTransaction,
physicalGenerationId: OpfsPhysicalGenerationId,
): Promise<BrowserDataResult<void>> {
const cleanup = await dependencies.worker.abortPreparedPut({
scope: transaction.scope,
transactionId: transaction.transactionId,
physicalGenerationId,
...(compensationSignal ? { signal: compensationSignal } : {}),
});
if (!cleanup.ok || cleanup.value.kind === "EFFECT_UNKNOWN") {
return browserDataFailure("CONFLICT", "OBJECT_RECONCILE", {
recovery: "RETRY",
});
}
const rolledBack = await dependencies.journal.rollback(
transaction.transactionId,
transaction.fencingToken,
);
if (!rolledBack.ok) {
return rebaseFailure(rolledBack.error, "OBJECT_RECONCILE");
}
return browserDataSuccess(undefined);
}
async function reconcileTransaction(
transaction: OpfsJournalTransaction,
signal: AbortSignal | undefined,
): Promise<
BrowserDataResult<
Readonly<{ committed: number; rolledBack: number; cleaned: number }>
>
> {
if (transaction.phase === "PREPARING") {
const cleaned = await dependencies.worker.cleanupTransaction(
transaction.scope,
transaction.transactionId,
signal,
);
if (!cleaned.ok) return cleaned;
if (cleaned.value.kind === "EFFECT_UNKNOWN") {
return browserDataFailure("CONFLICT", "OBJECT_RECONCILE", {
recovery: "RETRY",
});
}
const rolledBack = await dependencies.journal.rollback(
transaction.transactionId,
transaction.fencingToken,
);
if (!rolledBack.ok) {
return rebaseFailure(rolledBack.error, "OBJECT_RECONCILE");
}
return browserDataSuccess({ committed: 0, rolledBack: 1, cleaned: 1 });
}
if (transaction.phase === "FILES_READY") {
if (
transaction.mutation !== "PUT" ||
!transaction.preparedObject
) {
return browserDataFailure("CORRUPT_DATA", "OBJECT_RECONCILE", {
recovery: "READ_ONLY",
});
}
const verified = await dependencies.worker.verifyObject(
transaction.preparedObject,
signal,
);
if (!verified.ok) return verified;
if (!verified.value) {
const cleaned = await dependencies.worker.cleanupTransaction(
transaction.scope,
transaction.transactionId,
signal,
);
if (!cleaned.ok) return cleaned;
const rolledBack = await dependencies.journal.rollback(
transaction.transactionId,
transaction.fencingToken,
);
if (!rolledBack.ok) {
return rebaseFailure(rolledBack.error, "OBJECT_RECONCILE");
}
return browserDataSuccess({
committed: 0,
rolledBack: 1,
cleaned: 1,
});
}
const committed = await dependencies.journal.commitPut(
transaction.transactionId,
transaction.fencingToken,
);
if (!committed.ok) {
return rebaseFailure(committed.error, "OBJECT_RECONCILE");
}
const finalized = await dependencies.worker.finalizePut(
transaction.transactionId,
transaction.preparedObject,
signal,
);
if (!finalized.ok) return finalized;
const completed = await dependencies.journal.complete(
transaction.transactionId,
transaction.fencingToken,
);
if (!completed.ok) {
return rebaseFailure(completed.error, "OBJECT_RECONCILE");
}
return browserDataSuccess({ committed: 1, rolledBack: 0, cleaned: 1 });
}
if (transaction.phase === "COMMITTED") {
if (transaction.mutation === "PUT") {
if (!transaction.preparedObject) {
return browserDataFailure("CORRUPT_DATA", "OBJECT_RECONCILE", {
recovery: "READ_ONLY",
});
}
const finalized = await dependencies.worker.finalizePut(
transaction.transactionId,
transaction.preparedObject,
signal,
);
if (!finalized.ok) return finalized;
} else {
const removed = await dependencies.worker.removeObject(
transaction.scope,
transaction.objectId,
Math.max(1, transaction.targetGeneration - 1),
signal,
);
if (!removed.ok) return removed;
}
const completed = await dependencies.journal.complete(
transaction.transactionId,
transaction.fencingToken,
);
if (!completed.ok) {
return rebaseFailure(completed.error, "OBJECT_RECONCILE");
}
return browserDataSuccess({ committed: 0, rolledBack: 0, cleaned: 1 });
}
return browserDataFailure("CORRUPT_DATA", "OBJECT_RECONCILE", {
recovery: "READ_ONLY",
});
}
async function reconcileOrphanChunks(
deadline: number,
signal: AbortSignal | undefined,
): Promise<
BrowserDataResult<
Readonly<{
inspected: number;
deleted: number;
status:
| "COMPLETED"
| "DEADLINE_REACHED"
| "STAGING_STATE_UNREADABLE";
}>
>
> {
const olderThanEpochMs = Math.max(
0,
now() - policy.orphanGracePeriodMs,
);
const candidates = await dependencies.worker.listOrphanCandidates(
dependencies.scope,
olderThanEpochMs,
policy.orphanGcBatchSize,
signal,
);
if (!candidates.ok) return candidates;
if (!candidates.value.safeToSweep) {
return browserDataSuccess({
inspected: 0,
deleted: 0,
status: "STAGING_STATE_UNREADABLE",
});
}
let inspected = 0;
let deleted = 0;
for (const digestHex of candidates.value.digests) {
if (signal?.aborted) {
return browserDataFailure("ABORTED", "OBJECT_RECONCILE");
}
if (now() >= deadline) {
return browserDataSuccess({
inspected,
deleted,
status: "DEADLINE_REACHED",
});
}
inspected += 1;
const referenced = await dependencies.journal.isChunkReferenced(
dependencies.scope,
digestHex,
);
if (!referenced.ok) {
return rebaseFailure(referenced.error, "OBJECT_RECONCILE");
}
if (referenced.value) continue;
const removal = await dependencies.worker.deleteOrphanChunk(
dependencies.scope,
digestHex,
olderThanEpochMs,
signal,
);
if (!removal.ok) return removal;
if (removal.value.skippedUnsafe) {
return browserDataSuccess({
inspected,
deleted,
status: "STAGING_STATE_UNREADABLE",
});
}
if (removal.value.deleted) deleted += 1;
}
return browserDataSuccess({
inspected,
deleted,
status:
candidates.value.moreAvailable
? "DEADLINE_REACHED"
: "COMPLETED",
});
}
}
function snapshotPutRequest(
input: PutDurableObjectRequest,
): BrowserDataResult<PutDurableObjectRequest> {
try {
if (!input || typeof input !== "object") {
return browserDataFailure("INVALID_INPUT", "OBJECT_WRITE");
}
const sourceInput = input.source;
if (!sourceInput || typeof sourceInput !== "object") {
return browserDataFailure("INVALID_INPUT", "OBJECT_WRITE");
}
const objectId = input.objectId;
const expectedGeneration = input.expectedGeneration;
const mediaType = input.mediaType;
const signal = input.signal;
const onProgress = input.onProgress;
const byteLength = sourceInput.byteLength;
const stream = sourceInput.stream;
if (
typeof stream !== "function" ||
(onProgress !== undefined &&
typeof onProgress !== "function")
) {
return browserDataFailure("INVALID_INPUT", "OBJECT_WRITE");
}
const boundStream = stream.bind(sourceInput);
const source = Object.freeze({
byteLength,
stream(signal: AbortSignal) {
return boundStream(signal);
},
});
return browserDataSuccess(
Object.freeze({
objectId,
expectedGeneration,
mediaType,
source,
...(signal === undefined ? {} : { signal }),
...(onProgress === undefined
? {}
: { onProgress }),
}),
);
} catch {
return browserDataFailure("INVALID_INPUT", "OBJECT_WRITE");
}
}
function snapshotOpenRequest(
input: Parameters<DurableObjectStorePort["open"]>[0],
): BrowserDataResult<
Parameters<DurableObjectStorePort["open"]>[0]
> {
try {
if (!input || typeof input !== "object") {
return browserDataFailure("INVALID_INPUT", "OBJECT_READ");
}
const objectId = input.objectId;
const generation = input.generation;
const signal = input.signal;
return browserDataSuccess(
Object.freeze({
objectId,
...(generation === undefined
? {}
: { generation }),
...(signal === undefined ? {} : { signal }),
}),
);
} catch {
return browserDataFailure("INVALID_INPUT", "OBJECT_READ");
}
}
function snapshotRemoveRequest(
input: Parameters<DurableObjectStorePort["remove"]>[0],
): BrowserDataResult<
Parameters<DurableObjectStorePort["remove"]>[0]
> {
try {
if (!input || typeof input !== "object") {
return browserDataFailure("INVALID_INPUT", "OBJECT_DELETE");
}
const objectId = input.objectId;
const expectedGeneration = input.expectedGeneration;
const signal = input.signal;
return browserDataSuccess(
Object.freeze({
objectId,
expectedGeneration,
...(signal === undefined ? {} : { signal }),
}),
);
} catch {
return browserDataFailure("INVALID_INPUT", "OBJECT_DELETE");
}
}
function snapshotReconciliationRequest(
input: NonNullable<
Parameters<DurableObjectMaintenancePort["reconcile"]>[0]
>,
): BrowserDataResult<
NonNullable<
Parameters<DurableObjectMaintenancePort["reconcile"]>[0]
>
> {
try {
if (!input || typeof input !== "object") {
return browserDataFailure("INVALID_INPUT", "OBJECT_RECONCILE");
}
const budgetMs = input.budgetMs;
const maxTransactions = input.maxTransactions;
const signal = input.signal;
return browserDataSuccess(
Object.freeze({
...(budgetMs === undefined
? {}
: { budgetMs }),
...(maxTransactions === undefined
? {}
: { maxTransactions }),
...(signal === undefined ? {} : { signal }),
}),
);
} catch {
return browserDataFailure("INVALID_INPUT", "OBJECT_RECONCILE");
}
}
function snapshotPolicyMaintenanceRequest(
input: Parameters<
DurableObjectMaintenancePort["enforcePolicies"]
>[0],
): BrowserDataResult<
Parameters<DurableObjectMaintenancePort["enforcePolicies"]>[0]
> {
try {
if (!input || typeof input !== "object") {
return browserDataFailure("INVALID_INPUT", "OBJECT_RECONCILE");
}
const reason = input.reason;
const budgetMs = input.budgetMs;
const maxObjects = input.maxObjects;
const signal = input.signal;
const common = {
...(budgetMs === undefined
? {}
: { budgetMs }),
...(maxObjects === undefined
? {}
: { maxObjects }),
...(signal === undefined ? {} : { signal }),
};
switch (reason) {
case "TTL":
case "LOGOUT":
case "SESSION_END":
case "UNTIL_SYNCED":
case "ACCOUNT_DELETION":
return browserDataSuccess(
Object.freeze({ reason, ...common }),
);
case "PRESSURE": {
const targetBytesToRelease = input.targetBytesToRelease;
return browserDataSuccess(
Object.freeze({
reason: "PRESSURE",
targetBytesToRelease,
...common,
}),
);
}
default:
return browserDataFailure(
"INVALID_INPUT",
"OBJECT_RECONCILE",
);
}
} catch {
return browserDataFailure("INVALID_INPUT", "OBJECT_RECONCILE");
}
}
function snapshotOpfsJournal(
source: OpfsJournalPort,
): OpfsJournalPort {
if (!source || typeof source !== "object") {
throw new TypeError("OPFS journal dependency is invalid.");
}
const {
getCommittedObject,
begin,
markFilesReady,
commitPut,
commitDelete,
complete,
rollback,
listIncomplete,
listCommittedObjects,
isChunkReferenced,
} = source;
if (
[
getCommittedObject,
begin,
markFilesReady,
commitPut,
commitDelete,
complete,
rollback,
listIncomplete,
listCommittedObjects,
isChunkReferenced,
].some((method) => typeof method !== "function")
) {
throw new TypeError("OPFS journal dependency is invalid.");
}
return Object.freeze({
getCommittedObject: getCommittedObject.bind(source),
begin: begin.bind(source),
markFilesReady: markFilesReady.bind(source),
commitPut: commitPut.bind(source),
commitDelete: commitDelete.bind(source),
complete: complete.bind(source),
rollback: rollback.bind(source),
listIncomplete: listIncomplete.bind(source),
listCommittedObjects: listCommittedObjects.bind(source),
isChunkReferenced: isChunkReferenced.bind(source),
});
}
function snapshotOpfsWorker(
source: OpfsWorkerGateway,
): OpfsWorkerGateway {
if (!source || typeof source !== "object") {
throw new TypeError("OPFS worker dependency is invalid.");
}
const {
capabilities,
preparePut,
verifyObject,
openObject,
removeObject,
cleanupTransaction,
abortPreparedPut,
finalizePut,
listOrphanCandidates,
deleteOrphanChunk,
close,
} = source;
if (
[
capabilities,
preparePut,
verifyObject,
openObject,
removeObject,
cleanupTransaction,
abortPreparedPut,
finalizePut,
listOrphanCandidates,
deleteOrphanChunk,
close,
].some((method) => typeof method !== "function")
) {
throw new TypeError("OPFS worker dependency is invalid.");
}
return Object.freeze({
capabilities: capabilities.bind(source),
preparePut: preparePut.bind(source),
verifyObject: verifyObject.bind(source),
openObject: openObject.bind(source),
removeObject: removeObject.bind(source),
cleanupTransaction: cleanupTransaction.bind(source),
abortPreparedPut: abortPreparedPut.bind(source),
finalizePut: finalizePut.bind(source),
listOrphanCandidates: listOrphanCandidates.bind(source),
deleteOrphanChunk: deleteOrphanChunk.bind(source),
close: close.bind(source),
});
}
function validatePut(
request: PutDurableObjectRequest,
policy: OpfsRuntimePolicy,
scope: OpfsStorageScope,
storagePolicy: BrowserStoragePolicy,
): BrowserDataResult<never> | null {
const aborted = abortedResult(request.signal, "OBJECT_WRITE");
if (aborted) return aborted;
if (
!validateObjectWriteInput(
{
objectId: request.objectId,
scope,
expectedGeneration: request.expectedGeneration,
mediaType: request.mediaType,
byteLength: request.source.byteLength,
storagePolicy,
},
policy,
)
) {
return browserDataFailure(
request.source.byteLength !== null &&
request.source.byteLength > policy.maxObjectBytes
? "LIMIT_EXCEEDED"
: "INVALID_INPUT",
"OBJECT_WRITE",
);
}
return null;
}
function rebaseFailure(
failure: BrowserDataFailure,
operation: BrowserDataOperation,
): BrowserFailureResult {
return {
ok: false,
error: Object.freeze({ ...failure, operation }),
};
}
function observeFailure(
failure: BrowserDataResult<unknown>,
observer: OpfsSafeObserver | undefined,
byteLength?: number,
): BrowserFailureResult {
if (failure.ok) {
throw new TypeError("Expected an OPFS failure result.");
}
observeOpfsSafely(observer, {
operation: failure.error.operation,
outcome: "FAILED",
failureCode: failure.error.code,
byteBucket:
byteLength === undefined ? undefined : byteBucket(byteLength),
});
return failure;
}
function notifyProgress(
request: PutDurableObjectRequest,
phase: "VALIDATING" | "PREPARING" | "FINALIZING",
transferredBytes: number,
): void {
try {
request.onProgress?.({
phase,
transferredBytes,
totalBytes: request.source.byteLength,
});
} catch {
// A UI callback cannot affect persistence.
}
}
function exceedsConfiguredCeiling(
requested: number | undefined,
configured: number,
): boolean {
return requested !== undefined && requested > configured;
}
function isValidOptionalPositiveInteger(
value: number | undefined,
): boolean {
return (
value === undefined ||
(Number.isSafeInteger(value) && value > 0)
);
}
function isExpired(
descriptor: DurableObjectDescriptor,
nowEpochMs: number,
): boolean {
const retention = descriptor.storagePolicy.retention;
return (
retention.kind === "TTL" &&
descriptor.createdAtEpochMs + retention.maxAgeMs <= nowEpochMs
);
}
function policyMaintenanceOrder(
left: OpfsPreparedObject,
right: OpfsPreparedObject,
): number {
const priorities = {
RECONSTRUCTABLE: 0,
SYNCED_COPY: 1,
USER_AUTHORED: 2,
} as const;
const priority =
priorities[left.descriptor.storagePolicy.evictionPriority] -
priorities[right.descriptor.storagePolicy.evictionPriority];
return priority !== 0
? priority
: left.descriptor.createdAtEpochMs -
right.descriptor.createdAtEpochMs;
}
function shouldRemoveForPolicy(
object: OpfsPreparedObject,
request: Parameters<
DurableObjectMaintenancePort["enforcePolicies"]
>[0],
nowEpochMs: number,
): boolean {
if (request.reason === "TTL") {
return isExpired(object.descriptor, nowEpochMs);
}
if (request.reason === "LOGOUT") {
return (
object.descriptor.storagePolicy.logoutAction ===
"PURGE_PARTITION" ||
object.descriptor.storagePolicy.logoutAction ===
"EXPORT_THEN_PURGE"
);
}
if (request.reason === "SESSION_END") {
return object.descriptor.storagePolicy.retention.kind === "SESSION";
}
if (request.reason === "UNTIL_SYNCED") {
return object.descriptor.storagePolicy.retention.kind === "UNTIL_SYNCED";
}
if (request.reason === "ACCOUNT_DELETION") {
return (
object.descriptor.storagePolicy.accountDeletionAction ===
"PURGE_PARTITION"
);
}
return (
object.descriptor.storagePolicy.retention.kind !== "EXPLICIT_DELETE" &&
object.descriptor.storagePolicy.pressureAction ===
"EVICT_RECONSTRUCTABLE"
);
}
function maintenanceReasonAllowed(
reason: Parameters<
DurableObjectMaintenancePort["enforcePolicies"]
>[0]["reason"],
policy: BrowserStoragePolicy,
): boolean {
switch (reason) {
case "TTL":
return policy.retention.kind === "TTL";
case "SESSION_END":
return policy.retention.kind === "SESSION";
case "UNTIL_SYNCED":
return policy.retention.kind === "UNTIL_SYNCED";
case "LOGOUT":
return (
policy.accountScope === "OPAQUE_PARTITION" &&
(policy.logoutAction === "PURGE_PARTITION" ||
policy.logoutAction === "EXPORT_THEN_PURGE")
);
case "ACCOUNT_DELETION":
return (
policy.accountScope === "OPAQUE_PARTITION" &&
policy.accountDeletionAction === "PURGE_PARTITION"
);
case "PRESSURE":
return policy.pressureAction === "EVICT_RECONSTRUCTABLE";
}
}
function requiresMaintenanceAuthority(
request: Parameters<
DurableObjectMaintenancePort["enforcePolicies"]
>[0],
): request is Extract<
Parameters<DurableObjectMaintenancePort["enforcePolicies"]>[0],
{ reason: "LOGOUT" | "UNTIL_SYNCED" | "ACCOUNT_DELETION" }
> {
return (
request.reason === "LOGOUT" ||
request.reason === "UNTIL_SYNCED" ||
request.reason === "ACCOUNT_DELETION"
);
}
const OPAQUE_AUTHORITY_PROOF = /^[A-Za-z0-9_-]{16,512}$/u;
const MAX_AUTHORITY_PROOF_LIFETIME_MS = 5 * 60 * 1_000;