// @vitest-environment node import { IDBFactory } from "fake-indexeddb"; import { describe, expect, it, vi } from "vitest"; import { readToolcraftPersistedResourceRefs } from "../../state/persistence-resource-refs"; import type { ToolcraftBinaryAssetRepository, ToolcraftPersistedResourceRefsReader } from "./binary-asset-repository"; import { createIndexedDbToolcraftBinaryAssetRepository } from "./indexeddb-binary-asset-repository"; import { createManualHeartbeatScheduler, createManualOwnerLivenessProvider } from "./indexeddb-binary-asset-test-support"; import { createMemoryToolcraftBinaryAssetRepository } from "./memory-binary-asset-repository"; async function seed(repository: ToolcraftBinaryAssetRepository) { const lease = await repository.beginLease("seed"); await lease.put("texture:a", new Uint8Array([1]), { contentType: "image/png", durable: true }); await lease.put("document:a", new Uint8Array([2]), { contentType: "application/json", durable: true, dependencies: ["texture:a"] }); await lease.put("file:b", new Uint8Array([3]), { contentType: "text/plain", durable: true }); await lease.commit(); } function indexedRepository(getPersistedResourceRefs: ToolcraftPersistedResourceRefsReader) { return createIndexedDbToolcraftBinaryAssetRepository({ indexedDB: new IDBFactory(), ownerLivenessProvider: createManualOwnerLivenessProvider().provider, heartbeatScheduler: createManualHeartbeatScheduler().scheduler, getPersistedResourceRefs, }); } for (const [name, create] of [ ["memory", (getPersistedResourceRefs: ToolcraftPersistedResourceRefsReader) => createMemoryToolcraftBinaryAssetRepository({ getPersistedResourceRefs })], ["indexedDB", indexedRepository], ] as const) { describe(`${name} persisted collection roots`, () => { it("retains a saved model dependency graph and releases it after durable supersession", async () => { let saved = JSON.stringify({ version: 9, state: { mediaAssets: [{ assetKind: "model", activeDocumentRef: "document:a" }] } }); const storage = { length: 1, key: () => "closed-app-custom-key", getItem: () => saved }; const repository = create(() => readToolcraftPersistedResourceRefs(() => storage)); try { await seed(repository); expect(await repository.collect(new Set(["file:b"]))).toEqual([]); expect(await repository.get("texture:a")).not.toBeNull(); saved = JSON.stringify({ version: 9, state: { mediaAssets: [{ assetKind: "file", resourceRef: "file:b" }] } }); expect(await repository.collect(new Set())).toEqual(["document:a", "texture:a"]); expect(await repository.get("file:b")).not.toBeNull(); } finally { await repository.dispose(); } }); it("blocks deletion while durable ownership is unavailable and resumes after recovery", async () => { let refs: ReadonlySet | null = null; const repository = create(() => refs); try { await seed(repository); expect(await repository.collect(new Set())).toEqual([]); expect(await repository.get("file:b")).not.toBeNull(); refs = new Set(); expect(await repository.collect(new Set())).toEqual(["document:a", "file:b", "texture:a"]); } finally { await repository.dispose(); } }); }); } it("reads durable roots when a queued IndexedDB collection actually runs", async () => { const liveness = createManualOwnerLivenessProvider(); let refs = new Set(["document:a"]); const getPersistedResourceRefs = vi.fn(() => refs); const repository = createIndexedDbToolcraftBinaryAssetRepository({ indexedDB: new IDBFactory(), ownerLivenessProvider: liveness.provider, heartbeatScheduler: createManualHeartbeatScheduler().scheduler, getPersistedResourceRefs, }); try { await seed(repository); const pause = liveness.pauseNextQuery(); const collection = repository.collect(new Set()); await pause.started; expect(getPersistedResourceRefs).not.toHaveBeenCalled(); refs = new Set(["file:b"]); pause.resume(); expect(await collection).toEqual(["document:a", "texture:a"]); expect(await repository.get("file:b")).not.toBeNull(); expect(getPersistedResourceRefs).toHaveBeenCalledTimes(1); } finally { await repository.dispose(); } });