fix(web): preserve drafts during memo creation
This commit is contained in:
1 parent
c674719e80
commit
2e6cc5d71c
2 files changed
+119
-1
No files matched your search
@@ -6,8 +6,10 @@ import type { MemoUpdateSyncPayload } from "../apps/web/src/lib/local-db";
|
||||
const { localDb } = await import("../apps/web/src/lib/local-db");
|
||||
const {
|
||||
discardWebMemoConflict,
|
||||
getMemoCreateQueueId,
|
||||
getMemoUpdateQueueId,
|
||||
isMemoUpdateAlreadyApplied,
|
||||
queueMemoCreate,
|
||||
queueMemoUpdate,
|
||||
syncQueuedChanges,
|
||||
} = await import("../apps/web/src/lib/sync-queue");
|
||||
@@ -83,6 +85,7 @@ describe("web sync queue concurrency", () => {
|
||||
|
||||
expect(queued).toBeDefined();
|
||||
expect(isMemoUpdateAlreadyApplied(memo("already saved"), queued!)).toBe(true);
|
||||
expect(isMemoUpdateAlreadyApplied(memo("already saved", 0), queued!)).toBe(false);
|
||||
expect(isMemoUpdateAlreadyApplied(memo("different remote text"), queued!)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -141,6 +144,69 @@ describe("web sync queue concurrency", () => {
|
||||
expect(queued?.payload.expectedContentHash).toBe("hash-1");
|
||||
});
|
||||
|
||||
test("queues a draft saved while memo creation is in flight after remapping its id", async () => {
|
||||
const scope = "test-scope";
|
||||
const temporaryId = "local_create_race";
|
||||
const remoteMemo = { ...memo("", 1), id: "memo_created", title: null };
|
||||
const draftTitle = "written during create";
|
||||
const draftContent = payload(draftTitle).contentJson;
|
||||
await queueMemoCreate(scope, {
|
||||
temporaryId,
|
||||
notebookId: "nb_inbox",
|
||||
title: "",
|
||||
contentMarkdown: "",
|
||||
tags: [],
|
||||
createdAt: "2026-07-15T00:00:00.000Z",
|
||||
updatedAt: "2026-07-15T00:00:00.000Z",
|
||||
});
|
||||
await localDb.drafts.put({
|
||||
memoId: temporaryId,
|
||||
title: draftTitle,
|
||||
contentJson: draftContent,
|
||||
tagsText: "",
|
||||
updatedAt: "2026-07-15T00:00:02.000Z",
|
||||
});
|
||||
|
||||
const requests: Array<{ method: string; url: string }> = [];
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method ?? "GET";
|
||||
requests.push({ method, url });
|
||||
if (method === "POST" && url.endsWith("/api/v1/memos")) {
|
||||
return jsonResponse({ memo: remoteMemo }, 201);
|
||||
}
|
||||
if (url.endsWith("/edit-sessions")) {
|
||||
return jsonResponse({ editSession: { ...session(1), memoId: remoteMemo.id } });
|
||||
}
|
||||
return jsonResponse({ memo: { ...remoteMemo, title: draftTitle, contentJson: draftContent, revision: 2, contentHash: "hash-2" } });
|
||||
}) as typeof fetch;
|
||||
|
||||
const createResult = await syncQueuedChanges({ scope });
|
||||
const queuedUpdate = await localDb.syncQueue.get(getMemoUpdateQueueId(remoteMemo.id));
|
||||
|
||||
expect(createResult).toEqual({ attempted: 1, synced: 1, failed: 0, conflicted: 0 });
|
||||
expect(await localDb.syncQueue.get(getMemoCreateQueueId(temporaryId))).toBeUndefined();
|
||||
expect(await localDb.drafts.get(temporaryId)).toBeUndefined();
|
||||
expect(await localDb.drafts.get(remoteMemo.id)).toMatchObject({ title: draftTitle, contentJson: draftContent });
|
||||
expect(queuedUpdate).toMatchObject({
|
||||
kind: "memo.update",
|
||||
scope,
|
||||
memoId: remoteMemo.id,
|
||||
status: "pending",
|
||||
payload: {
|
||||
expectedRevision: 1,
|
||||
expectedContentHash: "hash-1",
|
||||
title: draftTitle,
|
||||
contentJson: draftContent,
|
||||
},
|
||||
});
|
||||
|
||||
const updateResult = await syncQueuedChanges({ scope });
|
||||
expect(updateResult).toEqual({ attempted: 1, synced: 1, failed: 0, conflicted: 0 });
|
||||
expect(requests.some(({ method, url }) => method === "PATCH" && url.endsWith(`/api/v1/memos/${remoteMemo.id}`))).toBe(true);
|
||||
expect(await localDb.syncQueue.get(getMemoUpdateQueueId(remoteMemo.id))).toBeUndefined();
|
||||
});
|
||||
|
||||
test("keeps a genuine server revision mismatch as a conflict", async () => {
|
||||
await queueMemoUpdate(payload("offline edit"));
|
||||
globalThis.fetch = (async () => jsonResponse({ editSession: session(1) })) as typeof fetch;
|
||||
|
||||
Reference in new issue
Block a user