mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 08:10:28 +09:00
186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
import * as IndexedDB from "#lib/client/storage/indexed-db/index.ts";
|
|
import { isTRPCClientError, trpc } from "#trpc/client.ts";
|
|
import { FilesystemCache } from "./cache.svelte.ts";
|
|
import { invalidateFile } from "./file.ts";
|
|
import {
|
|
decryptCategoryMetadata,
|
|
decryptFileMetadata,
|
|
encryptName,
|
|
generateEntryKeys,
|
|
} from "./metadata.ts";
|
|
import { useResource } from "./resource.svelte.ts";
|
|
import type { CategoryInfo, DataKey, LocalCategoryInfo, MaybeCategoryInfo } from "./types.ts";
|
|
|
|
const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({
|
|
async fetchFromIndexedDB(id) {
|
|
const [category, subCategories] = await Promise.all([
|
|
id !== "root" ? IndexedDB.getCategoryInfo(id) : undefined,
|
|
IndexedDB.getCategoryInfos(id),
|
|
]);
|
|
const files = category?.files
|
|
? await Promise.all(
|
|
category.files.map(async (file) => {
|
|
const fileInfo = await IndexedDB.getFileInfo(file.id);
|
|
return fileInfo
|
|
? {
|
|
id: file.id,
|
|
parentId: fileInfo.parentId,
|
|
contentType: fileInfo.contentType,
|
|
name: fileInfo.name,
|
|
createdAt: fileInfo.createdAt,
|
|
lastModifiedAt: fileInfo.lastModifiedAt,
|
|
isFavorite: fileInfo.isFavorite,
|
|
isRecursive: file.isRecursive,
|
|
}
|
|
: undefined;
|
|
}),
|
|
)
|
|
: undefined;
|
|
|
|
if (id === "root") {
|
|
return {
|
|
id,
|
|
exists: true,
|
|
subCategories,
|
|
};
|
|
} else if (category) {
|
|
return {
|
|
id,
|
|
exists: true,
|
|
parentId: category.parentId,
|
|
name: category.name,
|
|
subCategories,
|
|
files: files?.filter((file) => !!file) ?? [],
|
|
isFileRecursive: category.isFileRecursive ?? false,
|
|
};
|
|
}
|
|
},
|
|
|
|
async fetchFromServer(id, cachedInfo) {
|
|
try {
|
|
const category = await trpc.category.get.query({ id, recurse: true });
|
|
const [subCategories, files, metadata] = await Promise.all([
|
|
Promise.all(
|
|
category.subCategories.map(async (category) => ({
|
|
id: category.id,
|
|
parentId: id,
|
|
...(await decryptCategoryMetadata(category)),
|
|
})),
|
|
),
|
|
category.files &&
|
|
Promise.all(
|
|
category.files.map(async (file) => ({
|
|
id: file.id,
|
|
parentId: file.parent,
|
|
contentType: file.contentType,
|
|
isFavorite: file.isFavorite,
|
|
isRecursive: file.isRecursive,
|
|
...(await decryptFileMetadata(file)),
|
|
})),
|
|
),
|
|
category.metadata && decryptCategoryMetadata(category.metadata),
|
|
]);
|
|
|
|
return storeToIndexedDB(
|
|
id !== "root"
|
|
? {
|
|
id,
|
|
parentId: category.metadata!.parent,
|
|
subCategories,
|
|
files: files!,
|
|
isFileRecursive: cachedInfo?.isFileRecursive ?? false,
|
|
...metadata!,
|
|
}
|
|
: { id, subCategories },
|
|
);
|
|
} catch (e) {
|
|
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
|
await IndexedDB.deleteCategoryInfo(id as number);
|
|
return { id, exists: false };
|
|
}
|
|
throw e;
|
|
}
|
|
},
|
|
});
|
|
|
|
const storeToIndexedDB = (info: CategoryInfo) => {
|
|
if (info.id !== "root") {
|
|
void IndexedDB.storeCategoryInfo(info);
|
|
|
|
// TODO: Bulk Upsert
|
|
new Map(info.files.map((file) => [file.id, file])).forEach((file) => {
|
|
void IndexedDB.storeFileInfo(file);
|
|
});
|
|
}
|
|
|
|
// TODO: Bulk Upsert
|
|
info.subCategories.forEach((category) => {
|
|
void IndexedDB.storeCategoryInfo(category);
|
|
});
|
|
|
|
void IndexedDB.deleteDanglingCategoryInfos(
|
|
info.id,
|
|
new Set(info.subCategories.map(({ id }) => id)),
|
|
);
|
|
|
|
return { ...info, exists: true as const };
|
|
};
|
|
|
|
export const getCategoryInfo = (id: CategoryId) => {
|
|
return cache.get(id);
|
|
};
|
|
|
|
export const useCategory = (getId: () => CategoryId) => {
|
|
return useResource(getId, (id) => getCategoryInfo(id));
|
|
};
|
|
|
|
export const invalidateCategory = (id: CategoryId) => {
|
|
cache.invalidate(id);
|
|
};
|
|
|
|
export const createCategory = async (parentId: CategoryId, name: string) => {
|
|
await trpc.category.create.mutate({ parent: parentId, ...(await generateEntryKeys(name)) });
|
|
invalidateCategory(parentId);
|
|
};
|
|
|
|
export const renameCategory = async (
|
|
category: { id: number; parentId: CategoryId; dataKey?: DataKey },
|
|
newName: string,
|
|
) => {
|
|
if (!category.dataKey) {
|
|
throw new Error("Data key not loaded");
|
|
}
|
|
|
|
await trpc.category.rename.mutate({
|
|
id: category.id,
|
|
dekVersion: category.dataKey.version,
|
|
...(await encryptName(newName, category.dataKey.key)),
|
|
});
|
|
invalidateCategory(category.parentId);
|
|
invalidateCategory(category.id);
|
|
};
|
|
|
|
export const deleteCategory = async (category: { id: number; parentId: CategoryId }) => {
|
|
await trpc.category.delete.mutate({ id: category.id });
|
|
cache.evict(category.id);
|
|
invalidateCategory(category.parentId);
|
|
};
|
|
|
|
export const addFileToCategory = async (fileId: number, categoryId: number) => {
|
|
await trpc.category.addFile.mutate({ id: categoryId, file: fileId });
|
|
invalidateFile(fileId);
|
|
invalidateCategory(categoryId);
|
|
};
|
|
|
|
export const removeFileFromCategory = async (fileId: number, categoryId: number) => {
|
|
await trpc.category.removeFile.mutate({ id: categoryId, file: fileId });
|
|
invalidateFile(fileId);
|
|
invalidateCategory(categoryId);
|
|
};
|
|
|
|
// Whether to show the files of subcategories is a local preference
|
|
export const setCategoryFileRecursive = async (category: LocalCategoryInfo, value: boolean) => {
|
|
category.isFileRecursive = value;
|
|
await IndexedDB.updateCategoryInfo(category.id, { isFileRecursive: value });
|
|
};
|