mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
파일, 카테고리, 디렉터리 정보를 불러올 때 특정 조건에서 네트워크 요청이 여러 번 발생할 수 있는 버그 수정
This commit is contained in:
@@ -1,167 +1,121 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||
import type { MaybeCategoryInfo } from "./types";
|
||||
import type { CategoryInfo, MaybeCategoryInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo, Partial<MaybeCategoryInfo>>();
|
||||
|
||||
const fetchFromIndexedDB = async (id: CategoryId) => {
|
||||
const [category, subCategories] = await Promise.all([
|
||||
id !== "root" ? IndexedDB.getCategoryInfo(id) : undefined,
|
||||
IndexedDB.getCategoryInfos(id),
|
||||
]);
|
||||
const files = category
|
||||
? await Promise.all(
|
||||
category.files.map(async (file) => {
|
||||
const fileInfo = await IndexedDB.getFileInfo(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,
|
||||
exists: true as const,
|
||||
subCategories,
|
||||
};
|
||||
} else if (category) {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
name: category.name,
|
||||
subCategories,
|
||||
files: files!.filter((file) => !!file),
|
||||
isFileRecursive: category.isFileRecursive,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFromServer = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subCategories: subCategoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().category.get.query({ id, recurse: true });
|
||||
|
||||
void IndexedDB.deleteDanglingCategoryInfos(id, new Set(subCategoriesRaw.map(({ id }) => id)));
|
||||
|
||||
const subCategories = await Promise.all(
|
||||
subCategoriesRaw.map(async (category) => {
|
||||
const decrypted = await decryptCategoryMetadata(category, masterKey);
|
||||
const existing = await IndexedDB.getCategoryInfo(category.id);
|
||||
await IndexedDB.storeCategoryInfo({
|
||||
id: category.id,
|
||||
parentId: id,
|
||||
name: decrypted.name,
|
||||
files: existing?.files ?? [],
|
||||
isFileRecursive: existing?.isFileRecursive ?? false,
|
||||
});
|
||||
return {
|
||||
id: category.id,
|
||||
...decrypted,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const existingFiles = filesRaw
|
||||
? await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id))
|
||||
: [];
|
||||
const files = filesRaw
|
||||
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(
|
||||
filesRaw.map(async (file, index) => {
|
||||
const decrypted = await decryptFileMetadata(file, masterKey);
|
||||
const existing = existingFiles[index];
|
||||
if (existing) {
|
||||
const categoryIds = file.isRecursive
|
||||
? existing.categoryIds
|
||||
: Array.from(new Set([...existing.categoryIds, id as number]));
|
||||
await IndexedDB.storeFileInfo({
|
||||
id: file.id,
|
||||
parentId: existing.parentId,
|
||||
contentType: file.contentType,
|
||||
name: decrypted.name,
|
||||
createdAt: decrypted.createdAt,
|
||||
lastModifiedAt: decrypted.lastModifiedAt,
|
||||
categoryIds,
|
||||
});
|
||||
}
|
||||
return {
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
isRecursive: file.isRecursive,
|
||||
...decrypted,
|
||||
};
|
||||
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,
|
||||
isRecursive: file.isRecursive,
|
||||
}
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const decryptedMetadata = metadata
|
||||
? await decryptCategoryMetadata(metadata, masterKey)
|
||||
: undefined;
|
||||
if (id !== "root" && metadata && decryptedMetadata) {
|
||||
const existingCategory = await IndexedDB.getCategoryInfo(id);
|
||||
await IndexedDB.storeCategoryInfo({
|
||||
id: id as number,
|
||||
parentId: metadata.parent,
|
||||
name: decryptedMetadata.name,
|
||||
files:
|
||||
files?.map((file) => ({
|
||||
id: file.id,
|
||||
isRecursive: file.isRecursive,
|
||||
})) ??
|
||||
existingCategory?.files ??
|
||||
[],
|
||||
isFileRecursive: existingCategory?.isFileRecursive ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
if (id === "root") {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
subCategories,
|
||||
};
|
||||
} else {
|
||||
} else if (category) {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
parentId: category.parentId,
|
||||
name: category.name,
|
||||
subCategories,
|
||||
files,
|
||||
...decryptedMetadata!,
|
||||
files: files?.filter((file) => !!file) ?? [],
|
||||
isFileRecursive: category.isFileRecursive ?? false,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
await IndexedDB.deleteCategoryInfo(id as number);
|
||||
return { id, exists: false as const };
|
||||
},
|
||||
|
||||
async fetchFromServer(id, cachedInfo, masterKey) {
|
||||
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, masterKey)),
|
||||
})),
|
||||
),
|
||||
category.files &&
|
||||
Promise.all(
|
||||
category.files.map(async (file) => ({
|
||||
id: file.id,
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
isRecursive: file.isRecursive,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
),
|
||||
category.metadata && decryptCategoryMetadata(category.metadata, masterKey),
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
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 = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, async (isInitial, resolve) => {
|
||||
if (isInitial) {
|
||||
const info = await fetchFromIndexedDB(id);
|
||||
if (info) {
|
||||
resolve(info);
|
||||
}
|
||||
}
|
||||
|
||||
const info = await fetchFromServer(id, masterKey);
|
||||
if (info) {
|
||||
resolve(info);
|
||||
}
|
||||
});
|
||||
return await cache.get(id, masterKey);
|
||||
};
|
||||
|
||||
@@ -1,125 +1,102 @@
|
||||
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";
|
||||
import type { DirectoryInfo, MaybeDirectoryInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>();
|
||||
|
||||
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,
|
||||
const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>({
|
||||
async fetchFromIndexedDB(id) {
|
||||
const [directory, subDirectories, files] = await Promise.all([
|
||||
id !== "root" ? IndexedDB.getDirectoryInfo(id) : undefined,
|
||||
IndexedDB.getDirectoryInfos(id),
|
||||
IndexedDB.getFileInfos(id),
|
||||
]);
|
||||
|
||||
if (id !== "root" && metadata && decryptedMetadata) {
|
||||
await IndexedDB.storeDirectoryInfo({
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
name: decryptedMetadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (id === "root") {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
exists: true,
|
||||
subDirectories,
|
||||
files,
|
||||
};
|
||||
} else {
|
||||
} else if (directory) {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: metadata!.parent,
|
||||
exists: true,
|
||||
parentId: directory.parentId,
|
||||
name: directory.name,
|
||||
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 };
|
||||
},
|
||||
|
||||
async fetchFromServer(id, _cachedInfo, masterKey) {
|
||||
try {
|
||||
const directory = await trpc().directory.get.query({ id });
|
||||
const [subDirectories, files, metadata] = await Promise.all([
|
||||
Promise.all(
|
||||
directory.subDirectories.map(async (directory) => ({
|
||||
id: directory.id,
|
||||
parentId: id,
|
||||
...(await decryptDirectoryMetadata(directory, masterKey)),
|
||||
})),
|
||||
),
|
||||
Promise.all(
|
||||
directory.files.map(async (file) => ({
|
||||
id: file.id,
|
||||
parentId: id,
|
||||
contentType: file.contentType,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
),
|
||||
directory.metadata && decryptDirectoryMetadata(directory.metadata, masterKey),
|
||||
]);
|
||||
|
||||
return storeToIndexedDB(
|
||||
id !== "root"
|
||||
? {
|
||||
id,
|
||||
parentId: directory.metadata!.parent,
|
||||
subDirectories,
|
||||
files,
|
||||
...metadata!,
|
||||
}
|
||||
: { id, subDirectories, files },
|
||||
);
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
await IndexedDB.deleteDirectoryInfo(id as number);
|
||||
return { id, exists: false as const };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
throw e;
|
||||
},
|
||||
});
|
||||
|
||||
const storeToIndexedDB = (info: DirectoryInfo) => {
|
||||
if (info.id !== "root") {
|
||||
void IndexedDB.storeDirectoryInfo(info);
|
||||
}
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
info.subDirectories.forEach((subDirectory) => {
|
||||
void IndexedDB.storeDirectoryInfo(subDirectory);
|
||||
});
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
info.files.forEach((file) => {
|
||||
void IndexedDB.storeFileInfo(file);
|
||||
});
|
||||
|
||||
void IndexedDB.deleteDanglingDirectoryInfos(
|
||||
info.id,
|
||||
new Set(info.subDirectories.map(({ id }) => id)),
|
||||
);
|
||||
void IndexedDB.deleteDanglingFileInfos(info.id, new Set(info.files.map(({ id }) => id)));
|
||||
|
||||
return { ...info, exists: true as const };
|
||||
};
|
||||
|
||||
export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, (isInitial, resolve) =>
|
||||
monotonicResolve(
|
||||
[isInitial && fetchFromIndexedDB(id), fetchFromServer(id, masterKey)],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
return await cache.get(id, masterKey);
|
||||
};
|
||||
|
||||
@@ -1,175 +1,177 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { monotonicResolve } from "$lib/utils";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||
import type { MaybeFileInfo } from "./types";
|
||||
import type { FileInfo, MaybeFileInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<number, MaybeFileInfo>();
|
||||
const cache = new FilesystemCache<number, MaybeFileInfo>({
|
||||
async fetchFromIndexedDB(id) {
|
||||
const file = await IndexedDB.getFileInfo(id);
|
||||
const categories = file?.categoryIds
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category
|
||||
? { id: category.id, parentId: category.parentId, name: category.name }
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const fetchFromIndexedDB = async (id: number) => {
|
||||
const file = await IndexedDB.getFileInfo(id);
|
||||
const categories = file
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category ? { id: category.id, name: category.name } : undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (file) {
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: file.parentId,
|
||||
contentType: file.contentType,
|
||||
name: file.name,
|
||||
createdAt: file.createdAt,
|
||||
lastModifiedAt: file.lastModifiedAt,
|
||||
categories: categories!.filter((category) => !!category),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const bulkFetchFromIndexedDB = async (ids: number[]) => {
|
||||
const files = await IndexedDB.bulkGetFileInfos(ids);
|
||||
const categories = await Promise.all(
|
||||
files.map(async (file) =>
|
||||
file
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category ? { id: category.id, name: category.name } : undefined;
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
return new Map(
|
||||
files
|
||||
.map((file, index) =>
|
||||
file
|
||||
? ([
|
||||
file.id,
|
||||
{
|
||||
...file,
|
||||
exists: true,
|
||||
categories: categories[index]!.filter((category) => !!category),
|
||||
},
|
||||
] as const)
|
||||
: undefined,
|
||||
)
|
||||
.filter((file) => !!file),
|
||||
);
|
||||
};
|
||||
|
||||
const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
|
||||
try {
|
||||
const { categories: categoriesRaw, ...metadata } = await trpc().file.get.query({ id });
|
||||
const [categories, decryptedMetadata] = await Promise.all([
|
||||
Promise.all(
|
||||
categoriesRaw.map(async (category) => ({
|
||||
id: category.id,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
decryptFileMetadata(metadata, masterKey),
|
||||
]);
|
||||
|
||||
await IndexedDB.storeFileInfo({
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
contentType: metadata.contentType,
|
||||
name: decryptedMetadata.name,
|
||||
createdAt: decryptedMetadata.createdAt,
|
||||
lastModifiedAt: decryptedMetadata.lastModifiedAt,
|
||||
categoryIds: categories.map((category) => category.id),
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: metadata.parent,
|
||||
contentType: metadata.contentType,
|
||||
contentIv: metadata.contentIv,
|
||||
categories,
|
||||
...decryptedMetadata,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
await IndexedDB.deleteFileInfo(id);
|
||||
return { id, exists: false as const };
|
||||
if (file) {
|
||||
return {
|
||||
id,
|
||||
exists: true,
|
||||
parentId: file.parentId,
|
||||
contentType: file.contentType,
|
||||
name: file.name,
|
||||
createdAt: file.createdAt,
|
||||
lastModifiedAt: file.lastModifiedAt,
|
||||
categories: categories?.filter((category) => !!category) ?? [],
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
const bulkFetchFromServer = async (ids: number[], masterKey: CryptoKey) => {
|
||||
const filesRaw = await trpc().file.bulkGet.query({ ids });
|
||||
const files = await Promise.all(
|
||||
filesRaw.map(async (file) => {
|
||||
const [categories, decryptedMetadata] = await Promise.all([
|
||||
async fetchFromServer(id, _cachedInfo, masterKey) {
|
||||
try {
|
||||
const file = await trpc().file.get.query({ id });
|
||||
const [categories, metadata] = await Promise.all([
|
||||
Promise.all(
|
||||
file.categories.map(async (category) => ({
|
||||
id: category.id,
|
||||
parentId: category.parent,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
decryptFileMetadata(file, masterKey),
|
||||
]);
|
||||
|
||||
await IndexedDB.storeFileInfo({
|
||||
id: file.id,
|
||||
parentId: file.parent,
|
||||
contentType: file.contentType,
|
||||
name: decryptedMetadata.name,
|
||||
createdAt: decryptedMetadata.createdAt,
|
||||
lastModifiedAt: decryptedMetadata.lastModifiedAt,
|
||||
categoryIds: categories.map((category) => category.id),
|
||||
});
|
||||
return {
|
||||
id: file.id,
|
||||
exists: true as const,
|
||||
return storeToIndexedDB({
|
||||
id,
|
||||
parentId: file.parent,
|
||||
dataKey: metadata.dataKey,
|
||||
contentType: file.contentType,
|
||||
contentIv: file.contentIv,
|
||||
name: metadata.name,
|
||||
createdAt: metadata.createdAt,
|
||||
lastModifiedAt: metadata.lastModifiedAt,
|
||||
categories,
|
||||
...decryptedMetadata,
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
await IndexedDB.deleteFileInfo(id);
|
||||
return { id, exists: false as const };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
const existingIds = new Set(filesRaw.map(({ id }) => id));
|
||||
return new Map<number, MaybeFileInfo>([
|
||||
...files.map((file) => [file.id, file] as const),
|
||||
...ids.filter((id) => !existingIds.has(id)).map((id) => [id, { id, exists: false }] as const),
|
||||
]);
|
||||
async bulkFetchFromIndexedDB(ids) {
|
||||
const files = await IndexedDB.bulkGetFileInfos([...ids]);
|
||||
const categories = await Promise.all(
|
||||
files.map(async (file) =>
|
||||
file?.categoryIds
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category
|
||||
? { id: category.id, parentId: category.parentId, name: category.name }
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
return new Map(
|
||||
files
|
||||
.filter((file) => !!file)
|
||||
.map((file, index) => [
|
||||
file.id,
|
||||
{
|
||||
...file,
|
||||
exists: true,
|
||||
categories: categories[index]?.filter((category) => !!category) ?? [],
|
||||
},
|
||||
]),
|
||||
);
|
||||
},
|
||||
|
||||
async bulkFetchFromServer(ids, masterKey) {
|
||||
const idsArray = [...ids.keys()];
|
||||
|
||||
const filesRaw = await trpc().file.bulkGet.query({ ids: idsArray });
|
||||
const files = await Promise.all(
|
||||
filesRaw.map(async ({ id, categories: categoriesRaw, ...metadataRaw }) => {
|
||||
const [categories, metadata] = await Promise.all([
|
||||
Promise.all(
|
||||
categoriesRaw.map(async (category) => ({
|
||||
id: category.id,
|
||||
parentId: category.parent,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
decryptFileMetadata(metadataRaw, masterKey),
|
||||
]);
|
||||
|
||||
return {
|
||||
id,
|
||||
exists: true as const,
|
||||
parentId: metadataRaw.parent,
|
||||
contentType: metadataRaw.contentType,
|
||||
contentIv: metadataRaw.contentIv,
|
||||
categories,
|
||||
...metadata,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const existingIds = new Set(filesRaw.map(({ id }) => id));
|
||||
|
||||
return new Map<number, MaybeFileInfo>([
|
||||
...bulkStoreToIndexedDB(files),
|
||||
...idsArray
|
||||
.filter((id) => !existingIds.has(id))
|
||||
.map((id) => [id, { id, exists: false }] as const),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
const storeToIndexedDB = (info: FileInfo) => {
|
||||
void IndexedDB.storeFileInfo({
|
||||
...info,
|
||||
categoryIds: info.categories.map(({ id }) => id),
|
||||
});
|
||||
|
||||
info.categories.forEach((category) => {
|
||||
void IndexedDB.storeCategoryInfo(category);
|
||||
});
|
||||
|
||||
return { ...info, exists: true as const };
|
||||
};
|
||||
|
||||
const bulkStoreToIndexedDB = (infos: FileInfo[]) => {
|
||||
// TODO: Bulk Upsert
|
||||
infos.forEach((info) => {
|
||||
void IndexedDB.storeFileInfo({
|
||||
...info,
|
||||
categoryIds: info.categories.map(({ id }) => id),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Bulk Upsert
|
||||
new Map(
|
||||
infos.flatMap(({ categories }) => categories).map((category) => [category.id, category]),
|
||||
).forEach((category) => {
|
||||
void IndexedDB.storeCategoryInfo(category);
|
||||
});
|
||||
|
||||
return infos.map((info) => [info.id, { ...info, exists: true }] as const);
|
||||
};
|
||||
|
||||
export const getFileInfo = async (id: number, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, (isInitial, resolve) =>
|
||||
monotonicResolve(
|
||||
[isInitial && fetchFromIndexedDB(id), fetchFromServer(id, masterKey)],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
return await cache.get(id, masterKey);
|
||||
};
|
||||
|
||||
export const bulkGetFileInfo = async (ids: number[], masterKey: CryptoKey) => {
|
||||
return await cache.bulkGet(new Set(ids), (keys, resolve) =>
|
||||
monotonicResolve(
|
||||
[
|
||||
bulkFetchFromIndexedDB(
|
||||
Array.from(
|
||||
keys
|
||||
.entries()
|
||||
.filter(([, isInitial]) => isInitial)
|
||||
.map(([key]) => key),
|
||||
),
|
||||
),
|
||||
bulkFetchFromServer(Array.from(keys.keys()), masterKey),
|
||||
],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
return await cache.bulkGet(new Set(ids), masterKey);
|
||||
};
|
||||
|
||||
@@ -1,82 +1,120 @@
|
||||
import { untrack } from "svelte";
|
||||
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
||||
|
||||
export class FilesystemCache<K, V extends RV, RV = V> {
|
||||
private map = new Map<K, V | Promise<V>>();
|
||||
interface FilesystemCacheOptions<K, V> {
|
||||
fetchFromIndexedDB: (key: K) => Promise<V | undefined>;
|
||||
fetchFromServer: (key: K, cachedValue: V | undefined, masterKey: CryptoKey) => Promise<V>;
|
||||
bulkFetchFromIndexedDB?: (keys: Set<K>) => Promise<Map<K, V>>;
|
||||
bulkFetchFromServer?: (
|
||||
keys: Map<K, { cachedValue: V | undefined }>,
|
||||
masterKey: CryptoKey,
|
||||
) => Promise<Map<K, V>>;
|
||||
}
|
||||
|
||||
get(key: K, loader: (isInitial: boolean, resolve: (value: RV | undefined) => void) => void) {
|
||||
const info = this.map.get(key);
|
||||
if (info instanceof Promise) {
|
||||
return info;
|
||||
}
|
||||
export class FilesystemCache<K, V extends object> {
|
||||
private map = new Map<K, { value?: V; promise?: Promise<V> }>();
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<V>();
|
||||
if (!info) {
|
||||
this.map.set(key, promise);
|
||||
}
|
||||
constructor(private readonly options: FilesystemCacheOptions<K, V>) {}
|
||||
|
||||
loader(!info, (loadedInfo) => {
|
||||
if (!loadedInfo) return;
|
||||
get(key: K, masterKey: CryptoKey) {
|
||||
return untrack(() => {
|
||||
let state = this.map.get(key);
|
||||
if (state?.promise) return state.value ?? state.promise;
|
||||
|
||||
const info = this.map.get(key)!;
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(loadedInfo);
|
||||
this.map.set(key, state as V);
|
||||
resolve(state as V);
|
||||
} else {
|
||||
Object.assign(info, loadedInfo);
|
||||
resolve(info);
|
||||
const { promise: newPromise, resolve } = Promise.withResolvers<V>();
|
||||
|
||||
if (!state) {
|
||||
const newState = $state({});
|
||||
state = newState;
|
||||
this.map.set(key, newState);
|
||||
}
|
||||
});
|
||||
|
||||
return info ?? promise;
|
||||
state.promise = newPromise;
|
||||
|
||||
(state.value
|
||||
? Promise.resolve(state.value)
|
||||
: this.options.fetchFromIndexedDB(key).then((loadedInfo) => {
|
||||
if (loadedInfo) {
|
||||
state.value = loadedInfo;
|
||||
resolve(state.value);
|
||||
}
|
||||
return loadedInfo;
|
||||
})
|
||||
)
|
||||
.then((cachedInfo) => this.options.fetchFromServer(key, cachedInfo, masterKey))
|
||||
.then((loadedInfo) => {
|
||||
if (state.value) {
|
||||
Object.assign(state.value, loadedInfo);
|
||||
} else {
|
||||
state.value = loadedInfo;
|
||||
}
|
||||
resolve(state.value);
|
||||
})
|
||||
.finally(() => {
|
||||
state.promise = undefined;
|
||||
});
|
||||
|
||||
return newPromise;
|
||||
});
|
||||
}
|
||||
|
||||
async bulkGet(
|
||||
keys: Set<K>,
|
||||
loader: (keys: Map<K, boolean>, resolve: (values: Map<K, RV>) => void) => void,
|
||||
) {
|
||||
const states = new Map<K, V>();
|
||||
const promises = new Map<K, Promise<V>>();
|
||||
const resolvers = new Map<K, (value: V) => void>();
|
||||
bulkGet(keys: Set<K>, masterKey: CryptoKey) {
|
||||
return untrack(() => {
|
||||
const newPromises = new Map(
|
||||
keys
|
||||
.keys()
|
||||
.filter((key) => this.map.get(key)?.promise === undefined)
|
||||
.map((key) => [key, Promise.withResolvers<V>()]),
|
||||
);
|
||||
newPromises.forEach(({ promise }, key) => {
|
||||
const state = this.map.get(key);
|
||||
if (state) {
|
||||
state.promise = promise;
|
||||
} else {
|
||||
const newState = $state({ promise });
|
||||
this.map.set(key, newState);
|
||||
}
|
||||
});
|
||||
|
||||
keys.forEach((key) => {
|
||||
const info = this.map.get(key);
|
||||
if (info instanceof Promise) {
|
||||
promises.set(key, info);
|
||||
} else if (info) {
|
||||
states.set(key, info);
|
||||
} else {
|
||||
const { promise, resolve } = Promise.withResolvers<V>();
|
||||
this.map.set(key, promise);
|
||||
promises.set(key, promise);
|
||||
resolvers.set(key, resolve);
|
||||
}
|
||||
});
|
||||
|
||||
loader(
|
||||
new Map([
|
||||
...states.keys().map((key) => [key, false] as const),
|
||||
...resolvers.keys().map((key) => [key, true] as const),
|
||||
]),
|
||||
(loadedInfos) =>
|
||||
const resolve = (loadedInfos: Map<K, V>) => {
|
||||
loadedInfos.forEach((loadedInfo, key) => {
|
||||
const info = this.map.get(key)!;
|
||||
const resolve = resolvers.get(key);
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(loadedInfo);
|
||||
this.map.set(key, state as V);
|
||||
resolve?.(state as V);
|
||||
const state = this.map.get(key)!;
|
||||
if (state.value) {
|
||||
Object.assign(state.value, loadedInfo);
|
||||
} else {
|
||||
Object.assign(info, loadedInfo);
|
||||
resolve?.(info);
|
||||
state.value = loadedInfo;
|
||||
}
|
||||
}),
|
||||
);
|
||||
newPromises.get(key)!.resolve(state.value);
|
||||
});
|
||||
return loadedInfos;
|
||||
};
|
||||
|
||||
const newStates = await Promise.all(
|
||||
promises.entries().map(async ([key, promise]) => [key, await promise] as const),
|
||||
);
|
||||
return new Map([...states, ...newStates]);
|
||||
this.options.bulkFetchFromIndexedDB!(
|
||||
new Set(newPromises.keys().filter((key) => this.map.get(key)!.value === undefined)),
|
||||
)
|
||||
.then(resolve)
|
||||
.then(() =>
|
||||
this.options.bulkFetchFromServer!(
|
||||
new Map(
|
||||
newPromises.keys().map((key) => [key, { cachedValue: this.map.get(key)!.value }]),
|
||||
),
|
||||
masterKey,
|
||||
),
|
||||
)
|
||||
.then(resolve)
|
||||
.finally(() => {
|
||||
newPromises.forEach((_, key) => {
|
||||
this.map.get(key)!.promise = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
keys
|
||||
.keys()
|
||||
.filter((key) => this.map.get(key)!.value === undefined)
|
||||
.map((key) => this.map.get(key)!.promise!),
|
||||
).then(() => new Map(keys.keys().map((key) => [key, this.map.get(key)!.value!] as const)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,12 @@ interface RootDirectoryInfo {
|
||||
}
|
||||
|
||||
export type DirectoryInfo = LocalDirectoryInfo | RootDirectoryInfo;
|
||||
export type SubDirectoryInfo = Omit<LocalDirectoryInfo, "parentId" | "subDirectories" | "files">;
|
||||
export type MaybeDirectoryInfo =
|
||||
| (DirectoryInfo & { exists: true })
|
||||
| ({ id: DirectoryId; exists: false } & AllUndefined<Omit<DirectoryInfo, "id">>);
|
||||
|
||||
export type SubDirectoryInfo = Omit<LocalDirectoryInfo, "subDirectories" | "files">;
|
||||
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
@@ -34,17 +35,19 @@ export interface FileInfo {
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
lastModifiedAt: Date;
|
||||
categories: { id: number; name: string }[];
|
||||
categories: FileCategoryInfo[];
|
||||
}
|
||||
|
||||
export type SummarizedFileInfo = Omit<FileInfo, "parentId" | "contentIv" | "categories">;
|
||||
export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean };
|
||||
export type MaybeFileInfo =
|
||||
| (FileInfo & { exists: true })
|
||||
| ({ id: number; exists: false } & AllUndefined<Omit<FileInfo, "id">>);
|
||||
|
||||
export type SummarizedFileInfo = Omit<FileInfo, "contentIv" | "categories">;
|
||||
export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean };
|
||||
|
||||
interface LocalCategoryInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: DataKey;
|
||||
name: string;
|
||||
subCategories: SubCategoryInfo[];
|
||||
@@ -54,6 +57,7 @@ interface LocalCategoryInfo {
|
||||
|
||||
interface RootCategoryInfo {
|
||||
id: "root";
|
||||
parentId?: undefined;
|
||||
dataKey?: undefined;
|
||||
name?: undefined;
|
||||
subCategories: SubCategoryInfo[];
|
||||
@@ -62,10 +66,12 @@ interface RootCategoryInfo {
|
||||
}
|
||||
|
||||
export type CategoryInfo = LocalCategoryInfo | RootCategoryInfo;
|
||||
export type MaybeCategoryInfo =
|
||||
| (CategoryInfo & { exists: true })
|
||||
| ({ id: CategoryId; exists: false } & AllUndefined<Omit<CategoryInfo, "id">>);
|
||||
|
||||
export type SubCategoryInfo = Omit<
|
||||
LocalCategoryInfo,
|
||||
"subCategories" | "files" | "isFileRecursive"
|
||||
>;
|
||||
export type MaybeCategoryInfo =
|
||||
| (CategoryInfo & { exists: true })
|
||||
| ({ id: CategoryId; exists: false } & AllUndefined<Omit<CategoryInfo, "id">>);
|
||||
export type FileCategoryInfo = Omit<SubCategoryInfo, "dataKey">;
|
||||
|
||||
Reference in New Issue
Block a user