mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 08:10:28 +09:00
업로드 및 썸네일 등록의 경쟁 조건을 수정하고 캐시 무효화와 스트리밍 캐시의 메모리 사용량을 개선
This commit is contained in:
1 parent
75e8ed6c77
commit
d986493431
9 files changed
+232
-34
No files matched your search
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { UploadRepo, db } from "#lib/server/db/index.ts";
|
||||
import { FileService, UploadService } from "#lib/server/services/index.ts";
|
||||
import { isChunkUploaded } from "#lib/server/services/upload.ts";
|
||||
import { hashFile } from "#lib/server/storage/index.ts";
|
||||
import { ciphertext, createUser, newEntryParams, resetDatabase } from "../helpers/database.ts";
|
||||
|
||||
beforeEach(resetDatabase);
|
||||
@@ -38,6 +39,61 @@ describe("upload session bitmap", () => {
|
||||
});
|
||||
|
||||
describe("upload completion", () => {
|
||||
it.each([false, true])(
|
||||
"rejects a late duplicate without changing the file (completed: %s)",
|
||||
async (completeBeforeDuplicate) => {
|
||||
const { userId } = await createUser();
|
||||
const { uploadId } = await UploadService.startFileUpload({
|
||||
...newEntryParams(userId, "root"),
|
||||
totalChunks: 1,
|
||||
hskVersion: null,
|
||||
contentType: "image/png",
|
||||
encCreatedAt: null,
|
||||
encLastModifiedAt: ciphertext(),
|
||||
});
|
||||
const session = (await UploadRepo.getUploadSession(db, uploadId, userId))!;
|
||||
const started = Promise.withResolvers<void>();
|
||||
const release = Promise.withResolvers<void>();
|
||||
const duplicate = UploadService.uploadChunk(
|
||||
userId,
|
||||
uploadId,
|
||||
1,
|
||||
Readable.from(
|
||||
(async function* () {
|
||||
started.resolve();
|
||||
await release.promise;
|
||||
yield Buffer.alloc(100, 2);
|
||||
})(),
|
||||
),
|
||||
);
|
||||
await started.promise;
|
||||
|
||||
try {
|
||||
const chunk = Buffer.alloc(100, 1);
|
||||
const hash = createHash("sha256").update(chunk).digest("base64");
|
||||
await UploadService.uploadChunk(userId, uploadId, 1, Readable.from([chunk]));
|
||||
if (completeBeforeDuplicate) {
|
||||
await UploadService.completeFileUpload(userId, uploadId, null, hash);
|
||||
}
|
||||
|
||||
const rejected = expect(duplicate).rejects.toMatchObject({
|
||||
code: completeBeforeDuplicate ? "NOT_FOUND" : "CONFLICT",
|
||||
});
|
||||
release.resolve();
|
||||
await rejected;
|
||||
expect(await hashFile(session.path)).toBe(hash);
|
||||
|
||||
if (!completeBeforeDuplicate) {
|
||||
await UploadService.completeFileUpload(userId, uploadId, null, hash);
|
||||
}
|
||||
expect(await FileService.getAllFileIds(userId)).toHaveLength(1);
|
||||
} finally {
|
||||
release.resolve();
|
||||
await duplicate.catch(() => {});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("registers the file only once when completed concurrently", async () => {
|
||||
const { userId } = await createUser();
|
||||
const { uploadId } = await UploadService.startFileUpload({
|
||||
|
||||
@@ -30,6 +30,92 @@ const createCache = (options?: {
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
describe("FilesystemCache", () => {
|
||||
it("revalidates again after invalidation during a pending request, coalescing repeated calls", async () => {
|
||||
const pending = Promise.withResolvers<Entry>();
|
||||
const fetchFromServer = vi
|
||||
.fn<(id: number) => Promise<Entry>>()
|
||||
.mockImplementationOnce(() => pending.promise)
|
||||
.mockResolvedValue({ id: 1, name: "new" });
|
||||
const cache = new FilesystemCache<number, Entry>({
|
||||
fetchFromIndexedDB: async () => undefined,
|
||||
fetchFromServer,
|
||||
});
|
||||
const loading = cache.get(1);
|
||||
await vi.waitFor(() => expect(fetchFromServer).toHaveBeenCalledTimes(1));
|
||||
|
||||
cache.invalidate(1);
|
||||
cache.invalidate(1);
|
||||
cache.invalidate(1);
|
||||
pending.resolve({ id: 1, name: "old" });
|
||||
const entry = await loading;
|
||||
await vi.waitFor(() => expect(entry.name).toBe("new"));
|
||||
expect(fetchFromServer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("revalidates only invalidated entries after a pending bulk request", async () => {
|
||||
const pending = Promise.withResolvers<Map<number, Entry>>();
|
||||
const fetchFromServer = vi.fn(async (id: number) => ({ id, name: "new" }));
|
||||
const bulkFetchFromServer = vi.fn(() => pending.promise);
|
||||
const cache = new FilesystemCache<number, Entry>({
|
||||
fetchFromIndexedDB: async () => undefined,
|
||||
fetchFromServer,
|
||||
bulkFetchFromIndexedDB: async () => new Map(),
|
||||
bulkFetchFromServer,
|
||||
});
|
||||
const loading = cache.bulkGet(new Set([1, 2]));
|
||||
await vi.waitFor(() => expect(bulkFetchFromServer).toHaveBeenCalledTimes(1));
|
||||
cache.invalidate(1);
|
||||
pending.resolve(new Map([1, 2].map((id) => [id, { id, name: "old" }])));
|
||||
|
||||
const entries = await loading;
|
||||
await vi.waitFor(() => expect(entries.get(1)?.name).toBe("new"));
|
||||
expect(entries.get(2)?.name).toBe("old");
|
||||
expect(fetchFromServer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("preserves invalidation when a pending request fails", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const pending = Promise.withResolvers<Entry>();
|
||||
const fetchFromServer = vi
|
||||
.fn<() => Promise<Entry>>()
|
||||
.mockImplementationOnce(() => pending.promise)
|
||||
.mockResolvedValue({ id: 1, name: "new" });
|
||||
const cache = new FilesystemCache<number, Entry>({
|
||||
fetchFromIndexedDB: async () => ({ id: 1, name: "local" }),
|
||||
fetchFromServer,
|
||||
});
|
||||
const entry = await cache.get(1);
|
||||
await vi.waitFor(() => expect(fetchFromServer).toHaveBeenCalledTimes(1));
|
||||
cache.invalidate(1);
|
||||
pending.reject(new Error("Failed"));
|
||||
|
||||
await vi.waitFor(() => expect(entry.name).toBe("new"));
|
||||
expect(fetchFromServer).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("doesn't revalidate evicted entries after a pending bulk request", async () => {
|
||||
const pending = Promise.withResolvers<Map<number, Entry>>();
|
||||
const fetchFromServer = vi.fn(async (id: number) => ({ id, name: "new" }));
|
||||
const bulkFetchFromServer = vi.fn(() => pending.promise);
|
||||
const cache = new FilesystemCache<number, Entry>({
|
||||
fetchFromIndexedDB: async () => undefined,
|
||||
fetchFromServer,
|
||||
bulkFetchFromIndexedDB: async () => new Map(),
|
||||
bulkFetchFromServer,
|
||||
});
|
||||
const loading = cache.bulkGet(new Set([1]));
|
||||
await vi.waitFor(() => expect(bulkFetchFromServer).toHaveBeenCalledTimes(1));
|
||||
cache.invalidate(1);
|
||||
cache.evict(1);
|
||||
const replacement = await cache.get(1);
|
||||
pending.resolve(new Map([[1, { id: 1, name: "old" }]]));
|
||||
await loading;
|
||||
await flush();
|
||||
|
||||
expect(replacement.name).toBe("new");
|
||||
expect(fetchFromServer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resolves with the local value first and updates it in place", async () => {
|
||||
const { cache } = createCache({ local: { 1: "local" }, server: { 1: "server" } });
|
||||
|
||||
|
||||
Reference in new issue
Block a user