Files
clean-architecture-frontend…/src/adapters/storage/opfs/indexeddb-opfs-journal.ts
T
DongHyeonkaandClaude Opus 5 217c1dd52a refactor: move the OPFS journal onto the IndexedDB kernel, and make the fake
IndexedDB enforce unique indexes

저널은 이제 트랜잭션 안 요청 28곳이 각자 오류를 보고한다. 코드 줄은
1744 -> 1724로 줄었고 전체 줄이 1817 -> 1881로 는 것은 주석이다. 이행 전
이 파일의 주석은 6줄이었다.

사양의 OP-1 전제는 틀렸다. "중복 put의 답이 달라진다"고 봤으나 이행 전후
모두 CONFLICT / retryable:false / recovery:NONE이다. 요청 오류를 아무도
처리하지 않으면 스토어가 바로 그 에러로 abort해서 transaction.error가
request.error와 같기 때문이다. 바뀐 것은 답이 아니라 출처다.

그 과정에서 가짜 IndexedDB가 unique 인덱스를 전혀 강제하지 않는 것을
찾았다. 보강 전에는 중복 begin이 ok:true로 성공했다 — 브라우저가 거부할
상태를 테스트가 조용히 허용하고 있었다. put/add에 검사를 넣었다.
unique:true 인덱스는 레포 전체에서 이 저널의 2개뿐이라 반경이 좁고, 전체
test:unit으로 파급이 없음을 확인했다.

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

1882 lines
63 KiB
TypeScript

