import { IDBFactory } from "fake-indexeddb"; import { afterEach, describe, expect, it } from "vitest"; import { createManualOwnerLivenessProvider } from "./indexeddb-binary-asset-test-support"; import { binaryOptions, cleanupIndexedDbRepositories, createIndexedDbToolcraftBinaryAssetRepository, createRepository, } from "./indexeddb-binary-asset-repository.test-support"; afterEach(cleanupIndexedDbRepositories); describe("IndexedDB binary asset repository", () => { it("observes request failures and releases the failed job registration", async () => { const indexedDB = new IDBFactory(); const ids = ["owner-id", "duplicate-lease", "duplicate-lease", "unique-lease"]; const repository = createIndexedDbToolcraftBinaryAssetRepository({ databaseName: `toolcraft-request-failure-${crypto.randomUUID()}`, idFactory: () => ids.shift() ?? crypto.randomUUID(), indexedDB, }); const firstLease = await repository.beginLease("first-job"); await expect(repository.beginLease("second-job")).rejects.toMatchObject({ name: "ConstraintError", }); const secondLease = await repository.beginLease("second-job"); await secondLease.rollback(); await firstLease.rollback(); }); it("isolates same-job leases across repository instances sharing one database", async () => { const indexedDB = new IDBFactory(); const databaseName = `toolcraft-shared-repository-${crypto.randomUUID()}`; const firstRepository = createIndexedDbToolcraftBinaryAssetRepository({ databaseName, indexedDB, }); const secondRepository = createIndexedDbToolcraftBinaryAssetRepository({ databaseName, indexedDB, }); const firstLease = await firstRepository.beginLease("shared-job"); await firstLease.put("first:staged", new Uint8Array([1]), binaryOptions); const secondLease = await secondRepository.beginLease("shared-job"); await expect(secondLease.get("first:staged")).resolves.toBeNull(); await secondLease.put("second:committed", new Uint8Array([2]), binaryOptions); await secondLease.commit(); await expect(firstRepository.get("first:staged")).resolves.toBeNull(); await expect(firstLease.get("first:staged")).resolves.toMatchObject({ bytes: new Uint8Array([1]), }); await expect(secondRepository.get("second:committed")).resolves.not.toBeNull(); const rollbackLease = await secondRepository.beginLease("shared-job"); await rollbackLease.put("second:rolled-back", new Uint8Array([3]), binaryOptions); await rollbackLease.rollback(); await expect(firstLease.get("first:staged")).resolves.not.toBeNull(); await expect(secondRepository.get("second:rolled-back")).resolves.toBeNull(); await firstLease.commit(); await expect(firstRepository.get("first:staged")).resolves.not.toBeNull(); }); it("keeps staged entries visible only to their owning lease", async () => { const repository = createRepository(); const owner = await repository.beginLease("owner"); const observer = await repository.beginLease("observer"); await owner.put("source:a", new Uint8Array([1, 2, 3]), binaryOptions); await expect(owner.get("source:a")).resolves.toMatchObject({ bytes: new Uint8Array([1, 2, 3]), ref: "source:a", }); await expect(observer.get("source:a")).resolves.toBeNull(); await expect(repository.get("source:a")).resolves.toBeNull(); await owner.commit(); await expect(repository.get("source:a")).resolves.toMatchObject({ bytes: new Uint8Array([1, 2, 3]), }); }); it("rolls back staged entries and rejects terminal lease operations", async () => { const repository = createRepository(); const lease = await repository.beginLease("rollback"); await lease.put("source:a", new Uint8Array([1]), binaryOptions); await lease.put("document:a", new Uint8Array([2]), { contentType: "application/vnd.toolcraft.model-document", durable: false, }); await lease.rollback(); await expect(repository.get("source:a")).resolves.toBeNull(); await expect(repository.get("document:a")).resolves.toBeNull(); await expect(lease.get("source:a")).rejects.toThrow(/terminal/u); expect(lease.refs()).toEqual([]); await expect( lease.put("later", new Uint8Array([3]), binaryOptions), ).rejects.toThrow(/terminal/u); await expect(lease.commit()).rejects.toThrow(/terminal/u); await expect(lease.rollback()).rejects.toThrow(/terminal/u); }); it("keeps identical same-lease puts idempotent and rejects conflicting replacements", async () => { const repository = createRepository(); const lease = await repository.beginLease("same-lease-duplicate"); await lease.put("shared", new Uint8Array([1, 2]), binaryOptions); await lease.put("shared", new Uint8Array([1, 2]), binaryOptions); await expect( lease.put("shared", new Uint8Array([1, 3]), binaryOptions), ).rejects.toThrow(/shared/u); await expect( lease.put("shared", new Uint8Array([1, 2]), { ...binaryOptions, contentType: "application/octet-stream", }), ).rejects.toThrow(/shared/u); await expect( lease.put("shared", new Uint8Array([1, 2]), { ...binaryOptions, durable: false, }), ).rejects.toThrow(/shared/u); expect(lease.refs()).toEqual(["shared"]); await expect(lease.get("shared")).resolves.toMatchObject({ bytes: new Uint8Array([1, 2]), contentType: binaryOptions.contentType, durable: true, }); }); it("publishes one lease atomically and rejects conflicting duplicate refs", async () => { const repository = createRepository(); const original = await repository.beginLease("original"); await original.put("shared", new Uint8Array([1]), binaryOptions); await original.commit(); const conflicting = await repository.beginLease("conflicting"); await conflicting.put("shared", new Uint8Array([2]), binaryOptions); await conflicting.put("unique", new Uint8Array([3]), binaryOptions); await expect(conflicting.commit()).rejects.toThrow(/shared/u); await expect(repository.get("shared")).resolves.toMatchObject({ bytes: new Uint8Array([1]), }); await expect(repository.get("unique")).resolves.toBeNull(); await conflicting.rollback(); const identical = await repository.beginLease("identical"); await identical.put("shared", new Uint8Array([1]), binaryOptions); await identical.put("published", new Uint8Array([4]), binaryOptions); await identical.commit(); await expect(repository.get("published")).resolves.not.toBeNull(); }); it("deterministically gives the first concurrent commit an immutable duplicate ref", async () => { const repository = createRepository(); const first = await repository.beginLease("first-concurrent"); const second = await repository.beginLease("second-concurrent"); await first.put("shared", new Uint8Array([1]), binaryOptions); await first.put("first-only", new Uint8Array([2]), binaryOptions); await second.put("shared", new Uint8Array([3]), binaryOptions); await second.put("second-only", new Uint8Array([4]), binaryOptions); const results = await Promise.allSettled([first.commit(), second.commit()]); expect(results.map((result) => result.status)).toEqual([ "fulfilled", "rejected", ]); await expect(repository.get("shared")).resolves.toMatchObject({ bytes: new Uint8Array([1]), }); await expect(repository.get("first-only")).resolves.not.toBeNull(); await expect(repository.get("second-only")).resolves.toBeNull(); await second.rollback(); }); it("defensively copies bytes on put and get", async () => { const repository = createRepository(); const lease = await repository.beginLease("copies"); const bytes = new Uint8Array([1, 2, 3]); await lease.put("source:a", bytes, binaryOptions); bytes[0] = 99; const stagedRead = await lease.get("source:a"); stagedRead!.bytes[1] = 88; await expect(lease.get("source:a")).resolves.toMatchObject({ bytes: new Uint8Array([1, 2, 3]), }); await lease.commit(); const committedRead = await repository.get("source:a"); committedRead!.bytes[2] = 77; await expect(repository.get("source:a")).resolves.toMatchObject({ bytes: new Uint8Array([1, 2, 3]), }); }); it("snapshots put bytes and options before asynchronous admission", async () => { const indexedDB = new IDBFactory(); const liveness = createManualOwnerLivenessProvider(); const repository = createIndexedDbToolcraftBinaryAssetRepository({ databaseName: `toolcraft-put-snapshot-${crypto.randomUUID()}`, indexedDB, ownerLivenessProvider: liveness.provider, }); const lease = await repository.beginLease("put-snapshot"); const bytes = new Uint8Array([1, 2, 3]); const options = { contentType: "model/gltf-binary", durable: true, }; const query = liveness.pauseNextQuery(); const put = lease.put("snapshot:put", bytes, options); bytes[0] = 9; options.contentType = "application/octet-stream"; options.durable = false; await query.started; query.resume(); await put; await lease.commit(); await expect(repository.get("snapshot:put")).resolves.toMatchObject({ bytes: new Uint8Array([1, 2, 3]), contentType: "model/gltf-binary", durable: true, }); }); it("collects committed refs deterministically without deleting staged refs", async () => { const repository = createRepository(); const committed = await repository.beginLease("committed"); await committed.put("z-ref", new Uint8Array([1]), binaryOptions); await committed.put("keep", new Uint8Array([2]), binaryOptions); await committed.put("a-ref", new Uint8Array([3]), binaryOptions); await committed.commit(); const staged = await repository.beginLease("staged"); await staged.put("staged-only", new Uint8Array([4]), binaryOptions); await expect(repository.collect(new Set(["keep"]))).resolves.toEqual([ "a-ref", "z-ref", ]); await expect(staged.get("staged-only")).resolves.not.toBeNull(); await staged.commit(); await expect(repository.get("staged-only")).resolves.not.toBeNull(); }); it("snapshots collect reachability before asynchronous admission", async () => { const indexedDB = new IDBFactory(); const liveness = createManualOwnerLivenessProvider(); const repository = createIndexedDbToolcraftBinaryAssetRepository({ databaseName: `toolcraft-collect-snapshot-${crypto.randomUUID()}`, indexedDB, ownerLivenessProvider: liveness.provider, }); const lease = await repository.beginLease("collect-snapshot"); await lease.put("snapshot:reachable", new Uint8Array([1]), binaryOptions); await lease.commit(); const reachableRefs = new Set(["snapshot:reachable"]); const query = liveness.pauseNextQuery(); const collection = repository.collect(reachableRefs); reachableRefs.clear(); await query.started; query.resume(); await expect(collection).resolves.toEqual([]); await expect(repository.get("snapshot:reachable")).resolves.not.toBeNull(); }); it("compensates for a post-commit failure by collecting the unreferenced publication", async () => { const repository = createRepository(); const lease = await repository.beginLease("post-commit-compensation"); await lease.put("orphaned-publication", new Uint8Array([1]), binaryOptions); await lease.commit(); await expect(lease.get("orphaned-publication")).rejects.toThrow(/terminal/u); expect(lease.refs()).toEqual([]); await expect(lease.rollback()).rejects.toThrow(/terminal/u); await expect(repository.get("orphaned-publication")).resolves.not.toBeNull(); await expect(repository.collect(new Set())).resolves.toEqual([ "orphaned-publication", ]); await expect(repository.get("orphaned-publication")).resolves.toBeNull(); });});