import { describe, expect, it } from "vitest"; import type { ResumableUploadCheckpoint } from "../../src/application/ports/browser-transfer/resumable-upload.ts"; import { RESUMABLE_UPLOAD_PROTOCOL } from "../../src/application/ports/browser-transfer/resumable-upload.ts"; import { createIndexedDbResumableUploadCheckpointRuntime, createIndexedDbResumableUploadCheckpointStore, uploadCheckpointDatabaseName, } from "../../src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts"; import { MemoryIndexedDbFactory } from "../helpers/memory-indexeddb.ts"; const scope = Object.freeze({ authorityToken: "authority_token_01", namespaceToken: "namespace_token_01", partitionToken: "partition_token_01", }); function checkpoint( revision: number, overrides: Partial = {}, ): ResumableUploadCheckpoint { return { schemaVersion: 1, protocol: RESUMABLE_UPLOAD_PROTOCOL, revision, state: "ACTIVE", uploadKey: "upload_key_01", requestBindingSha256: "a".repeat(64), fingerprint: { algorithm: "SHA-256-PARTS-V1", digestHex: "b".repeat(64), byteLength: 4, partSizeBytes: 4, partCount: 1, }, sessionId: "session_01", sessionExpiresAtEpochMs: 5_000, sessionMaxConcurrency: 1, acceptedParts: [], updatedAtEpochMs: 1_000, ...overrides, }; } function deletingFactory( memory: MemoryIndexedDbFactory, mode: "SUCCESS" | "BLOCKED", ): IDBFactory { return { open: memory.factory.open.bind(memory.factory), cmp: memory.factory.cmp.bind(memory.factory), deleteDatabase: () => { const request = { result: undefined, error: null, transaction: null, source: null, readyState: "pending", onsuccess: null, onerror: null, onblocked: null, onupgradeneeded: null, addEventListener() {}, removeEventListener() {}, dispatchEvent: () => true, } as unknown as IDBOpenDBRequest; queueMicrotask(() => { if (mode === "SUCCESS") { request.onsuccess?.(new Event("success")); } else { request.onblocked?.({ oldVersion: 1, newVersion: null, } as IDBVersionChangeEvent); } }); return request; }, databases: async () => [], } as IDBFactory; } /** * A database the test drives by hand. The memory fake commits or aborts on its * own, so the three places the checkpoint store deliberately disagrees with the * other IndexedDB adapters — a commit that produced no value, a request error * that must abort, and an `abort()` that throws — are only observable here. * * The scope-binding transaction is driven automatically because no test below * is about it; only checkpoint transactions are handed to the test. */ type ScriptedRequest = { result: unknown; error: DOMException | null; onsuccess: ((event: Event) => unknown) | null; onerror: ((event: Event) => unknown) | null; }; type ScriptedTransaction = { abortCalls: number; error: DOMException | null; oncomplete: ((event: Event) => unknown) | null; onerror: ((event: Event) => unknown) | null; onabort: ((event: Event) => unknown) | null; readonly requests: ScriptedRequest[]; objectStore(name: string): IDBObjectStore; abort(): void; complete(): void; }; function scriptedFactory( options: Readonly<{ abortThrows?: boolean }> = {}, ): Readonly<{ factory: IDBFactory; /** One entry per `database.transaction(...)` call, holding its argument count. */ transactionArguments: number[]; checkpoints: ScriptedTransaction[]; }> { const transactionArguments: number[] = []; const checkpoints: ScriptedTransaction[] = []; const scriptedRequest = (): ScriptedRequest => ({ result: undefined, error: null, onsuccess: null, onerror: null, }); const makeTransaction = (): ScriptedTransaction => { const requests: ScriptedRequest[] = []; const transaction: ScriptedTransaction = { abortCalls: 0, error: null, oncomplete: null, onerror: null, onabort: null, requests, objectStore: () => { const queue = () => { const request = scriptedRequest(); requests.push(request); return request as unknown as IDBRequest; }; return { get: queue, add: queue, put: queue, delete: queue, } as unknown as IDBObjectStore; }, abort() { transaction.abortCalls += 1; if (options.abortThrows === true) { throw new DOMException("Transaction is finished.", "InvalidStateError"); } transaction.error = new DOMException("Transaction aborted.", "AbortError"); transaction.onerror?.(new Event("error")); transaction.onabort?.(new Event("abort")); }, complete() { transaction.oncomplete?.(new Event("complete")); }, }; return transaction; }; const database = { close() {}, transaction: (...args: unknown[]) => { transactionArguments.push(args.length); const requested = args[0]; const names = typeof requested === "string" ? [requested] : [...(requested as Iterable)]; const transaction = makeTransaction(); if (names.includes("governance")) { queueMicrotask(() => { transaction.requests[0]?.onsuccess?.(new Event("success")); queueMicrotask(() => { transaction.requests[1]?.onsuccess?.(new Event("success")); queueMicrotask(() => { transaction.complete(); }); }); }); } else { checkpoints.push(transaction); } return transaction as unknown as IDBTransaction; }, } as unknown as IDBDatabase; const factory = { open: () => { const request = { result: database, error: null, transaction: null, onsuccess: null, onerror: null, onblocked: null, onupgradeneeded: null, } as unknown as IDBOpenDBRequest; queueMicrotask(() => request.onsuccess?.(new Event("success"))); return request; }, cmp: () => 0, databases: async () => [], } as unknown as IDBFactory; return { factory, transactionArguments, checkpoints }; } async function flushTasks(): Promise { for (let tick = 0; tick < 5; tick += 1) { await new Promise((resolve) => { setTimeout(resolve, 0); }); } } async function openedCheckpointTransaction( scripted: ReturnType, ): Promise { await flushTasks(); const transaction = scripted.checkpoints[0]; if (!transaction) throw new Error("no checkpoint transaction was opened"); return transaction; } describe("IndexedDB resumable upload checkpoint", () => { it("length-prefixes scope tuples so delimiter placement cannot collide", () => { const first = uploadCheckpointDatabaseName({ authorityToken: "aaaaaaaa-bbbbbbbb", namespaceToken: "cccccccc", partitionToken: "dddddddd", }); const second = uploadCheckpointDatabaseName({ authorityToken: "aaaaaaaa", namespaceToken: "bbbbbbbb-cccccccc", partitionToken: "dddddddd", }); expect(first).not.toBe(second); expect(first).toContain("17:aaaaaaaa-bbbbbbbb"); expect(second).toContain("8:aaaaaaaa"); }); it("commits CAS only at transaction completion and rejects stale revisions", async () => { const memory = new MemoryIndexedDbFactory(); const store = createIndexedDbResumableUploadCheckpointStore({ scope, factory: memory.factory, }); const first = checkpoint(1); expect( await store.compareAndSwap({ expectedRevision: null, checkpoint: first, }), ).toEqual({ ok: true, value: first }); expect(await store.read(first.uploadKey)).toEqual({ ok: true, value: first, }); memory.failNextWriteCommit( new DOMException("commit failed", "UnknownError"), ); const failed = await store.compareAndSwap({ expectedRevision: 1, checkpoint: checkpoint(2, { state: "ABORT_PENDING" }), }); expect(failed).toMatchObject({ ok: false, error: { code: "UNAVAILABLE" }, }); expect(await store.read(first.uploadKey)).toEqual({ ok: true, value: first, }); expect( await store.compareAndSwap({ expectedRevision: 2, checkpoint: checkpoint(3), }), ).toMatchObject({ ok: false, error: { code: "CONFLICT", recovery: "RECONCILE" }, }); }); it("rejects unknown persisted fields so URLs and credentials cannot enter a checkpoint", async () => { const memory = new MemoryIndexedDbFactory(); const store = createIndexedDbResumableUploadCheckpointStore({ scope, factory: memory.factory, }); const smuggled = { ...checkpoint(1), signedUrl: "https://object.invalid/secret?signature=value", } as unknown as ResumableUploadCheckpoint; expect( await store.compareAndSwap({ expectedRevision: null, checkpoint: smuggled, }), ).toMatchObject({ ok: false, error: { code: "INVALID_INPUT" }, }); expect(await store.read("upload_key_01")).toEqual({ ok: true, value: null, }); }); it("closes the bound partition before successful lifecycle deletion", async () => { const memory = new MemoryIndexedDbFactory(); const runtime = createIndexedDbResumableUploadCheckpointRuntime({ scope, factory: deletingFactory(memory, "SUCCESS"), blockedTimeoutMs: 10, }); expect( await runtime.store.compareAndSwap({ expectedRevision: null, checkpoint: checkpoint(1), }), ).toMatchObject({ ok: true }); expect(await runtime.admin.deletePartition()).toEqual({ ok: true, value: { state: "DELETED", effect: "APPLIED" }, }); expect(await runtime.store.read("upload_key_01")).toMatchObject({ ok: false, error: { code: "UNAVAILABLE", recovery: "RESUME" }, }); }); it("returns PENDING UNKNOWN when deleteDatabase is still blocked", async () => { const memory = new MemoryIndexedDbFactory(); const factory = deletingFactory(memory, "BLOCKED"); const runtime = createIndexedDbResumableUploadCheckpointRuntime({ scope, factory, blockedTimeoutMs: 1, }); // BT-UP-03. The native request is still live, so the deadline is not // evidence that nothing happened. expect(await runtime.admin.deletePartition()).toEqual({ ok: true, value: { state: "PENDING", effect: "UNKNOWN", reason: "BLOCKED_DEADLINE", }, }); }); it("keeps the checkpoint store closed until a pending delete is resolved externally", async () => { const memory = new MemoryIndexedDbFactory(); const factory = deletingFactory(memory, "BLOCKED"); const runtime = createIndexedDbResumableUploadCheckpointRuntime({ scope, factory, blockedTimeoutMs: 1, }); expect(await runtime.admin.deletePartition()).toMatchObject({ ok: true, value: { state: "PENDING" }, }); // A second runtime over the same realm and database would race an unknown // native effect. expect(() => createIndexedDbResumableUploadCheckpointRuntime({ scope, factory, blockedTimeoutMs: 1, }), ).toThrow(TypeError); }); it("never reports a false abort after irreversible deleteDatabase dispatch", async () => { const memory = new MemoryIndexedDbFactory(); const runtime = createIndexedDbResumableUploadCheckpointRuntime({ scope, factory: deletingFactory(memory, "SUCCESS"), }); const controller = new AbortController(); const deletion = runtime.admin.deletePartition(controller.signal); controller.abort(); expect(await deletion).toEqual({ ok: true, value: { state: "DELETED", effect: "APPLIED" }, }); const preAborted = new AbortController(); preAborted.abort(); const second = createIndexedDbResumableUploadCheckpointRuntime({ scope: { ...scope, partitionToken: "partition_token_02" }, factory: deletingFactory(new MemoryIndexedDbFactory(), "SUCCESS"), }); expect( await second.admin.deletePartition(preAborted.signal), ).toMatchObject({ ok: false, error: { code: "ABORTED" }, }); }); // CP-1. Three IndexedDB adapters report a value-less commit as UNAVAILABLE; // this one reports CORRUPT_DATA/RECONCILE because a checkpoint read that // committed while producing nothing means the stored row could not be turned // into a checkpoint, and the caller has to reconcile rather than retry. it("reports a checkpoint commit that produced no value as CORRUPT_DATA", async () => { const scripted = scriptedFactory(); const store = createIndexedDbResumableUploadCheckpointStore({ scope, factory: scripted.factory, }); const pending = store.read("upload_key_01"); const transaction = await openedCheckpointTransaction(scripted); transaction.complete(); expect(await pending).toMatchObject({ ok: false, error: { code: "CORRUPT_DATA", operation: "UPLOAD_RECONCILE", recovery: "RECONCILE", }, }); }); // CP-2. A third argument would be a `{durability}` options bag. An engine // that has never seen the bag treats it differently from no bag at all, so // checkpoint writes stay on whatever the engine defaults to. it("opens checkpoint transactions without a durability options bag", async () => { const scripted = scriptedFactory(); const store = createIndexedDbResumableUploadCheckpointStore({ scope, factory: scripted.factory, }); const pending = store.read("upload_key_01"); const transaction = await openedCheckpointTransaction(scripted); transaction.complete(); await pending; expect(scripted.transactionArguments).toEqual([2, 2]); }); // CP-3. Recording the error and letting the transaction run on would let // `compareAndSwap`'s put commit after its get had already failed. it("aborts the checkpoint transaction as soon as a request fails", async () => { const scripted = scriptedFactory(); const store = createIndexedDbResumableUploadCheckpointStore({ scope, factory: scripted.factory, }); const pending = store.read("upload_key_01"); const transaction = await openedCheckpointTransaction(scripted); const read = transaction.requests[0]; if (!read) throw new Error("the checkpoint read queued no request"); read.error = new DOMException("disk is unreadable", "NotReadableError"); read.onerror?.(new Event("error")); expect(transaction.abortCalls).toBe(1); expect(await pending).toMatchObject({ ok: false, error: { code: "NOT_READABLE", retryable: true, recovery: "REOPEN" }, }); }); // CP-4. The transaction can already be durably committed while its completion // event is still queued, which is when `abort()` throws. Claiming the abort // would report a committed checkpoint as ABORTED. it("keeps a committed checkpoint when the abort call itself throws", async () => { const scripted = scriptedFactory({ abortThrows: true }); const store = createIndexedDbResumableUploadCheckpointStore({ scope, factory: scripted.factory, }); const controller = new AbortController(); const stored = checkpoint(1); const pending = store.read(stored.uploadKey, controller.signal); const transaction = await openedCheckpointTransaction(scripted); const read = transaction.requests[0]; if (!read) throw new Error("the checkpoint read queued no request"); read.result = stored; read.onsuccess?.(new Event("success")); controller.abort(); expect(transaction.abortCalls).toBe(1); transaction.complete(); expect(await pending).toEqual({ ok: true, value: stored }); }); // CP-5. The blocked deadline is the only bound on an open that another // context is holding up, so it has to fire from the scheduler the store was // built with rather than leave the read pending forever. it("bounds a blocked open with the configured deadline", async () => { const memory = new MemoryIndexedDbFactory(); const store = createIndexedDbResumableUploadCheckpointStore({ scope, factory: memory.factory, blockedTimeoutMs: 1, }); memory.blockNextOpen(); expect(await store.read("upload_key_01")).toMatchObject({ ok: false, error: { code: "BLOCKED", retryable: true, recovery: "RESUME" }, }); }); });