From f7c5144c625a0443e632cc82142ade654fee9181 Mon Sep 17 00:00:00 2001 From: Isaac Sanchez-Hawkins <266845420+isanchez404@users.noreply.github.com> Date: Tue, 12 May 2026 04:09:31 -0400 Subject: [PATCH] fix(sync): settle failed attachment reads (#1193) Co-authored-by: Isaac Sanchez --- packages/ui/src/sync/input-store.test.ts | 34 ++++++++++++++++++++++++ packages/ui/src/sync/input-store.ts | 27 ++++++++++++------- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/sync/input-store.test.ts b/packages/ui/src/sync/input-store.test.ts index 4fb4aa08..e5c1f55e 100644 --- a/packages/ui/src/sync/input-store.test.ts +++ b/packages/ui/src/sync/input-store.test.ts @@ -4,6 +4,9 @@ import { useInputStore } from "./input-store" class MockFileReader { result: string | ArrayBuffer | null = null onload: ((this: FileReader, event: ProgressEvent) => unknown) | null = null + onerror: ((this: FileReader, event: ProgressEvent) => unknown) | null = null + onabort: ((this: FileReader, event: ProgressEvent) => unknown) | null = null + error: DOMException | null = null readAsDataURL() { pendingReaders.push(this) @@ -17,6 +20,11 @@ const resolveReader = (reader: MockFileReader, result: string) => { reader.onload?.call(reader as unknown as FileReader, {} as ProgressEvent) } +const rejectReader = (reader: MockFileReader) => { + reader.error = new DOMException("read failed", "NotReadableError") + reader.onerror?.call(reader as unknown as FileReader, {} as ProgressEvent) +} + describe("input-store attachments", () => { beforeEach(() => { pendingReaders.length = 0 @@ -85,4 +93,30 @@ describe("input-store attachments", () => { expect(useInputStore.getState().attachedFiles).toEqual([]) }) + + test("does not leave local file reads pending after a reader error", async () => { + const addPromise = useInputStore.getState().addAttachedFile(new File(["hello"], "hello.txt", { type: "text/plain" })) + expect(pendingReaders).toHaveLength(1) + + rejectReader(pendingReaders[0]) + await addPromise + + expect(useInputStore.getState().attachedFiles).toEqual([]) + }) + + test("cleans up pending VS Code selection keys after a reader error", async () => { + const file = new File(["hello"], "hello.txt", { type: "text/plain" }) + const firstAdd = useInputStore.getState().addVSCodeSelectionAttachment("/workspace/hello.txt", file) + expect(pendingReaders).toHaveLength(1) + + rejectReader(pendingReaders[0]) + await firstAdd + + const secondAdd = useInputStore.getState().addVSCodeSelectionAttachment("/workspace/hello.txt", file) + expect(pendingReaders).toHaveLength(2) + resolveReader(pendingReaders[1], "data:text/plain;base64,aGVsbG8=") + await secondAdd + + expect(useInputStore.getState().attachedFiles.map((attached) => attached.filename)).toEqual(["hello.txt"]) + }) }) diff --git a/packages/ui/src/sync/input-store.ts b/packages/ui/src/sync/input-store.ts index aa58f158..bb10bed3 100644 --- a/packages/ui/src/sync/input-store.ts +++ b/packages/ui/src/sync/input-store.ts @@ -34,6 +34,14 @@ const toFileUrl = (filepath: string): string => { const getVSCodeSelectionKey = (path: string, filename: string): string => `${path}\u0000${filename}` +const readFileAsDataUrl = (file: File): Promise => new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error ?? new Error("Failed to read file")) + reader.onabort = () => reject(new Error("File read aborted")) + reader.readAsDataURL(file) +}) + const isSameVSCodeActiveEditorFile = (a: VSCodeActiveEditorFile | null, b: VSCodeActiveEditorFile | null): boolean => { if (a === b) return true if (!a || !b) return false @@ -110,11 +118,12 @@ export const useInputStore = create()((set, get) => ({ addAttachedFile: async (file: File) => { const id = `${Date.now()}-${Math.random().toString(36).slice(2)}` const generation = attachmentReadGeneration - const dataUrl = await new Promise((resolve) => { - const reader = new FileReader() - reader.onload = () => resolve(reader.result as string) - reader.readAsDataURL(file) - }) + let dataUrl: string + try { + dataUrl = await readFileAsDataUrl(file) + } catch { + return + } if (generation !== attachmentReadGeneration) return const attached: AttachedFile = { id, @@ -176,11 +185,9 @@ export const useInputStore = create()((set, get) => ({ pendingVSCodeSelectionKeys.add(selectionKey) let dataUrl: string try { - dataUrl = await new Promise((resolve) => { - const reader = new FileReader() - reader.onload = () => resolve(reader.result as string) - reader.readAsDataURL(file) - }) + dataUrl = await readFileAsDataUrl(file) + } catch { + return } finally { pendingVSCodeSelectionKeys.delete(selectionKey) }