mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
카테고리 페이지에서의 네트워크 호출 최적화
This commit is contained in:
@@ -3,11 +3,6 @@ import {
|
||||
getFileInfo as getFileInfoFromIndexedDB,
|
||||
storeFileInfo,
|
||||
deleteFileInfo,
|
||||
getCategoryInfos as getCategoryInfosFromIndexedDB,
|
||||
getCategoryInfo as getCategoryInfoFromIndexedDB,
|
||||
storeCategoryInfo,
|
||||
updateCategoryInfo as updateCategoryInfoInIndexedDB,
|
||||
deleteCategoryInfo,
|
||||
} from "$lib/indexedDB";
|
||||
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
@@ -25,28 +20,7 @@ export interface FileInfo {
|
||||
categoryIds: number[];
|
||||
}
|
||||
|
||||
export type CategoryInfo =
|
||||
| {
|
||||
id: "root";
|
||||
dataKey?: undefined;
|
||||
dataKeyVersion?: undefined;
|
||||
name?: undefined;
|
||||
subCategoryIds: number[];
|
||||
files?: undefined;
|
||||
isFileRecursive?: undefined;
|
||||
}
|
||||
| {
|
||||
id: number;
|
||||
dataKey?: CryptoKey;
|
||||
dataKeyVersion?: Date;
|
||||
name: string;
|
||||
subCategoryIds: number[];
|
||||
files: { id: number; isRecursive: boolean }[];
|
||||
isFileRecursive: boolean;
|
||||
};
|
||||
|
||||
const fileInfoStore = new Map<number, Writable<FileInfo | null>>();
|
||||
const categoryInfoStore = new Map<CategoryId, Writable<CategoryInfo | null>>();
|
||||
|
||||
const fetchFileInfoFromIndexedDB = async (id: number, info: Writable<FileInfo | null>) => {
|
||||
if (get(info)) return;
|
||||
@@ -130,124 +104,3 @@ export const getFileInfo = (fileId: number, masterKey: CryptoKey) => {
|
||||
fetchFileInfo(fileId, info, masterKey); // Intended
|
||||
return info;
|
||||
};
|
||||
|
||||
const fetchCategoryInfoFromIndexedDB = async (
|
||||
id: CategoryId,
|
||||
info: Writable<CategoryInfo | null>,
|
||||
) => {
|
||||
if (get(info)) return;
|
||||
|
||||
const [category, subCategories] = await Promise.all([
|
||||
id !== "root" ? getCategoryInfoFromIndexedDB(id) : undefined,
|
||||
getCategoryInfosFromIndexedDB(id),
|
||||
]);
|
||||
const subCategoryIds = subCategories.map(({ id }) => id);
|
||||
|
||||
if (id === "root") {
|
||||
info.set({ id, subCategoryIds });
|
||||
} else {
|
||||
if (!category) return;
|
||||
info.set({
|
||||
id,
|
||||
name: category.name,
|
||||
subCategoryIds,
|
||||
files: category.files,
|
||||
isFileRecursive: category.isFileRecursive,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategoryInfoFromServer = async (
|
||||
id: CategoryId,
|
||||
info: Writable<CategoryInfo | null>,
|
||||
masterKey: CryptoKey,
|
||||
) => {
|
||||
let data;
|
||||
try {
|
||||
data = await trpc().category.get.query({ id });
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
info.set(null);
|
||||
await deleteCategoryInfo(id as number);
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to fetch category information");
|
||||
}
|
||||
|
||||
const { metadata, subCategories } = data;
|
||||
|
||||
if (id === "root") {
|
||||
info.set({ id, subCategoryIds: subCategories });
|
||||
} else {
|
||||
const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey);
|
||||
const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey);
|
||||
|
||||
let files;
|
||||
try {
|
||||
files = await trpc().category.files.query({ id, recurse: true });
|
||||
} catch {
|
||||
throw new Error("Failed to fetch category files");
|
||||
}
|
||||
|
||||
const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive }));
|
||||
let isFileRecursive: boolean | undefined = undefined;
|
||||
|
||||
info.update((value) => {
|
||||
const newValue = {
|
||||
isFileRecursive: false,
|
||||
...value,
|
||||
id,
|
||||
dataKey,
|
||||
dataKeyVersion: new Date(metadata!.dekVersion),
|
||||
name,
|
||||
subCategoryIds: subCategories,
|
||||
files: filesMapped,
|
||||
};
|
||||
isFileRecursive = newValue.isFileRecursive;
|
||||
return newValue;
|
||||
});
|
||||
await storeCategoryInfo({
|
||||
id,
|
||||
parentId: metadata!.parent,
|
||||
name,
|
||||
files: filesMapped,
|
||||
isFileRecursive: isFileRecursive!,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategoryInfo = async (
|
||||
id: CategoryId,
|
||||
info: Writable<CategoryInfo | null>,
|
||||
masterKey: CryptoKey,
|
||||
) => {
|
||||
await fetchCategoryInfoFromIndexedDB(id, info);
|
||||
await fetchCategoryInfoFromServer(id, info, masterKey);
|
||||
};
|
||||
|
||||
export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) => {
|
||||
// TODO: MEK rotation
|
||||
|
||||
let info = categoryInfoStore.get(categoryId);
|
||||
if (!info) {
|
||||
info = writable(null);
|
||||
categoryInfoStore.set(categoryId, info);
|
||||
}
|
||||
|
||||
fetchCategoryInfo(categoryId, info, masterKey); // Intended
|
||||
return info;
|
||||
};
|
||||
|
||||
export const updateCategoryInfo = async (
|
||||
categoryId: number,
|
||||
changes: { isFileRecursive?: boolean },
|
||||
) => {
|
||||
await updateCategoryInfoInIndexedDB(categoryId, changes);
|
||||
categoryInfoStore.get(categoryId)?.update((value) => {
|
||||
if (!value) return value;
|
||||
if (changes.isFileRecursive !== undefined) {
|
||||
value.isFileRecursive = changes.isFileRecursive;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -54,13 +54,14 @@ interface FileInfo {
|
||||
}
|
||||
|
||||
export type SummarizedFileInfo = Omit<FileInfo, "parentId" | "contentIv" | "categories">;
|
||||
export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean };
|
||||
|
||||
interface LocalCategoryInfo {
|
||||
id: number;
|
||||
dataKey: DataKey | undefined;
|
||||
dataKey?: DataKey | undefined;
|
||||
name: string;
|
||||
subCategories: Omit<LocalCategoryInfo, "subCategories" | "files" | "isFileRecursive">[];
|
||||
files: { id: number; name: string; isRecursive: boolean }[];
|
||||
subCategories: SubCategoryInfo[];
|
||||
files: CategoryFileInfo[];
|
||||
isFileRecursive: boolean;
|
||||
}
|
||||
|
||||
@@ -68,13 +69,19 @@ interface RootCategoryInfo {
|
||||
id: "root";
|
||||
dataKey?: undefined;
|
||||
name?: undefined;
|
||||
subCategories: Omit<LocalCategoryInfo, "subCategories" | "files" | "isFileRecursive">[];
|
||||
subCategories: SubCategoryInfo[];
|
||||
files?: undefined;
|
||||
isFileRecursive?: undefined;
|
||||
}
|
||||
|
||||
export type CategoryInfo = LocalCategoryInfo | RootCategoryInfo;
|
||||
export type SubCategoryInfo = Omit<
|
||||
LocalCategoryInfo,
|
||||
"subCategories" | "files" | "isFileRecursive"
|
||||
>;
|
||||
|
||||
const directoryInfoCache = new Map<DirectoryId, DirectoryInfo | Promise<DirectoryInfo>>();
|
||||
const categoryInfoCache = new Map<CategoryId, CategoryInfo | Promise<CategoryInfo>>();
|
||||
|
||||
export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
const info = directoryInfoCache.get(id);
|
||||
@@ -189,3 +196,146 @@ const fetchDirectoryInfoFromServer = async (
|
||||
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
|
||||
return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10));
|
||||
};
|
||||
|
||||
export const getCategoryInfo = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
const info = categoryInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<CategoryInfo>();
|
||||
if (!info) {
|
||||
categoryInfoCache.set(id, promise);
|
||||
const categoryInfo = await fetchCategoryInfoFromIndexedDB(id);
|
||||
if (categoryInfo) {
|
||||
const state = $state(categoryInfo);
|
||||
categoryInfoCache.set(id, state);
|
||||
resolve(state);
|
||||
}
|
||||
}
|
||||
|
||||
fetchCategoryInfoFromServer(id, masterKey).then((categoryInfo) => {
|
||||
if (!categoryInfo) return;
|
||||
|
||||
let info = categoryInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(categoryInfo);
|
||||
categoryInfoCache.set(id, state);
|
||||
resolve(state);
|
||||
} else {
|
||||
Object.assign(info!, categoryInfo);
|
||||
resolve(info!);
|
||||
}
|
||||
});
|
||||
|
||||
return info ?? promise;
|
||||
};
|
||||
|
||||
const fetchCategoryInfoFromIndexedDB = async (
|
||||
id: CategoryId,
|
||||
): Promise<CategoryInfo | undefined> => {
|
||||
const [category, subCategories] = await Promise.all([
|
||||
id !== "root" ? getCategoryInfoFromIndexedDB(id) : undefined,
|
||||
getCategoryInfosFromIndexedDB(id),
|
||||
]);
|
||||
const files = category
|
||||
? await Promise.all(
|
||||
category.files.map(async (file) => {
|
||||
const fileInfo = await getFileInfoFromIndexedDB(file.id);
|
||||
return fileInfo
|
||||
? {
|
||||
id: file.id,
|
||||
contentType: fileInfo.contentType,
|
||||
name: fileInfo.name,
|
||||
createdAt: fileInfo.createdAt,
|
||||
lastModifiedAt: fileInfo.lastModifiedAt,
|
||||
isRecursive: file.isRecursive,
|
||||
}
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subCategories };
|
||||
} else if (category) {
|
||||
return {
|
||||
id,
|
||||
name: category.name,
|
||||
subCategories,
|
||||
files: files!.filter((file) => !!file),
|
||||
isFileRecursive: category.isFileRecursive,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategoryInfoFromServer = async (
|
||||
id: CategoryId,
|
||||
masterKey: CryptoKey,
|
||||
): Promise<CategoryInfo | undefined> => {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subCategories: subCategoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().category.get.query({ id, recurse: true });
|
||||
const [subCategories, files] = await Promise.all([
|
||||
Promise.all(
|
||||
subCategoriesRaw.map(async (category) => {
|
||||
const { dataKey } = await unwrapDataKey(category.dek, masterKey);
|
||||
const name = await decryptString(category.name, category.nameIv, dataKey);
|
||||
return {
|
||||
id: category.id,
|
||||
dataKey: { key: dataKey, version: category.dekVersion },
|
||||
name,
|
||||
};
|
||||
}),
|
||||
),
|
||||
id !== "root"
|
||||
? Promise.all(
|
||||
filesRaw!.map(async (file) => {
|
||||
const { dataKey } = await unwrapDataKey(file.dek, masterKey);
|
||||
const [name, createdAt, lastModifiedAt] = await Promise.all([
|
||||
decryptString(file.name, file.nameIv, dataKey),
|
||||
file.createdAt
|
||||
? decryptDate(file.createdAt, file.createdAtIv!, dataKey)
|
||||
: undefined,
|
||||
decryptDate(file.lastModifiedAt, file.lastModifiedAtIv, dataKey),
|
||||
]);
|
||||
return {
|
||||
id: file.id,
|
||||
dataKey: { key: dataKey, version: file.dekVersion },
|
||||
contentType: file.contentType,
|
||||
name,
|
||||
createdAt,
|
||||
lastModifiedAt,
|
||||
isRecursive: file.isRecursive,
|
||||
};
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subCategories };
|
||||
} else {
|
||||
const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey);
|
||||
const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey);
|
||||
return {
|
||||
id,
|
||||
dataKey: { key: dataKey, version: metadata!.dekVersion },
|
||||
name,
|
||||
subCategories,
|
||||
files: files!,
|
||||
isFileRecursive: false,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
categoryInfoCache.delete(id);
|
||||
await deleteCategoryInfo(id as number);
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to fetch category information");
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user