import { IDBFactory } from "fake-indexeddb"; import { describe, expect, it } from "vitest"; import type { ToolcraftBinaryAssetRepository } from "./binary-asset-repository"; import { TOOLCRAFT_BINARY_ASSET_MAX_DEPENDENCIES } from "./binary-asset-repository"; import { createManualHeartbeatScheduler } from "./indexeddb-binary-asset-test-support"; import { createIndexedDbToolcraftBinaryAssetRepository } from "./indexeddb-binary-asset-repository"; import { createMemoryToolcraftBinaryAssetRepository } from "./memory-binary-asset-repository"; const durable = { contentType: "application/octet-stream", durable: true, } as const; async function exerciseDependencyClosure( repository: ToolcraftBinaryAssetRepository, ): Promise { const lease = await repository.beginLease("dependency-closure"); await lease.put("source:a", new Uint8Array([1]), durable); await lease.put("source:b", new Uint8Array([2]), durable); await lease.put("document:a", new Uint8Array([3]), { ...durable, dependencies: ["source:b"], }); await lease.put("bundle:a", new Uint8Array([4]), { ...durable, dependencies: ["source:a", "document:a", "source:a"], }); await lease.put("unrelated", new Uint8Array([5]), durable); await lease.commit(); await expect(repository.get("bundle:a")).resolves.toMatchObject({ dependencies: ["document:a", "source:a"], }); await expect(repository.collect(new Set(["bundle:a"]))).resolves.toEqual([ "unrelated", ]); await expect(repository.get("source:a")).resolves.not.toBeNull(); await expect(repository.get("source:b")).resolves.not.toBeNull(); await expect(repository.get("document:a")).resolves.not.toBeNull(); await expect(repository.collect(new Set())).resolves.toEqual([ "bundle:a", "document:a", "source:a", "source:b", ]); } describe("binary asset dependency reachability", () => { it("preserves transitive dependencies in memory", async () => { const repository = createMemoryToolcraftBinaryAssetRepository(); try { await exerciseDependencyClosure(repository); } finally { await repository.dispose(); } }); it("preserves transitive dependencies in IndexedDB", async () => { const repository = createIndexedDbToolcraftBinaryAssetRepository({ databaseName: `toolcraft-dependencies-${crypto.randomUUID()}`, heartbeatScheduler: createManualHeartbeatScheduler().scheduler, indexedDB: new IDBFactory(), }); try { await exerciseDependencyClosure(repository); } finally { await repository.dispose(); } }); it("rejects self dependencies and treats dependency changes as conflicts", async () => { const repository = createMemoryToolcraftBinaryAssetRepository(); try { const lease = await repository.beginLease("dependency-validation"); await expect( lease.put("self", new Uint8Array([1]), { ...durable, dependencies: ["self"], }), ).rejects.toThrow(/cannot depend on itself/u); await lease.put("manifest", new Uint8Array([2]), { ...durable, dependencies: ["source:a"], }); await expect( lease.put("manifest", new Uint8Array([2]), { ...durable, dependencies: ["source:b"], }), ).rejects.toThrow(/manifest/u); } finally { await repository.dispose(); } }); it("bounds and snapshots dependency metadata without invoking caller iterators", async () => { const repository = createMemoryToolcraftBinaryAssetRepository(); try { const lease = await repository.beginLease("dependency-snapshot"); const sparse = ["source:a"]; sparse.length = 2; await expect( lease.put("sparse", new Uint8Array([1]), { ...durable, dependencies: sparse, }), ).rejects.toThrow(/cannot be sparse/u); await expect( lease.put("excessive", new Uint8Array([1]), { ...durable, dependencies: Array.from( { length: TOOLCRAFT_BINARY_ASSET_MAX_DEPENDENCIES + 1 }, (_, index) => `source:${index}`, ), }), ).rejects.toThrow(/exceed/u); const dependencies = ["source:b", "source:a"]; Object.defineProperty(dependencies, Symbol.iterator, { value: () => { throw new Error("caller iterator must not run"); }, }); await lease.put("manifest", new Uint8Array([2]), { ...durable, dependencies, }); dependencies[0] = "source:mutated"; await expect(lease.get("manifest")).resolves.toMatchObject({ dependencies: ["source:a", "source:b"], }); } finally { await repository.dispose(); } }); it.each([ ["memory", () => createMemoryToolcraftBinaryAssetRepository()], [ "IndexedDB", () => createIndexedDbToolcraftBinaryAssetRepository({ databaseName: `toolcraft-dependency-admission-${crypto.randomUUID()}`, heartbeatScheduler: createManualHeartbeatScheduler().scheduler, indexedDB: new IDBFactory(), }), ], ] as const)( "snapshots dependencies at put admission in %s", async (_name, createRepository) => { const repository: ToolcraftBinaryAssetRepository = createRepository(); try { const lease = await repository.beginLease("immediate-mutation"); const dependencies = ["source:original"]; const pendingPut = lease.put("manifest", new Uint8Array([1]), { ...durable, dependencies, }); dependencies[0] = "source:mutated"; await pendingPut; const staged = await lease.get("manifest"); expect(staged).toMatchObject({ dependencies: ["source:original"], }); expect(Object.isFrozen(staged)).toBe(true); let invalidPut: Promise | undefined; expect(() => { invalidPut = lease.put("self", new Uint8Array([2]), { ...durable, dependencies: ["self"], }); }).not.toThrow(); await expect(invalidPut).rejects.toThrow(/cannot depend on itself/u); } finally { await repository.dispose(); } }, ); });