import * as IndexedDB from "$lib/indexedDB"; import { monotonicResolve } from "$lib/utils"; import { trpc, isTRPCClientError } from "$trpc/client"; import { FilesystemCache, decryptDirectoryMetadata, decryptFileMetadata } from "./internal.svelte"; import type { MaybeDirectoryInfo } from "./types"; const cache = new FilesystemCache(); const fetchFromIndexedDB = async (id: DirectoryId) => { const [directory, subDirectories, files] = await Promise.all([ id !== "root" ? IndexedDB.getDirectoryInfo(id) : undefined, IndexedDB.getDirectoryInfos(id), IndexedDB.getFileInfos(id), ]); if (id === "root") { return { id, exists: true as const, subDirectories, files, }; } else if (directory) { return { id, exists: true as const, parentId: directory.parentId, name: directory.name, subDirectories, files, }; } }; const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => { try { const { metadata, subDirectories: subDirectoriesRaw, files: filesRaw, } = await trpc().directory.get.query({ id }); void IndexedDB.deleteDanglingDirectoryInfos(id, new Set(subDirectoriesRaw.map(({ id }) => id))); void IndexedDB.deleteDanglingFileInfos(id, new Set(filesRaw.map(({ id }) => id))); const existingFiles = await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id)); const [subDirectories, files, decryptedMetadata] = await Promise.all([ Promise.all( subDirectoriesRaw.map(async (directory) => { const decrypted = await decryptDirectoryMetadata(directory, masterKey); await IndexedDB.storeDirectoryInfo({ id: directory.id, parentId: id, name: decrypted.name, }); return { id: directory.id, ...decrypted, }; }), ), Promise.all( filesRaw.map(async (file, index) => { const decrypted = await decryptFileMetadata(file, masterKey); await IndexedDB.storeFileInfo({ id: file.id, parentId: id, contentType: file.contentType, name: decrypted.name, createdAt: decrypted.createdAt, lastModifiedAt: decrypted.lastModifiedAt, categoryIds: existingFiles[index]?.categoryIds ?? [], }); return { id: file.id, contentType: file.contentType, ...decrypted, }; }), ), metadata ? decryptDirectoryMetadata(metadata, masterKey) : undefined, ]); if (id !== "root" && metadata && decryptedMetadata) { await IndexedDB.storeDirectoryInfo({ id, parentId: metadata.parent, name: decryptedMetadata.name, }); } if (id === "root") { return { id, exists: true as const, subDirectories, files, }; } else { return { id, exists: true as const, parentId: metadata!.parent, subDirectories, files, ...decryptedMetadata!, }; } } catch (e) { if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { await IndexedDB.deleteDirectoryInfo(id as number); return { id, exists: false as const }; } throw e; } }; export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => { return await cache.get(id, (isInitial, resolve) => monotonicResolve( [isInitial && fetchFromIndexedDB(id), fetchFromServer(id, masterKey)], resolve, ), ); };