import type {
BeginOpfsJournalTransaction,
OpfsCommittedObjectPage,
OpfsJournalPage,
OpfsJournalPort,
OpfsJournalTransaction,
OpfsPreparedObject,
OpfsStorageScope,
} from "../../../application/ports/browser-file-storage/opfs-ports.ts";
import {
assertValidStoragePolicy,
type BrowserDataFailure,
type BrowserDataOperation,
type BrowserDataResult,
type BrowserStoragePolicy,
} from "../../../application/ports/browser-file-storage/shared.ts";
import {
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
import { snapshotAbortTimers } from "../../platform/abortable-operation.ts";
import {
createIndexedDbConnection,
openIndexedDbDatabase,
type IndexedDbTranslate,
} from "../../platform/indexeddb-connection.ts";
import {
onIndexedDbRequest,
runIndexedDbTransaction,
walkIndexedDbCursor,
type IndexedDbRequestSink,
type IndexedDbTransactionContext,
} from "../../platform/indexeddb-transaction.ts";
import { mapIndexedDbException } from "../indexeddb/indexeddb-failure.ts";
import { isValidOpfsStorageScope } from "./opfs-policy.ts";
export type IndexedDbOpfsJournalDependencies = Readonly<{
authorityToken: string;
databaseName?: string;
factory?: IDBFactory;
keyRange?: Pick<typeof IDBKeyRange, "bound">;
createFencingToken?: () => string;
blockedTimeoutMs?: number;
scheduler?: Readonly<{
setTimeout(callback: () => void, milliseconds: number): unknown;
clearTimeout(handle: unknown): void;
}>;
observe?: (
event: Readonly<{
operation: BrowserDataOperation;
outcome: "SUCCEEDED" | "FAILED";
failureCode?: BrowserDataFailure["code"];
}>,
) => void;
}>;
export interface IndexedDbOpfsJournal extends OpfsJournalPort {
close(): void;
}
type TransactionContext<Value> = IndexedDbTransactionContext<
Value,
BrowserDataFailure
>;
type StoredJournalRow = OpfsJournalTransaction &
Readonly<{ logicalKey: string }>;
type StoredObjectRow = Readonly<{
logicalKey: string;
scopeObjectKey: string;
scopeKey: string;
objectId: string;
preparedObject: OpfsPreparedObject;
}>;
type StoredBudgetRow = Readonly<{
budgetKey: string;
namespace: string;
authorityToken: string;
namespaceToken: string;
partitionToken: string;
hardBudgetBytes: number;
committedBytes: number;
reservedBytes: number;
}>;
type StoredScopeBinding = Readonly<{
scopeKey: string;
namespace: string;
authorityToken: string;
namespaceToken: string;
partitionToken: string;
policyFingerprint: string;
}>;
type StoredLogicalScopeBinding = Readonly<{
logicalScopeKey: string;
physicalScopeKey: string;
authorityToken: string;
namespace: string;
namespaceToken: string;
partitionToken: string;
}>;
type StoredChunkReference = Readonly<{
referenceKey: string;
scopeKey: string;
digestHex: string;
referenceCount: number;
}>;
const DATABASE_VERSION = 1;
const JOURNAL_STORE = "opfs-journal";
const OBJECT_STORE = "opfs-objects";
const BUDGET_STORE = "opfs-budgets";
const SCOPE_BINDING_STORE = "opfs-scope-bindings";
const LOGICAL_SCOPE_BINDING_STORE = "opfs-logical-scope-bindings";
const CHUNK_REFERENCE_STORE = "opfs-chunk-references";
const LOGICAL_KEY_INDEX = "by-logical-key";
const STARTED_AT_INDEX = "by-started-at";
const SCOPE_OBJECT_INDEX = "by-scope-object";
const SAFE_DATABASE_NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
const SAFE_BOUNDARY_ID = /^[A-Za-z0-9_-]{8,128}$/u;
const SHA256_HEX = /^[a-f0-9]{64}$/u;
export function opfsJournalDatabaseName(
authorityToken: string,
): string {
if (!SAFE_BOUNDARY_ID.test(authorityToken)) {
throw new TypeError("OPFS authority token is invalid.");
}
return `ca-frontend-opfs-metadata-v1:${authorityToken}`;
}
export function createIndexedDbOpfsJournal(
dependencies: IndexedDbOpfsJournalDependencies,
): IndexedDbOpfsJournal {
const expectedDatabaseName = opfsJournalDatabaseName(
dependencies.authorityToken,
);
const databaseName = dependencies.databaseName ?? expectedDatabaseName;
const factory =
dependencies.factory ??
(typeof globalThis.indexedDB === "undefined"
? undefined
: globalThis.indexedDB);
const keyRange =
dependencies.keyRange ??
(typeof globalThis.IDBKeyRange === "undefined"
? undefined
: globalThis.IDBKeyRange);
const createFencingToken =
dependencies.createFencingToken ??
(() => globalThis.crypto.randomUUID());
const blockedTimeoutMs = dependencies.blockedTimeoutMs ?? 10_000;
if (
!SAFE_BOUNDARY_ID.test(dependencies.authorityToken) ||
!SAFE_DATABASE_NAME.test(databaseName) ||
databaseName !== expectedDatabaseName ||
!Number.isSafeInteger(blockedTimeoutMs) ||
blockedTimeoutMs < 0
) {
throw new TypeError("IndexedDB OPFS journal configuration is invalid.");
}
// X-AUDIT-02. The timer callables are captured once, bound to their
// receiver, so replacing a global after composition cannot change how an
// open already in flight is bounded.
const timers = snapshotAbortTimers(
dependencies.scheduler ?? {
setTimeout: (callback: () => void, milliseconds: number): unknown =>
globalThis.setTimeout(callback, milliseconds),
clearTimeout: (handle: unknown): void => {
globalThis.clearTimeout(
handle as ReturnType<typeof globalThis.setTimeout>,
);
},
},
);
/**
* The handle owns the cached connection, the single-flight open and the
* `versionchange`/`close` invalidation this file used to wire by hand. No
* `onVersionChange` callback is passed because dropping the cached
* connection so the next call reopens — which the kernel already does — was
* this journal's entire listener body.
*/
const connection = createIndexedDbConnection<BrowserDataFailure>({
translate: translateFor("INDEXEDDB_OPEN"),
open: (signal) =>
factory === undefined
? Promise.resolve(
browserDataFailure("UNSUPPORTED", "INDEXEDDB_OPEN", {
recovery: "ONLINE_ONLY",
}),
)
: openIndexedDbDatabase<BrowserDataFailure>({
factory,
databaseName,
version: DATABASE_VERSION,
translate: translateFor("INDEXEDDB_OPEN"),
signal,
blockedTimeoutMs,
timers,
upgrade: ({ database, oldVersion }) => {
// Version 1 is the only schema this journal has ever had, so any
// other starting point belongs to a database it does not own.
// Rejecting aborts the versionchange transaction, which is what
// the hand-written `onupgradeneeded` did.
if (oldVersion !== 0) return { kind: "REJECTED" };
const journalStore = database.createObjectStore(JOURNAL_STORE, {
keyPath: "transactionId",
});
journalStore.createIndex(
LOGICAL_KEY_INDEX,
"logicalKey",
{ unique: true },
);
journalStore.createIndex(
STARTED_AT_INDEX,
"startedAtEpochMs",
{ unique: false },
);
const objectStore = database.createObjectStore(OBJECT_STORE, {
keyPath: "logicalKey",
});
objectStore.createIndex(
SCOPE_OBJECT_INDEX,
"scopeObjectKey",
{ unique: true },
);
database.createObjectStore(BUDGET_STORE, {
keyPath: "budgetKey",
});
database.createObjectStore(SCOPE_BINDING_STORE, {
keyPath: "scopeKey",
});
database.createObjectStore(LOGICAL_SCOPE_BINDING_STORE, {
keyPath: "logicalScopeKey",
});
database.createObjectStore(CHUNK_REFERENCE_STORE, {
keyPath: "referenceKey",
});
return { kind: "APPLIED" };
},
}),
});
const journal: IndexedDbOpfsJournal = {
async getCommittedObject(scope, objectId) {
if (
!isBoundScope(scope) ||
!validScopedObject(scope, objectId)
) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_READ");
}
return await withDatabase("INDEXEDDB_READ", (db) =>
runTransaction(
db,
[OBJECT_STORE],
"readonly",
"INDEXEDDB_READ",
(transaction, context) => {
onIndexedDbRequest<unknown, BrowserDataFailure>(
transaction
.objectStore(OBJECT_STORE)
.get(logicalObjectKey(scope, objectId)),
context,
(stored) => {
if (stored === undefined) {
context.succeed(null);
return;
}
if (
!isStoredObjectRow(stored) ||
!sameScope(stored.preparedObject.descriptor.scope, scope)
) {
context.fail(corrupt("INDEXEDDB_READ"));
return;
}
context.succeed(stored.preparedObject);
},
);
},
),
);
},
async begin(input) {
if (
!isBoundScope(input.scope) ||
!isBeginTransaction(input)
) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_WRITE");
}
const fencingToken = createFencingToken();
if (!SAFE_BOUNDARY_ID.test(fencingToken)) {
return browserDataFailure("UNAVAILABLE", "INDEXEDDB_WRITE", {
recovery: "REOPEN",
});
}
return await withDatabase("INDEXEDDB_WRITE", (db) =>
runTransaction(
db,
[
JOURNAL_STORE,
OBJECT_STORE,
BUDGET_STORE,
SCOPE_BINDING_STORE,
LOGICAL_SCOPE_BINDING_STORE,
],
"readwrite",
"INDEXEDDB_WRITE",
(nativeTransaction, context) => {
const bindingStore =
nativeTransaction.objectStore(SCOPE_BINDING_STORE);
const scopeKey = storageScopeKey(input.scope);
onIndexedDbRequest<unknown, BrowserDataFailure>(
bindingStore.get(scopeKey),
context,
(storedBinding) => {
if (
storedBinding !== undefined &&
!isScopeBinding(storedBinding)
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
const expectedBinding = scopeBinding(
input.scope,
input.targetStoragePolicy,
);
const logicalBindingStore = nativeTransaction.objectStore(
LOGICAL_SCOPE_BINDING_STORE,
);
const expectedLogicalBinding = logicalScopeBinding(
input.scope,
);
onIndexedDbRequest<unknown, BrowserDataFailure>(
logicalBindingStore.get(
expectedLogicalBinding.logicalScopeKey,
),
context,
(storedLogicalBinding) => {
if (
storedLogicalBinding !== undefined &&
!isLogicalScopeBinding(storedLogicalBinding)
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
if (
(storedBinding &&
stableJson(storedBinding) !==
stableJson(expectedBinding)) ||
(storedLogicalBinding &&
stableJson(storedLogicalBinding) !==
stableJson(expectedLogicalBinding))
) {
context.fail(policyRejected("INDEXEDDB_WRITE"));
return;
}
if (
(storedBinding === undefined) !==
(storedLogicalBinding === undefined)
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
if (!storedBinding) {
watchWrite(bindingStore.add(expectedBinding), context);
watchWrite(
logicalBindingStore.add(expectedLogicalBinding),
context,
);
}
},
);
},
);
const logicalKey = logicalObjectKey(
input.scope,
input.objectId,
);
onIndexedDbRequest<unknown, BrowserDataFailure>(
nativeTransaction.objectStore(OBJECT_STORE).get(logicalKey),
context,
(storedCurrent) => {
if (
storedCurrent !== undefined &&
!isStoredObjectRow(storedCurrent)
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
const current: OpfsPreparedObject | undefined =
storedCurrent?.preparedObject;
if (
!generationMatches(
current,
input.expectedGeneration,
) ||
input.targetGeneration !==
(current?.descriptor.generation ?? 0) + 1
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
const currentBytes =
current?.descriptor.byteLength ?? 0;
const reservedBytes =
input.mutation === "PUT"
? Math.max(0, input.targetByteLength - currentBytes)
: 0;
const budgetKey = storageBudgetKey(input.scope);
onIndexedDbRequest<unknown, BrowserDataFailure>(
nativeTransaction.objectStore(BUDGET_STORE).get(budgetKey),
context,
(budgetResult) => {
if (
budgetResult !== undefined &&
!isBudgetRow(budgetResult)
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
if (
budgetResult &&
(budgetResult.hardBudgetBytes !==
input.targetStoragePolicy.hardBudgetBytes ||
budgetResult.namespace !== input.scope.namespace ||
budgetResult.authorityToken !==
input.scope.authorityToken ||
budgetResult.namespaceToken !==
input.scope.namespaceToken ||
budgetResult.partitionToken !==
input.scope.partitionToken)
) {
context.fail(policyRejected("INDEXEDDB_WRITE"));
return;
}
if (!budgetResult && current) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
const budget: StoredBudgetRow =
budgetResult ?? {
budgetKey,
namespace: input.scope.namespace,
authorityToken: input.scope.authorityToken,
namespaceToken: input.scope.namespaceToken,
partitionToken: input.scope.partitionToken,
hardBudgetBytes:
input.targetStoragePolicy.hardBudgetBytes,
committedBytes: 0,
reservedBytes: 0,
};
if (
budget.committedBytes +
budget.reservedBytes +
reservedBytes >
budget.hardBudgetBytes
) {
context.fail(limitExceeded("INDEXEDDB_WRITE"));
return;
}
const nextBudget: StoredBudgetRow = Object.freeze({
...budget,
reservedBytes:
budget.reservedBytes + reservedBytes,
});
watchWrite(
nativeTransaction.objectStore(BUDGET_STORE).put(nextBudget),
context,
);
const row: StoredJournalRow = Object.freeze({
...input,
logicalKey,
fencingToken,
phase: "PREPARING",
budgetReservation: Object.freeze({
namespace: input.scope.namespace,
reservedBytes,
hardBudgetBytes:
input.targetStoragePolicy.hardBudgetBytes,
}),
});
// OP-1. The `by-logical-key` index is unique, so a second open
// row for the same object is rejected here rather than by any
// predicate the journal evaluated: the ConstraintError belongs
// to this request, and it is now reported as such.
watchWrite(
nativeTransaction.objectStore(JOURNAL_STORE).add(row),
context,
);
context.succeed(row);
},
);
},
);
},
),
);
},
async markFilesReady(
transactionId,
fencingToken,
preparedObject,
) {
if (
!SAFE_BOUNDARY_ID.test(transactionId) ||
!SAFE_BOUNDARY_ID.test(fencingToken) ||
!isPreparedObject(preparedObject) ||
!isBoundScope(preparedObject.descriptor.scope)
) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_WRITE");
}
return await updateJournalRow(
transactionId,
fencingToken,
(row, context, store) => {
if (
row.mutation !== "PUT" ||
row.objectId !== preparedObject.descriptor.objectId ||
!sameScope(row.scope, preparedObject.descriptor.scope) ||
row.targetGeneration !==
preparedObject.descriptor.generation ||
row.targetByteLength !==
preparedObject.descriptor.byteLength ||
stableJson(row.targetStoragePolicy) !==
stableJson(preparedObject.descriptor.storagePolicy)
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
if (row.phase === "FILES_READY") {
if (
!row.preparedObject ||
stableJson(row.preparedObject) !==
stableJson(preparedObject)
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
context.succeed(row);
return;
}
if (row.phase !== "PREPARING") {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
const updated: StoredJournalRow = Object.freeze({
...row,
phase: "FILES_READY",
preparedObject,
});
watchWrite(store.put(updated), context);
context.succeed(updated);
},
);
},
async commitPut(transactionId, fencingToken) {
return await commitMutation(
transactionId,
fencingToken,
"PUT",
);
},
async commitDelete(transactionId, fencingToken) {
return await commitMutation(
transactionId,
fencingToken,
"DELETE",
);
},
async complete(transactionId, fencingToken) {
if (!validTransactionIdentity(transactionId, fencingToken)) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_WRITE");
}
return await withDatabase("INDEXEDDB_WRITE", (db) =>
runTransaction(
db,
[JOURNAL_STORE],
"readwrite",
"INDEXEDDB_WRITE",
(transaction, context) => {
const store = transaction.objectStore(JOURNAL_STORE);
onIndexedDbRequest<unknown, BrowserDataFailure>(
store.get(transactionId),
context,
(stored) => {
if (stored === undefined) {
context.succeed(undefined);
return;
}
if (
!isStoredJournalRow(stored) ||
!isBoundScope(stored.scope) ||
stored.fencingToken !== fencingToken ||
stored.phase !== "COMMITTED"
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
watchWrite(store.delete(transactionId), context);
context.succeed(undefined);
},
);
},
),
);
},
async rollback(transactionId, fencingToken) {
if (!validTransactionIdentity(transactionId, fencingToken)) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_WRITE");
}
return await withDatabase("INDEXEDDB_WRITE", (db) =>
runTransaction(
db,
[JOURNAL_STORE, BUDGET_STORE],
"readwrite",
"INDEXEDDB_WRITE",
(transaction, context) => {
const journalStore =
transaction.objectStore(JOURNAL_STORE);
onIndexedDbRequest<unknown, BrowserDataFailure>(
journalStore.get(transactionId),
context,
(row) => {
if (row === undefined) {
context.succeed(undefined);
return;
}
if (
!isStoredJournalRow(row) ||
!isBoundScope(row.scope) ||
row.fencingToken !== fencingToken ||
row.phase === "COMMITTED"
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
const budgetStore =
transaction.objectStore(BUDGET_STORE);
onIndexedDbRequest<unknown, BrowserDataFailure>(
budgetStore.get(storageBudgetKey(row.scope)),
context,
(budget) => {
if (!isBudgetRow(budget)) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
if (
budget.reservedBytes <
row.budgetReservation.reservedBytes
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
putOrDeleteBudget(
budgetStore,
Object.freeze({
...budget,
reservedBytes:
budget.reservedBytes -
row.budgetReservation.reservedBytes,
}),
context,
);
watchWrite(journalStore.delete(transactionId), context);
context.succeed(undefined);
},
);
},
);
},
),
);
},
async listIncomplete(limit) {
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > 1_000
) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_READ");
}
return await withDatabase("INDEXEDDB_READ", (db) =>
runTransaction(
db,
[JOURNAL_STORE],
"readonly",
"INDEXEDDB_READ",
(transaction, context) => {
const rows: OpfsJournalTransaction[] = [];
walkIndexedDbCursor<BrowserDataFailure>({
request: transaction
.objectStore(JOURNAL_STORE)
.index(STARTED_AT_INDEX)
.openCursor(),
sink: context,
translate: translateFor("INDEXEDDB_READ"),
// OP-4. Neither a signal nor a budget is passed: the journal
// offers no cancellation and bounds the page on `limit` alone.
visit: ({ cursor }) => {
// The row is validated before the page limit is checked, so a
// corrupt row one past the page still fails the read rather
// than being hidden behind `moreAvailable`.
if (
!isStoredJournalRow(cursor.value) ||
!isBoundScope(cursor.value.scope)
) {
context.fail(corrupt("INDEXEDDB_READ"));
// `fail` aborts, so the pump has nowhere to go. Suspending
// without ever resuming says that without claiming a summary
// the scan never reached.
return { kind: "SUSPEND" };
}
if (rows.length === limit) return { kind: "STOP" };
rows.push(cursor.value);
return { kind: "CONTINUE" };
},
done: (summary) => {
context.succeed(
Object.freeze({
transactions: Object.freeze(rows),
moreAvailable:
summary.reason === "STOPPED" && rows.length === limit,
}),
);
},
});
},
),
);
},
async listCommittedObjects(request) {
if (
!keyRange ||
!isBoundScope(request.scope) ||
!isValidOpfsStorageScope(request.scope) ||
!Number.isSafeInteger(request.limit) ||
request.limit < 1 ||
request.limit > 1_000 ||
(request.afterObjectId !== undefined &&
!SAFE_BOUNDARY_ID.test(request.afterObjectId))
) {
return browserDataFailure(
keyRange ? "INVALID_INPUT" : "UNSUPPORTED",
"INDEXEDDB_READ",
);
}
return await withDatabase("INDEXEDDB_READ", (db) =>
runTransaction(
db,
[OBJECT_STORE],
"readonly",
"INDEXEDDB_READ",
(transaction, context) => {
const prefix = `${storageScopeKey(request.scope)}|`;
const lower = request.afterObjectId
? `${prefix}${request.afterObjectId}`
: prefix;
const range = keyRange.bound(
lower,
`${prefix}\uffff`,
request.afterObjectId !== undefined,
false,
);
const objects: OpfsPreparedObject[] = [];
walkIndexedDbCursor<BrowserDataFailure>({
request: transaction
.objectStore(OBJECT_STORE)
.index(SCOPE_OBJECT_INDEX)
.openCursor(range),
sink: context,
translate: translateFor("INDEXEDDB_READ"),
visit: ({ cursor }) => {
if (
!isStoredObjectRow(cursor.value) ||
!sameScope(
cursor.value.preparedObject.descriptor.scope,
request.scope,
)
) {
context.fail(corrupt("INDEXEDDB_READ"));
return { kind: "SUSPEND" };
}
if (objects.length === request.limit) {
return { kind: "STOP" };
}
objects.push(cursor.value.preparedObject);
return { kind: "CONTINUE" };
},
done: (summary) => {
// A page cut short by the limit hands back the cursor the
// caller resumes from; an exhausted scan has nothing to resume.
const more =
summary.reason === "STOPPED" &&
objects.length === request.limit;
context.succeed(
Object.freeze({
objects: Object.freeze(objects),
nextObjectId: more
? objects.at(-1)?.descriptor.objectId ?? null
: null,
moreAvailable: more,
}),
);
},
});
},
),
);
},
async isChunkReferenced(scope, digestHex) {
if (
!isBoundScope(scope) ||
!isValidOpfsStorageScope(scope) ||
!SHA256_HEX.test(digestHex)
) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_READ");
}
const referenceKey = chunkReferenceKey(scope, digestHex);
return await withDatabase("INDEXEDDB_READ", (db) =>
runTransaction(
db,
[CHUNK_REFERENCE_STORE],
"readonly",
"INDEXEDDB_READ",
(transaction, context) => {
onIndexedDbRequest<unknown, BrowserDataFailure>(
transaction
.objectStore(CHUNK_REFERENCE_STORE)
.get(referenceKey),
context,
(stored) => {
if (stored === undefined) {
context.succeed(false);
return;
}
if (
!isChunkReference(stored) ||
stored.referenceKey !== referenceKey ||
stored.scopeKey !== storageScopeKey(scope)
) {
context.fail(corrupt("INDEXEDDB_READ"));
return;
}
context.succeed(stored.referenceCount > 0);
},
);
},
),
);
},
close() {
connection.close();
},
};
return Object.freeze(journal);
async function commitMutation(
transactionId: string,
fencingToken: string,
mutation: "PUT" | "DELETE",
): Promise<BrowserDataResult<OpfsJournalTransaction>> {
if (!validTransactionIdentity(transactionId, fencingToken)) {
return browserDataFailure("INVALID_INPUT", "INDEXEDDB_WRITE");
}
return await withDatabase("INDEXEDDB_WRITE", (db) =>
runTransaction(
db,
[
JOURNAL_STORE,
OBJECT_STORE,
BUDGET_STORE,
CHUNK_REFERENCE_STORE,
],
"readwrite",
"INDEXEDDB_WRITE",
(nativeTransaction, context) => {
const journalStore =
nativeTransaction.objectStore(JOURNAL_STORE);
onIndexedDbRequest<unknown, BrowserDataFailure>(
journalStore.get(transactionId),
context,
(row) => {
if (
!isStoredJournalRow(row) ||
!isBoundScope(row.scope) ||
row.fencingToken !== fencingToken ||
row.mutation !== mutation
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
if (row.phase === "COMMITTED") {
context.succeed(row);
return;
}
if (
mutation === "PUT" &&
(row.phase !== "FILES_READY" || !row.preparedObject)
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
if (mutation === "DELETE" && row.phase !== "PREPARING") {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
const objectStore =
nativeTransaction.objectStore(OBJECT_STORE);
onIndexedDbRequest<unknown, BrowserDataFailure>(
objectStore.get(row.logicalKey),
context,
(storedCurrent) => {
if (
storedCurrent !== undefined &&
!isStoredObjectRow(storedCurrent)
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
const current: OpfsPreparedObject | undefined =
storedCurrent?.preparedObject;
if (
!generationMatches(current, row.expectedGeneration)
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
const budgetStore =
nativeTransaction.objectStore(BUDGET_STORE);
onIndexedDbRequest<unknown, BrowserDataFailure>(
budgetStore.get(storageBudgetKey(row.scope)),
context,
(budget) => {
if (!isBudgetRow(budget)) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
const currentBytes =
current?.descriptor.byteLength ?? 0;
const nextBytes =
mutation === "PUT" ? row.targetByteLength : 0;
const nextCommitted =
budget.committedBytes - currentBytes + nextBytes;
const nextReserved =
budget.reservedBytes -
row.budgetReservation.reservedBytes;
if (
nextCommitted < 0 ||
nextReserved < 0 ||
nextCommitted + nextReserved >
budget.hardBudgetBytes
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
const referenceDeltas = chunkReferenceDeltas(
current,
mutation === "PUT" ? row.preparedObject : undefined,
);
applyChunkReferenceDeltas(
nativeTransaction.objectStore(
CHUNK_REFERENCE_STORE,
),
row.scope,
referenceDeltas,
context,
() => {
putOrDeleteBudget(
budgetStore,
Object.freeze({
...budget,
committedBytes: nextCommitted,
reservedBytes: nextReserved,
}),
context,
);
if (mutation === "PUT") {
watchWrite(
objectStore.put(
storedObjectRow(row.preparedObject!),
),
context,
);
} else {
watchWrite(
objectStore.delete(row.logicalKey),
context,
);
}
const committed: StoredJournalRow = Object.freeze({
...row,
phase: "COMMITTED",
});
watchWrite(journalStore.put(committed), context);
context.succeed(committed);
},
);
},
);
},
);
},
);
},
),
);
}
async function updateJournalRow(
transactionId: string,
fencingToken: string,
update: (
row: StoredJournalRow,
context: TransactionContext<OpfsJournalTransaction>,
store: IDBObjectStore,
) => void,
): Promise<BrowserDataResult<OpfsJournalTransaction>> {
return await withDatabase("INDEXEDDB_WRITE", (db) =>
runTransaction(
db,
[JOURNAL_STORE],
"readwrite",
"INDEXEDDB_WRITE",
(transaction, context) => {
const store = transaction.objectStore(JOURNAL_STORE);
onIndexedDbRequest<unknown, BrowserDataFailure>(
store.get(transactionId),
context,
(stored) => {
if (
!isStoredJournalRow(stored) ||
!isBoundScope(stored.scope) ||
stored.fencingToken !== fencingToken
) {
context.fail(conflict("INDEXEDDB_WRITE"));
return;
}
update(stored, context, store);
},
);
},
),
);
}
async function withDatabase<Value>(
operation: BrowserDataOperation,
task: (db: IDBDatabase) => Promise<BrowserDataResult<Value>>,
): Promise<BrowserDataResult<Value>> {
const opened = await connection.acquire();
if (!opened.ok) return opened;
try {
const result = await task(opened.value);
observe(result, operation);
return result;
} catch (error) {
const result = mapIndexedDbException(error, operation);
observe(result, operation);
return result;
}
}
function observe(
result: BrowserDataResult<unknown>,
operation: BrowserDataOperation,
): void {
try {
dependencies.observe?.(
result.ok
? { operation, outcome: "SUCCEEDED" }
: {
operation,
outcome: "FAILED",
failureCode: result.error.code,
},
);
} catch {
// Journal behavior never depends on telemetry.
}
}
function isBoundScope(scope: OpfsStorageScope): boolean {
return scope.authorityToken === dependencies.authorityToken;
}
}
/**
* Every readwrite transaction is strict because the journal is the durable
* record of a two-phase OPFS mutation: a write that is only queued when the
* tab goes away would leave files on disk that no journal row claims. Reads
* pass `undefined`, which opens with no options bag at all rather than with
* `{durability:"default"}` — the form this file has always used.
*/
function runTransaction<Value>(
database: IDBDatabase,
storeNames: readonly string[],
mode: IDBTransactionMode,
operation: BrowserDataOperation,
run: (
transaction: IDBTransaction,
context: TransactionContext<Value>,
) => void,
): Promise<BrowserDataResult<Value>> {
if (mode === "versionchange") {
// The journal only ever opens readonly or readwrite transactions; a
// versionchange transaction belongs to the open request, not here.
return Promise.resolve(
browserDataFailure("INVALID_INPUT", operation),
);
}
return runIndexedDbTransaction<Value, BrowserDataFailure>({
database,
stores: storeNames,
mode,
translate: translateFor(operation),
durability: mode === "readwrite" ? "strict" : undefined,
queue: run,
});
}
function applyChunkReferenceDeltas(
store: IDBObjectStore,
scope: OpfsStorageScope,
deltas: ReadonlyMap<string, number>,
context: TransactionContext<OpfsJournalTransaction>,
completed: () => void,
): void {
const entries = [...deltas].filter(([, delta]) => delta !== 0);
if (entries.length === 0) {
completed();
return;
}
let remaining = entries.length;
for (const [digestHex, delta] of entries) {
const referenceKey = chunkReferenceKey(scope, digestHex);
onIndexedDbRequest<unknown, BrowserDataFailure>(
store.get(referenceKey),
context,
(stored) => {
if (
stored !== undefined &&
(!isChunkReference(stored) ||
stored.referenceKey !== referenceKey ||
stored.scopeKey !== storageScopeKey(scope))
) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
const current =
(stored as StoredChunkReference | undefined)?.referenceCount ?? 0;
const next = current + delta;
if (!Number.isSafeInteger(next) || next < 0) {
context.fail(corrupt("INDEXEDDB_WRITE"));
return;
}
if (next === 0) {
watchWrite(store.delete(referenceKey), context);
} else {
watchWrite(
store.put(
Object.freeze({
referenceKey,
scopeKey: storageScopeKey(scope),
digestHex,
referenceCount: next,
}),
),
context,
);
}
remaining -= 1;
if (remaining === 0) completed();
},
);
}
}
function chunkReferenceDeltas(
previous: OpfsPreparedObject | undefined,
next: OpfsPreparedObject | undefined,
): ReadonlyMap<string, number> {
const deltas = new Map<string, number>();
for (const chunk of previous?.chunks ?? []) {
deltas.set(chunk.digestHex, (deltas.get(chunk.digestHex) ?? 0) - 1);
}
for (const chunk of next?.chunks ?? []) {
deltas.set(chunk.digestHex, (deltas.get(chunk.digestHex) ?? 0) + 1);
}
return deltas;
}
function putOrDeleteBudget(
store: IDBObjectStore,
budget: StoredBudgetRow,
sink: IndexedDbRequestSink<BrowserDataFailure>,
): void {
// Separate calls rather than one over a ternary: `IDBRequest` is invariant
// in its result through the `this` type of its handlers, so the two request
// types have no useful union.
if (budget.committedBytes === 0 && budget.reservedBytes === 0) {
watchWrite(store.delete(budget.budgetKey), sink);
return;
}
watchWrite(store.put(budget), sink);
}
function storedObjectRow(
preparedObject: OpfsPreparedObject,
): StoredObjectRow {
const { scope, objectId } = preparedObject.descriptor;
return Object.freeze({
logicalKey: logicalObjectKey(scope, objectId),
scopeObjectKey: `${storageScopeKey(scope)}|${objectId}`,
scopeKey: storageScopeKey(scope),
objectId,
preparedObject,
});
}
function generationMatches(
current: OpfsPreparedObject | undefined,
expectedGeneration: number | null,
): boolean {
return expectedGeneration === null
? current === undefined
: current?.descriptor.generation === expectedGeneration;
}
function logicalObjectKey(
scope: OpfsStorageScope,
objectId: string,
): string {
return `${storageScopeKey(scope)}|${objectId}`;
}
function storageScopeKey(scope: OpfsStorageScope): string {
return [
scope.authorityToken,
scope.namespaceToken,
scope.partitionToken,
].join("|");
}
function storageBudgetKey(scope: OpfsStorageScope): string {
return storageScopeKey(scope);
}
function scopeBinding(
scope: OpfsStorageScope,
storagePolicy: BrowserStoragePolicy,
): StoredScopeBinding {
return Object.freeze({
scopeKey: storageScopeKey(scope),
namespace: scope.namespace,
authorityToken: scope.authorityToken,
namespaceToken: scope.namespaceToken,
partitionToken: scope.partitionToken,
policyFingerprint: stableJson(storagePolicy),
});
}
function logicalScopeBinding(
scope: OpfsStorageScope,
): StoredLogicalScopeBinding {
return Object.freeze({
logicalScopeKey: [
scope.authorityToken,
scope.namespace,
scope.partitionToken,
].join("|"),
physicalScopeKey: storageScopeKey(scope),
authorityToken: scope.authorityToken,
namespace: scope.namespace,
namespaceToken: scope.namespaceToken,
partitionToken: scope.partitionToken,
});
}
function chunkReferenceKey(
scope: OpfsStorageScope,
digestHex: string,
): string {
return `${storageScopeKey(scope)}|${digestHex}`;
}
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 validScopedObject(
scope: OpfsStorageScope,
objectId: string,
): boolean {
return (
isValidOpfsStorageScope(scope) &&
SAFE_BOUNDARY_ID.test(objectId)
);
}
function validTransactionIdentity(
transactionId: string,
fencingToken: string,
): boolean {
return (
SAFE_BOUNDARY_ID.test(transactionId) &&
SAFE_BOUNDARY_ID.test(fencingToken)
);
}
function isBeginTransaction(
value: BeginOpfsJournalTransaction,
): boolean {
try {
assertValidStoragePolicy(value.targetStoragePolicy);
} catch {
return false;
}
return Boolean(
validScopedObject(value.scope, value.objectId) &&
SAFE_BOUNDARY_ID.test(value.transactionId) &&
value.targetStoragePolicy.namespace === value.scope.namespace &&
(value.mutation === "PUT" || value.mutation === "DELETE") &&
(value.expectedGeneration === null ||
(Number.isSafeInteger(value.expectedGeneration) &&
value.expectedGeneration > 0)) &&
Number.isSafeInteger(value.targetGeneration) &&
value.targetGeneration > 0 &&
Number.isSafeInteger(value.targetByteLength) &&
value.targetByteLength >= 0 &&
(value.mutation === "PUT" || value.targetByteLength === 0) &&
Number.isSafeInteger(value.startedAtEpochMs) &&
value.startedAtEpochMs >= 0,
);
}
function isStoredJournalRow(
value: unknown,
): value is StoredJournalRow {
if (
!value ||
typeof value !== "object" ||
!("logicalKey" in value) ||
typeof value.logicalKey !== "string" ||
!("transactionId" in value) ||
typeof value.transactionId !== "string" ||
!SAFE_BOUNDARY_ID.test(value.transactionId) ||
!("fencingToken" in value) ||
typeof value.fencingToken !== "string" ||
!SAFE_BOUNDARY_ID.test(value.fencingToken) ||
!("scope" in value) ||
!value.scope ||
typeof value.scope !== "object" ||
!isValidOpfsStorageScope(value.scope as OpfsStorageScope) ||
!("mutation" in value) ||
(value.mutation !== "PUT" && value.mutation !== "DELETE") ||
!("phase" in value) ||
!["PREPARING", "FILES_READY", "COMMITTED"].includes(
String(value.phase),
) ||
!("objectId" in value) ||
typeof value.objectId !== "string" ||
!SAFE_BOUNDARY_ID.test(value.objectId) ||
value.logicalKey !==
logicalObjectKey(value.scope as OpfsStorageScope, value.objectId) ||
!("expectedGeneration" in value) ||
(value.expectedGeneration !== null &&
(typeof value.expectedGeneration !== "number" ||
!Number.isSafeInteger(value.expectedGeneration) ||
value.expectedGeneration < 1)) ||
!("targetGeneration" in value) ||
typeof value.targetGeneration !== "number" ||
!Number.isSafeInteger(value.targetGeneration) ||
value.targetGeneration < 1 ||
!("targetByteLength" in value) ||
typeof value.targetByteLength !== "number" ||
!Number.isSafeInteger(value.targetByteLength) ||
value.targetByteLength < 0 ||
!("targetStoragePolicy" in value) ||
!value.targetStoragePolicy ||
typeof value.targetStoragePolicy !== "object" ||
!("budgetReservation" in value) ||
!isBudgetReservation(value.budgetReservation) ||
!("startedAtEpochMs" in value) ||
typeof value.startedAtEpochMs !== "number" ||
!Number.isSafeInteger(value.startedAtEpochMs) ||
value.startedAtEpochMs < 0
) {
return false;
}
try {
assertValidStoragePolicy(
value.targetStoragePolicy as OpfsJournalTransaction["targetStoragePolicy"],
);
} catch {
return false;
}
const scope = value.scope as OpfsStorageScope;
const storagePolicy =
value.targetStoragePolicy as OpfsJournalTransaction["targetStoragePolicy"];
const budgetReservation =
value.budgetReservation as OpfsJournalTransaction["budgetReservation"];
if (
storagePolicy.namespace !== scope.namespace ||
budgetReservation.namespace !== scope.namespace ||
budgetReservation.hardBudgetBytes !==
storagePolicy.hardBudgetBytes
) {
return false;
}
if ("preparedObject" in value && value.preparedObject !== undefined) {
return (
value.mutation === "PUT" &&
value.phase !== "PREPARING" &&
isPreparedObject(value.preparedObject) &&
value.preparedObject.descriptor.objectId === value.objectId &&
sameScope(
value.preparedObject.descriptor.scope,
value.scope as OpfsStorageScope,
)
);
}
return (
value.phase === "PREPARING" ||
(value.phase === "COMMITTED" && value.mutation === "DELETE")
);
}
function isBudgetReservation(value: unknown): boolean {
return Boolean(
value &&
typeof value === "object" &&
"namespace" in value &&
typeof value.namespace === "string" &&
"reservedBytes" in value &&
typeof value.reservedBytes === "number" &&
Number.isSafeInteger(value.reservedBytes) &&
value.reservedBytes >= 0 &&
"hardBudgetBytes" in value &&
typeof value.hardBudgetBytes === "number" &&
Number.isSafeInteger(value.hardBudgetBytes) &&
value.hardBudgetBytes >= 0,
);
}
function isStoredObjectRow(value: unknown): value is StoredObjectRow {
if (
!value ||
typeof value !== "object" ||
!("logicalKey" in value) ||
typeof value.logicalKey !== "string" ||
!("scopeObjectKey" in value) ||
typeof value.scopeObjectKey !== "string" ||
!("scopeKey" in value) ||
typeof value.scopeKey !== "string" ||
!("objectId" in value) ||
typeof value.objectId !== "string" ||
!("preparedObject" in value) ||
!isPreparedObject(value.preparedObject)
) {
return false;
}
const scope = value.preparedObject.descriptor.scope;
return (
value.objectId === value.preparedObject.descriptor.objectId &&
value.logicalKey === logicalObjectKey(scope, value.objectId) &&
value.scopeKey === storageScopeKey(scope) &&
value.scopeObjectKey === `${value.scopeKey}|${value.objectId}`
);
}
function isBudgetRow(value: unknown): value is StoredBudgetRow {
return Boolean(
value &&
typeof value === "object" &&
"budgetKey" in value &&
typeof value.budgetKey === "string" &&
"namespace" in value &&
typeof value.namespace === "string" &&
"authorityToken" in value &&
typeof value.authorityToken === "string" &&
SAFE_BOUNDARY_ID.test(value.authorityToken) &&
"namespaceToken" in value &&
typeof value.namespaceToken === "string" &&
SAFE_BOUNDARY_ID.test(value.namespaceToken) &&
"partitionToken" in value &&
typeof value.partitionToken === "string" &&
SAFE_BOUNDARY_ID.test(value.partitionToken) &&
value.budgetKey ===
`${value.authorityToken}|${value.namespaceToken}|${value.partitionToken}` &&
"hardBudgetBytes" in value &&
typeof value.hardBudgetBytes === "number" &&
Number.isSafeInteger(value.hardBudgetBytes) &&
value.hardBudgetBytes >= 0 &&
"committedBytes" in value &&
typeof value.committedBytes === "number" &&
Number.isSafeInteger(value.committedBytes) &&
value.committedBytes >= 0 &&
"reservedBytes" in value &&
typeof value.reservedBytes === "number" &&
Number.isSafeInteger(value.reservedBytes) &&
value.reservedBytes >= 0 &&
value.committedBytes + value.reservedBytes <=
value.hardBudgetBytes,
);
}
function isScopeBinding(value: unknown): value is StoredScopeBinding {
return Boolean(
value &&
typeof value === "object" &&
"scopeKey" in value &&
typeof value.scopeKey === "string" &&
"namespace" in value &&
typeof value.namespace === "string" &&
"authorityToken" in value &&
typeof value.authorityToken === "string" &&
SAFE_BOUNDARY_ID.test(value.authorityToken) &&
"namespaceToken" in value &&
typeof value.namespaceToken === "string" &&
SAFE_BOUNDARY_ID.test(value.namespaceToken) &&
"partitionToken" in value &&
typeof value.partitionToken === "string" &&
SAFE_BOUNDARY_ID.test(value.partitionToken) &&
value.scopeKey ===
`${value.authorityToken}|${value.namespaceToken}|${value.partitionToken}` &&
"policyFingerprint" in value &&
typeof value.policyFingerprint === "string" &&
value.policyFingerprint.length > 0 &&
value.policyFingerprint.length <= 4_096,
);
}
function isLogicalScopeBinding(
value: unknown,
): value is StoredLogicalScopeBinding {
return Boolean(
value &&
typeof value === "object" &&
"logicalScopeKey" in value &&
typeof value.logicalScopeKey === "string" &&
"physicalScopeKey" in value &&
typeof value.physicalScopeKey === "string" &&
"authorityToken" in value &&
typeof value.authorityToken === "string" &&
SAFE_BOUNDARY_ID.test(value.authorityToken) &&
"namespace" in value &&
typeof value.namespace === "string" &&
"namespaceToken" in value &&
typeof value.namespaceToken === "string" &&
SAFE_BOUNDARY_ID.test(value.namespaceToken) &&
"partitionToken" in value &&
typeof value.partitionToken === "string" &&
SAFE_BOUNDARY_ID.test(value.partitionToken) &&
value.logicalScopeKey ===
`${value.authorityToken}|${value.namespace}|${value.partitionToken}` &&
value.physicalScopeKey ===
`${value.authorityToken}|${value.namespaceToken}|${value.partitionToken}`,
);
}
function isChunkReference(
value: unknown,
): value is StoredChunkReference {
return Boolean(
value &&
typeof value === "object" &&
"referenceKey" in value &&
typeof value.referenceKey === "string" &&
"scopeKey" in value &&
typeof value.scopeKey === "string" &&
"digestHex" in value &&
typeof value.digestHex === "string" &&
SHA256_HEX.test(value.digestHex) &&
value.referenceKey ===
`${value.scopeKey}|${value.digestHex}` &&
"referenceCount" in value &&
typeof value.referenceCount === "number" &&
Number.isSafeInteger(value.referenceCount) &&
value.referenceCount > 0,
);
}
/**
* 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 isPreparedObject(value: unknown): 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" ||
!SAFE_BOUNDARY_ID.test(descriptor.objectId) ||
!descriptor.scope ||
typeof descriptor.scope !== "object" ||
!isValidOpfsStorageScope(descriptor.scope as OpfsStorageScope) ||
typeof descriptor.generation !== "number" ||
!Number.isSafeInteger(descriptor.generation) ||
descriptor.generation < 1 ||
typeof descriptor.byteLength !== "number" ||
!Number.isSafeInteger(descriptor.byteLength) ||
descriptor.byteLength < 0 ||
typeof descriptor.mediaType !== "string" ||
descriptor.mediaType.length < 1 ||
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) ||
typeof integrity.chunkSizeBytes !== "number" ||
!Number.isSafeInteger(integrity.chunkSizeBytes) ||
integrity.chunkSizeBytes < 1 ||
!descriptor.storagePolicy ||
typeof descriptor.storagePolicy !== "object"
) {
return false;
}
try {
assertValidStoragePolicy(
descriptor.storagePolicy as OpfsPreparedObject["descriptor"]["storagePolicy"],
);
} catch {
return false;
}
if (
(descriptor.scope as OpfsStorageScope).namespace !==
(
descriptor.storagePolicy as OpfsPreparedObject["descriptor"]["storagePolicy"]
).namespace
) {
return false;
}
let totalBytes = 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 > integrity.chunkSizeBytes ||
!("digestHex" in chunk) ||
typeof chunk.digestHex !== "string" ||
!SHA256_HEX.test(chunk.digestHex)
) {
return false;
}
if (
index < value.chunks.length - 1 &&
chunk.byteLength !== integrity.chunkSizeBytes
) {
return false;
}
totalBytes += chunk.byteLength;
}
return (
totalBytes === descriptor.byteLength &&
value.chunks.length ===
Math.ceil(descriptor.byteLength / integrity.chunkSizeBytes)
);
}
function stableJson(value: unknown): string {
if (
value === null ||
typeof value === "string" ||
typeof value === "boolean" ||
typeof value === "number"
) {
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 TypeError("Journal value is not JSON-safe.");
}
/**
* `browserDataFailure` and `mapIndexedDbException` build a `Result`, while the
* kernel's `translate` and `context.fail` want the failure on its own. Both
* only ever build the failure arm, so the branch below narrows rather than
* claims.
*/
function failureOf(result: BrowserDataResult<never>): BrowserDataFailure {
if (result.ok) {
throw new TypeError("A browser data failure was expected.");
}
return result.error;
}
/**
* A write nobody waits on still has to report its own failure. This file wired
* no request `onerror` at all before the kernel, so a unique-index
* `ConstraintError` reached the caller only as whatever `transaction.error`
* happened to hold once the store unwound — a provenance the journal never
* chose and cannot rely on.
*
* `requestFailed` records without aborting, which is deliberate: the store
* already aborts a transaction whose request error goes unhandled, and calling
* `fail` here would report the journal as the party that rejected a write the
* index rejected.
*/
function watchWrite<Value>(
request: IDBRequest<Value>,
sink: IndexedDbRequestSink<BrowserDataFailure>,
): void {
onIndexedDbRequest(request, sink, () => {
// A write's own success publishes nothing; the transaction's completion
// does.
});
}
function conflict(operation: BrowserDataOperation): BrowserDataFailure {
return failureOf(
browserDataFailure("CONFLICT", operation, { recovery: "REOPEN" }),
);
}
function corrupt(operation: BrowserDataOperation): BrowserDataFailure {
return failureOf(
browserDataFailure("CORRUPT_DATA", operation, { recovery: "READ_ONLY" }),
);
}
function policyRejected(
operation: BrowserDataOperation,
): BrowserDataFailure {
return failureOf(
browserDataFailure("POLICY_REJECTED", operation, {
recovery: "READ_ONLY",
}),
);
}
function limitExceeded(
operation: BrowserDataOperation,
): BrowserDataFailure {
return failureOf(
browserDataFailure("LIMIT_EXCEEDED", operation, {
recovery: "EXPORT_REQUIRED",
}),
);
}
/**
* STO-01. The journal keeps `mapIndexedDbException`, which the IndexedDB
* runtime and maintenance share, rather than the checkpoint store's own table:
* the same native error is a different answer in the two families and unifying
* them is a separate change from moving the mechanics onto the kernel.
*
* A translator per operation, because the journal labels a read and a write
* differently. The connection always translates as `INDEXEDDB_OPEN`: an open
* failure names the operation it belongs to, not the call that happened to ask
* for the connection, and that is why `withDatabase` returns it unobserved.
*/
function translateFor(
operation: BrowserDataOperation,
): IndexedDbTranslate<BrowserDataFailure> {
return (cause) => {
switch (cause.kind) {
case "NATIVE_EXCEPTION":
return failureOf(mapIndexedDbException(cause.error, operation));
case "BLOCKED":
case "BLOCKED_DEADLINE":
// The journal reports the same answer whether the blocked event is
// itself terminal or a deadline ran out: a `blockedTimeoutMs` of 0 is
// a valid configuration here, and it must not report a different code
// from the same standing conflict.
return failureOf(
browserDataFailure("BLOCKED", operation, {
retryable: true,
recovery: "RELOAD_OTHER_CONTEXTS",
}),
);
case "CLOSED":
case "UNSUPPORTED":
// A closed journal and a realm without IndexedDB are the same answer
// to the caller: this store cannot serve the request at all, so work
// online instead of retrying.
return failureOf(
browserDataFailure("UNSUPPORTED", operation, {
recovery: "ONLINE_ONLY",
}),
);
case "NO_VALUE_PRODUCED":
return failureOf(
browserDataFailure("UNAVAILABLE", operation, {
retryable: true,
recovery: "REOPEN",
}),
);
case "UPGRADE_REJECTED":
case "CALLER_ABORT":
// Rejecting an upgrade aborts the versionchange transaction, so the
// open request fails with an `AbortError` and the caller has always
// seen ABORTED. `CALLER_ABORT` shares the arm because the journal
// exposes no cancellation — only the connection handle's own close
// cancels an open, and its waiters are told CLOSED instead.
return failureOf(browserDataFailure("ABORTED", operation));
case "ADMISSION_REJECTED":
// Unreachable: no `admit` callback is installed. The journal validates
// stored rows per transaction rather than at open time.
return failureOf(
browserDataFailure("UNAVAILABLE", operation, {
retryable: true,
recovery: "REOPEN",
}),
);
default: {
const exhaustive: never = cause;
return exhaustive;
}
}
};
}