From 6a25a63c72c8020f9592e5157427b7c06c3dc232 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 22 Mar 2026 18:02:58 +0530 Subject: [PATCH] fix(web): verify composer attachment persistence after flush --- apps/web/src/composerDraftStore.test.ts | 54 ++++++++++++++++++------- apps/web/src/composerDraftStore.ts | 14 ++++++- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index 98a6f1733..116a4fdcf 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -1,4 +1,3 @@ -import * as Schema from "effect/Schema"; import { ProjectId, ThreadId } from "@t3tools/contracts"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -7,7 +6,7 @@ import { type ComposerImageAttachment, useComposerDraftStore, } from "./composerDraftStore"; -import { removeLocalStorageItem, setLocalStorageItem } from "./hooks/useLocalStorage"; +import { removeLocalStorageItem } from "./hooks/useLocalStorage"; import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER, insertInlineTerminalContextPlaceholder, @@ -204,26 +203,49 @@ describe("composerDraftStore syncPersistedAttachments", () => { removeLocalStorageItem(COMPOSER_DRAFT_STORAGE_KEY); }); - it("treats malformed persisted draft storage as empty", async () => { + it("keeps attachments persisted after flushing the pending draft write", async () => { const image = makeImage({ id: "img-persisted", previewUrl: "blob:persisted", }); useComposerDraftStore.getState().addImage(threadId, image); - setLocalStorageItem( - COMPOSER_DRAFT_STORAGE_KEY, + + useComposerDraftStore.getState().syncPersistedAttachments(threadId, [ { - version: 2, - state: { - draftsByThreadId: { - [threadId]: { - attachments: "not-an-array", - }, - }, - }, + id: image.id, + name: image.name, + mimeType: image.mimeType, + sizeBytes: image.sizeBytes, + dataUrl: image.previewUrl, }, - Schema.Unknown, - ); + ]); + await Promise.resolve(); + + expect( + useComposerDraftStore.getState().draftsByThreadId[threadId]?.persistedAttachments, + ).toEqual([ + { + id: image.id, + name: image.name, + mimeType: image.mimeType, + sizeBytes: image.sizeBytes, + dataUrl: image.previewUrl, + }, + ]); + expect( + useComposerDraftStore.getState().draftsByThreadId[threadId]?.nonPersistedImageIds, + ).toEqual([]); + }); + + it("marks attachments non-persisted when flushing draft storage fails", async () => { + const image = makeImage({ + id: "img-persisted", + previewUrl: "blob:persisted", + }); + useComposerDraftStore.getState().addImage(threadId, image); + const setItemSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("Quota exceeded"); + }); useComposerDraftStore.getState().syncPersistedAttachments(threadId, [ { @@ -242,6 +264,8 @@ describe("composerDraftStore syncPersistedAttachments", () => { expect( useComposerDraftStore.getState().draftsByThreadId[threadId]?.nonPersistedImageIds, ).toEqual([image.id]); + + setItemSpy.mockRestore(); }); }); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index e1c3c0b5c..2714712ab 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -846,6 +846,15 @@ function readPersistedAttachmentIdsFromStorage(threadId: ThreadId): string[] { } } +function flushComposerDraftStorage(): boolean { + try { + composerDebouncedStorage.flush(); + return true; + } catch { + return false; + } +} + function hydreatePersistedComposerImageAttachment( attachment: PersistedComposerImageAttachment, ): File | null { @@ -1662,7 +1671,10 @@ export const useComposerDraftStore = create()( return { draftsByThreadId: nextDraftsByThreadId }; }); Promise.resolve().then(() => { - const persistedIdSet = new Set(readPersistedAttachmentIdsFromStorage(threadId)); + const didFlushPersistedDraft = flushComposerDraftStorage(); + const persistedIdSet = didFlushPersistedDraft + ? new Set(readPersistedAttachmentIdsFromStorage(threadId)) + : new Set(); set((state) => { const current = state.draftsByThreadId[threadId]; if (!current) {