import { collectRetainedAttachmentIds } from "@/attachments/gc-retention"; import { getAttachmentStore } from "@/attachments/store"; import type { AttachmentMetadata, SaveAttachmentInput } from "@/attachments/types"; const activePersistence = new Set>(); const persistedDuringGarbageCollection = new Set(); let pendingGarbageCollections = 0; let garbageCollectionTail: Promise = Promise.resolve(); let persistenceBarrier: Promise | null = null; let releasePersistenceBarrier: (() => void) | null = null; async function waitForPersistenceBarrier(): Promise { const barrier = persistenceBarrier; if (!barrier) { return; } await barrier; await waitForPersistenceBarrier(); } async function persistAttachment(input: SaveAttachmentInput): Promise { await waitForPersistenceBarrier(); const pending = (async () => { const store = await getAttachmentStore(); const attachment = await store.save(input); if (pendingGarbageCollections > 0) { persistedDuringGarbageCollection.add(attachment.id); } return attachment; })(); activePersistence.add(pending); try { return await pending; } finally { activePersistence.delete(pending); } } export async function persistAttachmentFromBlob(input: { blob: Blob; mimeType?: string; fileName?: string | null; id?: string; }): Promise { return await persistAttachment({ id: input.id, mimeType: input.mimeType, fileName: input.fileName, source: { kind: "blob", blob: input.blob }, }); } export async function persistAttachmentFromDataUrl(input: { dataUrl: string; mimeType?: string; fileName?: string | null; id?: string; }): Promise { return await persistAttachment({ id: input.id, mimeType: input.mimeType, fileName: input.fileName, source: { kind: "data_url", dataUrl: input.dataUrl }, }); } export async function persistAttachmentFromBytes(input: { bytes: Uint8Array; mimeType?: string; fileName?: string | null; id?: string; }): Promise { return await persistAttachment({ id: input.id, mimeType: input.mimeType, fileName: input.fileName, source: { kind: "bytes", bytes: input.bytes }, }); } export async function persistAttachmentFromFileUri(input: { uri: string; mimeType?: string; fileName?: string | null; id?: string; }): Promise { return await persistAttachment({ id: input.id, mimeType: input.mimeType, fileName: input.fileName, source: { kind: "file_uri", uri: input.uri }, }); } export async function encodeAttachmentsForSend( attachments: readonly AttachmentMetadata[] | undefined, ): Promise | undefined> { if (!attachments || attachments.length === 0) { return undefined; } const store = await getAttachmentStore(); const encoded = await Promise.all( attachments.map(async (attachment) => { try { const data = await store.encodeBase64({ attachment }); return { data, mimeType: attachment.mimeType, }; } catch (error) { console.error("[attachments] Failed to encode attachment for send", { id: attachment.id, error, }); return null; } }), ); const valid = encoded.filter( (entry): entry is { data: string; mimeType: string } => entry !== null, ); return valid.length > 0 ? valid : undefined; } export async function resolveAttachmentPreviewUrl(attachment: AttachmentMetadata): Promise { const store = await getAttachmentStore(); return await store.resolvePreviewUrl({ attachment }); } export async function releaseAttachmentPreviewUrl(input: { attachment: AttachmentMetadata; url: string; }): Promise { const store = await getAttachmentStore(); if (!store.releasePreviewUrl) { return; } await store.releasePreviewUrl({ attachment: input.attachment, url: input.url }); } export async function deleteAttachments( attachments: readonly AttachmentMetadata[] | undefined, ): Promise { if (!attachments || attachments.length === 0) { return; } const store = await getAttachmentStore(); await Promise.all( attachments.map(async (attachment) => { try { await store.delete({ attachment }); } catch (error) { console.warn("[attachments] Failed to delete attachment", { id: attachment.id, error, }); } }), ); } export async function garbageCollectAttachments(input: { referencedIds: ReadonlySet; }): Promise { pendingGarbageCollections += 1; if (!persistenceBarrier) { persistenceBarrier = new Promise((resolve) => { releasePersistenceBarrier = resolve; }); } const previousGarbageCollection = garbageCollectionTail; const currentGarbageCollection = (async () => { await previousGarbageCollection; while (activePersistence.size > 0) { await Promise.allSettled(activePersistence); } const referencedIds = new Set(input.referencedIds); for (const id of collectRetainedAttachmentIds()) { referencedIds.add(id); } for (const id of persistedDuringGarbageCollection) { referencedIds.add(id); } const store = await getAttachmentStore(); await store.garbageCollect({ referencedIds }); })(); garbageCollectionTail = currentGarbageCollection.catch(() => undefined); try { await currentGarbageCollection; } finally { pendingGarbageCollections -= 1; if (pendingGarbageCollections === 0) { persistedDuringGarbageCollection.clear(); const release = releasePersistenceBarrier; releasePersistenceBarrier = null; persistenceBarrier = null; release?.(); } } }