| ... | @@ -75,6 +75,7 @@ import { | ... | @@ -75,6 +75,7 @@ import { |
| 75 | type ComposerFileAttachment, | 75 | type ComposerFileAttachment, |
| 76 | type ComposerImageAttachment, | 76 | type ComposerImageAttachment, |
| 77 | composerFileNeedsReattach, | 77 | composerFileNeedsReattach, |
| | 78 | partializeComposerDraftStoreState, |
| 78 | useComposerDraftStore, | 79 | useComposerDraftStore, |
| 79 | DraftId, | 80 | DraftId, |
| 80 | } from "./composerDraftStore"; | 81 | } from "./composerDraftStore"; |
| ... | @@ -84,7 +85,7 @@ import { | ... | @@ -84,7 +85,7 @@ import { |
| 84 | insertInlineTerminalContextPlaceholder, | 85 | insertInlineTerminalContextPlaceholder, |
| 85 | type TerminalContextDraft, | 86 | type TerminalContextDraft, |
| 86 | } from "./lib/terminalContext"; | 87 | } from "./lib/terminalContext"; |
| 87 | import { createDebouncedStorage } from "./lib/storage"; | 88 | import { createDeferredStorage } from "./lib/storage"; |
| 88 | | 89 | |
| 89 | function makeImage(input: { | 90 | function makeImage(input: { |
| 90 | id: string; | 91 | id: string; |
| ... | @@ -196,37 +197,42 @@ describe("composerDraftStore assistant citations", () => { | ... | @@ -196,37 +197,42 @@ describe("composerDraftStore assistant citations", () => { |
| 196 | beforeEach(resetComposerDraftStore); | 197 | beforeEach(resetComposerDraftStore); |
| 197 | afterEach(resetComposerDraftStore); | 198 | afterEach(resetComposerDraftStore); |
| 198 | | 199 | |
| 199 | it("keeps quotes, comments, and remote source IDs through persistence and removes them on clear", () => { | 200 | it("keeps quotes, comments, and remote source IDs through persistence and removes them on clear", async () => { |
| 200 | const threadId = ThreadId.make("citation-draft"); | 201 | await useComposerDraftStore.persist.clearStorage(); |
| 201 | const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId); | 202 | vi.useFakeTimers(); |
| 202 | const citation = { | 203 | try { |
| 203 | version: 1 as const, | 204 | const threadId = ThreadId.make("citation-draft"); |
| 204 | environmentId: EnvironmentId.make("remote-source"), | 205 | const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId); |
| 205 | threadId: ThreadId.make("source-thread"), | 206 | const citation = { |
| 206 | messageId: MessageId.make("source-message"), | 207 | version: 1 as const, |
| 207 | text: "Preserve the selected text after reload.", | 208 | environmentId: EnvironmentId.make("remote-source"), |
| 208 | comment: "Why is this important?\nPlease show an example.", | 209 | threadId: ThreadId.make("source-thread"), |
| 209 | start: 12, | 210 | messageId: MessageId.make("source-message"), |
| 210 | end: 52, | 211 | text: "Preserve the selected text after reload.", |
| 211 | prefix: "Before. ", | 212 | comment: "Why is this important?\nPlease show an example.", |
| 212 | suffix: " After.", | 213 | start: 12, |
| 213 | }; | 214 | end: 52, |
| 214 | const prompt = `Explain ${serializeAssistantCitation(citation)} further.`; | 215 | prefix: "Before. ", |
| 215 | useComposerDraftStore.getState().setPrompt(threadRef, prompt); | 216 | suffix: " After.", |
| 216 | const options = useComposerDraftStore.persist.getOptions(); | 217 | }; |
| 217 | const saved = JSON.parse( | 218 | const prompt = `Explain ${serializeAssistantCitation(citation)} further.`; |
| 218 | JSON.stringify(options.partialize!(useComposerDraftStore.getState())), | 219 | useComposerDraftStore.getState().setPrompt(threadRef, prompt); |
| 219 | ) as unknown; | 220 | await vi.advanceTimersByTimeAsync(300); |
| 220 | resetComposerDraftStore(); | 221 | resetComposerDraftStore(); |
| 221 | const hydrated = options.merge!(saved, useComposerDraftStore.getState()); | 222 | await useComposerDraftStore.persist.rehydrate(); |
| 222 | useComposerDraftStore.setState(hydrated); | 223 | const restored = draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt ?? ""; |
| 223 | const restored = draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt ?? ""; | 224 | expect(restored).toBe(prompt); |
| 224 | expect(restored).toBe(prompt); | 225 | expect(collectAssistantCitations(restored).map((entry) => entry.citation)).toEqual([ |
| 225 | expect(collectAssistantCitations(restored).map((entry) => entry.citation)).toEqual([citation]); | 226 | citation, |
| 226 | useComposerDraftStore.getState().clearComposerContent(threadRef); | 227 | ]); |
| 227 | expect( | 228 | useComposerDraftStore.getState().clearComposerContent(threadRef); |
| 228 | collectAssistantCitations(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt ?? ""), | 229 | expect( |
| 229 | ).toEqual([]); | 230 | collectAssistantCitations(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt ?? ""), |
| | 231 | ).toEqual([]); |
| | 232 | } finally { |
| | 233 | await useComposerDraftStore.persist.clearStorage(); |
| | 234 | vi.useRealTimers(); |
| | 235 | } |
| 230 | }); | 236 | }); |
| 231 | }); | 237 | }); |
| 232 | | 238 | |
| ... | @@ -388,7 +394,6 @@ describe("composerDraftStore file attachments", () => { | ... | @@ -388,7 +394,6 @@ describe("composerDraftStore file attachments", () => { |
| 388 | | 394 | |
| 389 | const persistApi = useComposerDraftStore.persist as unknown as { | 395 | const persistApi = useComposerDraftStore.persist as unknown as { |
| 390 | getOptions: () => { | 396 | getOptions: () => { |
| 391 | partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown; | | |
| 392 | merge: ( | 397 | merge: ( |
| 393 | persistedState: unknown, | 398 | persistedState: unknown, |
| 394 | currentState: ReturnType<typeof useComposerDraftStore.getState>, | 399 | currentState: ReturnType<typeof useComposerDraftStore.getState>, |
| ... | @@ -396,9 +401,7 @@ describe("composerDraftStore file attachments", () => { | ... | @@ -396,9 +401,7 @@ describe("composerDraftStore file attachments", () => { |
| 396 | }; | 401 | }; |
| 397 | }; | 402 | }; |
| 398 | const options = persistApi.getOptions(); | 403 | const options = persistApi.getOptions(); |
| 399 | const persisted = options.partialize(useComposerDraftStore.getState()) as { | 404 | const persisted = partializeComposerDraftStoreState(useComposerDraftStore.getState()); |
| 400 | draftsByThreadKey: Record<string, { files?: Array<Record<string, unknown>> }>; | | |
| 401 | }; | | |
| 402 | | 405 | |
| 403 | expect(persisted.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files).toEqual( | 406 | expect(persisted.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files).toEqual( |
| 404 | [ | 407 | [ |
| ... | @@ -436,7 +439,6 @@ describe("composerDraftStore file attachments", () => { | ... | @@ -436,7 +439,6 @@ describe("composerDraftStore file attachments", () => { |
| 436 | | 439 | |
| 437 | const persistApi = useComposerDraftStore.persist as unknown as { | 440 | const persistApi = useComposerDraftStore.persist as unknown as { |
| 438 | getOptions: () => { | 441 | getOptions: () => { |
| 439 | partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown; | | |
| 440 | merge: ( | 442 | merge: ( |
| 441 | persistedState: unknown, | 443 | persistedState: unknown, |
| 442 | currentState: ReturnType<typeof useComposerDraftStore.getState>, | 444 | currentState: ReturnType<typeof useComposerDraftStore.getState>, |
| ... | @@ -444,9 +446,7 @@ describe("composerDraftStore file attachments", () => { | ... | @@ -444,9 +446,7 @@ describe("composerDraftStore file attachments", () => { |
| 444 | }; | 446 | }; |
| 445 | }; | 447 | }; |
| 446 | const options = persistApi.getOptions(); | 448 | const options = persistApi.getOptions(); |
| 447 | const persisted = options.partialize(useComposerDraftStore.getState()) as { | 449 | const persisted = partializeComposerDraftStoreState(useComposerDraftStore.getState()); |
| 448 | draftsByThreadKey: Record<string, { files?: Array<Record<string, unknown>> }>; | | |
| 449 | }; | | |
| 450 | | 450 | |
| 451 | expect(persisted.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files).toEqual( | 451 | expect(persisted.draftsByThreadKey[threadKeyFor(threadId, TEST_ENVIRONMENT_ID)]?.files).toEqual( |
| 452 | [ | 452 | [ |
| ... | @@ -835,12 +835,9 @@ describe("composerDraftStore terminal contexts", () => { | ... | @@ -835,12 +835,9 @@ describe("composerDraftStore terminal contexts", () => { |
| 835 | .getState() | 835 | .getState() |
| 836 | .addTerminalContext(threadRef, makeTerminalContext({ id: "ctx-persist" })); | 836 | .addTerminalContext(threadRef, makeTerminalContext({ id: "ctx-persist" })); |
| 837 | | 837 | |
| 838 | const persistApi = useComposerDraftStore.persist as unknown as { | 838 | const persistedState = partializeComposerDraftStoreState( |
| 839 | getOptions: () => { | 839 | useComposerDraftStore.getState(), |
| 840 | partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown; | 840 | ) as unknown as { |
| 841 | }; | | |
| 842 | }; | | |
| 843 | const persistedState = persistApi.getOptions().partialize(useComposerDraftStore.getState()) as { | | |
| 844 | draftsByThreadKey?: Record<string, { terminalContexts?: Array<Record<string, unknown>> }>; | 841 | draftsByThreadKey?: Record<string, { terminalContexts?: Array<Record<string, unknown>> }>; |
| 845 | }; | 842 | }; |
| 846 | | 843 | |
| ... | @@ -1009,12 +1006,9 @@ describe("composerDraftStore element contexts", () => { | ... | @@ -1009,12 +1006,9 @@ describe("composerDraftStore element contexts", () => { |
| 1009 | | 1006 | |
| 1010 | it("persists element contexts via the partializer (round-trippable)", () => { | 1007 | it("persists element contexts via the partializer (round-trippable)", () => { |
| 1011 | useComposerDraftStore.getState().addElementContext(threadRef, baseSelection); | 1008 | useComposerDraftStore.getState().addElementContext(threadRef, baseSelection); |
| 1012 | const persistApi = useComposerDraftStore.persist as unknown as { | 1009 | const persisted = partializeComposerDraftStoreState( |
| 1013 | getOptions: () => { | 1010 | useComposerDraftStore.getState(), |
| 1014 | partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown; | 1011 | ) as unknown as { |
| 1015 | }; | | |
| 1016 | }; | | |
| 1017 | const persisted = persistApi.getOptions().partialize(useComposerDraftStore.getState()) as { | | |
| 1018 | draftsByThreadKey?: Record<string, { elementContexts?: Array<Record<string, unknown>> }>; | 1012 | draftsByThreadKey?: Record<string, { elementContexts?: Array<Record<string, unknown>> }>; |
| 1019 | }; | 1013 | }; |
| 1020 | const entry = | 1014 | const entry = |
| ... | @@ -1067,12 +1061,9 @@ describe("composerDraftStore review comments", () => { | ... | @@ -1067,12 +1061,9 @@ describe("composerDraftStore review comments", () => { |
| 1067 | it("persists review comments and clears them with composer content", () => { | 1061 | it("persists review comments and clears them with composer content", () => { |
| 1068 | const store = useComposerDraftStore.getState(); | 1062 | const store = useComposerDraftStore.getState(); |
| 1069 | store.addReviewComment(threadRef, comment); | 1063 | store.addReviewComment(threadRef, comment); |
| 1070 | const persistApi = useComposerDraftStore.persist as unknown as { | 1064 | const persisted = partializeComposerDraftStoreState( |
| 1071 | getOptions: () => { | 1065 | useComposerDraftStore.getState(), |
| 1072 | partialize: (state: ReturnType<typeof useComposerDraftStore.getState>) => unknown; | 1066 | ) as unknown as { |
| 1073 | }; | | |
| 1074 | }; | | |
| 1075 | const persisted = persistApi.getOptions().partialize(useComposerDraftStore.getState()) as { | | |
| 1076 | draftsByThreadKey?: Record<string, { reviewComments?: Array<Record<string, unknown>> }>; | 1067 | draftsByThreadKey?: Record<string, { reviewComments?: Array<Record<string, unknown>> }>; |
| 1077 | }; | 1068 | }; |
| 1078 | | 1069 | |
| ... | @@ -2517,7 +2508,7 @@ describe("composerDraftStore runtime and interaction settings", () => { | ... | @@ -2517,7 +2508,7 @@ describe("composerDraftStore runtime and interaction settings", () => { |
| 2517 | }); | 2508 | }); |
| 2518 | | 2509 | |
| 2519 | // --------------------------------------------------------------------------- | 2510 | // --------------------------------------------------------------------------- |
| 2520 | // createDebouncedStorage | 2511 | // createDeferredStorage |
| 2521 | // --------------------------------------------------------------------------- | 2512 | // --------------------------------------------------------------------------- |
| 2522 | | 2513 | |
| 2523 | function createMockStorage() { | 2514 | function createMockStorage() { |
| ... | @@ -2533,9 +2524,75 @@ function createMockStorage() { | ... | @@ -2533,9 +2524,75 @@ function createMockStorage() { |
| 2533 | }; | 2524 | }; |
| 2534 | } | 2525 | } |
| 2535 | | 2526 | |
| 2536 | describe("createDebouncedStorage", () => { | 2527 | describe("composer draft persistence", () => { |
| | 2528 | it("defers attachment reads and serialization until typing stops, then restores the last draft", async () => { |
| | 2529 | await useComposerDraftStore.persist.clearStorage(); |
| | 2530 | vi.useFakeTimers(); |
| | 2531 | const stringify = vi.spyOn(JSON, "stringify"); |
| | 2532 | try { |
| | 2533 | resetComposerDraftStore(); |
| | 2534 | const heavyThreadId = ThreadId.make("heavy-draft"); |
| | 2535 | const typingThreadId = ThreadId.make("typing-draft"); |
| | 2536 | const heavyRef = scopeThreadRef(TEST_ENVIRONMENT_ID, heavyThreadId); |
| | 2537 | const typingRef = scopeThreadRef(TEST_ENVIRONMENT_ID, typingThreadId); |
| | 2538 | const heavyKey = scopedThreadKey(heavyRef); |
| | 2539 | useComposerDraftStore.getState().setPrompt(heavyRef, "Keep this image"); |
| | 2540 | const attachments = [ |
| | 2541 | { |
| | 2542 | id: "heavy-image", |
| | 2543 | name: "image.png", |
| | 2544 | mimeType: "image/png", |
| | 2545 | sizeBytes: 49_152, |
| | 2546 | dataUrl: `data:image/png;base64,${"AQID".repeat(16_384)}`, |
| | 2547 | }, |
| | 2548 | ]; |
| | 2549 | let attachmentReads = 0; |
| | 2550 | useComposerDraftStore.setState((state) => ({ |
| | 2551 | draftsByThreadKey: { |
| | 2552 | ...state.draftsByThreadKey, |
| | 2553 | [heavyKey]: { |
| | 2554 | ...state.draftsByThreadKey[heavyKey]!, |
| | 2555 | get persistedAttachments() { |
| | 2556 | attachmentReads += 1; |
| | 2557 | return attachments; |
| | 2558 | }, |
| | 2559 | }, |
| | 2560 | }, |
| | 2561 | })); |
| | 2562 | attachmentReads = 0; |
| | 2563 | stringify.mockClear(); |
| | 2564 | |
| | 2565 | for (let index = 1; index <= 20; index++) { |
| | 2566 | useComposerDraftStore.getState().setPrompt(typingRef, `Draft ${index}`); |
| | 2567 | } |
| | 2568 | expect(attachmentReads).toBe(0); |
| | 2569 | expect(stringify).not.toHaveBeenCalled(); |
| | 2570 | |
| | 2571 | await vi.advanceTimersByTimeAsync(300); |
| | 2572 | expect(attachmentReads).toBe(1); |
| | 2573 | expect(stringify).toHaveBeenCalledTimes(1); |
| | 2574 | |
| | 2575 | resetComposerDraftStore(); |
| | 2576 | await useComposerDraftStore.persist.rehydrate(); |
| | 2577 | expect(draftFor(typingThreadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("Draft 20"); |
| | 2578 | expect(draftFor(heavyThreadId, TEST_ENVIRONMENT_ID)?.persistedAttachments).toEqual( |
| | 2579 | attachments, |
| | 2580 | ); |
| | 2581 | } finally { |
| | 2582 | stringify.mockRestore(); |
| | 2583 | await useComposerDraftStore.persist.clearStorage(); |
| | 2584 | vi.useRealTimers(); |
| | 2585 | resetComposerDraftStore(); |
| | 2586 | } |
| | 2587 | }); |
| | 2588 | }); |
| | 2589 | |
| | 2590 | describe("createDeferredStorage", () => { |
| | 2591 | const serialize = vi.fn((value: string) => `s:${value}`); |
| | 2592 | |
| 2537 | beforeEach(() => { | 2593 | beforeEach(() => { |
| 2538 | vi.useFakeTimers(); | 2594 | vi.useFakeTimers(); |
| | 2595 | serialize.mockClear(); |
| 2539 | }); | 2596 | }); |
| 2540 | | 2597 | |
| 2541 | afterEach(() => { | 2598 | afterEach(() => { |
| ... | @@ -2545,69 +2602,74 @@ describe("createDebouncedStorage", () => { | ... | @@ -2545,69 +2602,74 @@ describe("createDebouncedStorage", () => { |
| 2545 | it("delegates getItem immediately", () => { | 2602 | it("delegates getItem immediately", () => { |
| 2546 | const base = createMockStorage(); | 2603 | const base = createMockStorage(); |
| 2547 | base.getItem.mockReturnValueOnce("value"); | 2604 | base.getItem.mockReturnValueOnce("value"); |
| 2548 | const storage = createDebouncedStorage(base); | 2605 | const storage = createDeferredStorage(base, serialize); |
| 2549 | | 2606 | |
| 2550 | expect(storage.getItem("key")).toBe("value"); | 2607 | expect(storage.getItem("key")).toBe("value"); |
| 2551 | expect(base.getItem).toHaveBeenCalledWith("key"); | 2608 | expect(base.getItem).toHaveBeenCalledWith("key"); |
| 2552 | }); | 2609 | }); |
| 2553 | | 2610 | |
| 2554 | it("does not write to base storage until the debounce fires", () => { | 2611 | it("neither serializes nor writes until the debounce fires", () => { |
| 2555 | const base = createMockStorage(); | 2612 | const base = createMockStorage(); |
| 2556 | const storage = createDebouncedStorage(base); | 2613 | const storage = createDeferredStorage(base, serialize); |
| 2557 | | 2614 | |
| 2558 | storage.setItem("key", "v1"); | 2615 | storage.setItem("key", "v1"); |
| | 2616 | expect(serialize).not.toHaveBeenCalled(); |
| 2559 | expect(base.setItem).not.toHaveBeenCalled(); | 2617 | expect(base.setItem).not.toHaveBeenCalled(); |
| 2560 | | 2618 | |
| 2561 | vi.advanceTimersByTime(299); | 2619 | vi.advanceTimersByTime(299); |
| | 2620 | expect(serialize).not.toHaveBeenCalled(); |
| 2562 | expect(base.setItem).not.toHaveBeenCalled(); | 2621 | expect(base.setItem).not.toHaveBeenCalled(); |
| 2563 | | 2622 | |
| 2564 | vi.advanceTimersByTime(1); | 2623 | vi.advanceTimersByTime(1); |
| 2565 | expect(base.setItem).toHaveBeenCalledWith("key", "v1"); | 2624 | expect(base.setItem).toHaveBeenCalledWith("key", "s:v1"); |
| 2566 | }); | 2625 | }); |
| 2567 | | 2626 | |
| 2568 | it("only writes the last value when setItem is called rapidly", () => { | 2627 | it("serializes and writes only the last value when setItem is called rapidly", () => { |
| 2569 | const base = createMockStorage(); | 2628 | const base = createMockStorage(); |
| 2570 | const storage = createDebouncedStorage(base); | 2629 | const storage = createDeferredStorage(base, serialize); |
| 2571 | | 2630 | |
| 2572 | storage.setItem("key", "v1"); | 2631 | storage.setItem("key", "v1"); |
| 2573 | storage.setItem("key", "v2"); | 2632 | storage.setItem("key", "v2"); |
| 2574 | storage.setItem("key", "v3"); | 2633 | storage.setItem("key", "v3"); |
| 2575 | | 2634 | |
| 2576 | vi.advanceTimersByTime(300); | 2635 | vi.advanceTimersByTime(300); |
| | 2636 | expect(serialize).toHaveBeenCalledTimes(1); |
| 2577 | expect(base.setItem).toHaveBeenCalledTimes(1); | 2637 | expect(base.setItem).toHaveBeenCalledTimes(1); |
| 2578 | expect(base.setItem).toHaveBeenCalledWith("key", "v3"); | 2638 | expect(base.setItem).toHaveBeenCalledWith("key", "s:v3"); |
| 2579 | }); | 2639 | }); |
| 2580 | | 2640 | |
| 2581 | it("removeItem cancels a pending setItem write", () => { | 2641 | it("removeItem cancels a pending setItem write", () => { |
| 2582 | const base = createMockStorage(); | 2642 | const base = createMockStorage(); |
| 2583 | const storage = createDebouncedStorage(base); | 2643 | const storage = createDeferredStorage(base, serialize); |
| 2584 | | 2644 | |
| 2585 | storage.setItem("key", "v1"); | 2645 | storage.setItem("key", "v1"); |
| 2586 | storage.removeItem("key"); | 2646 | storage.removeItem("key"); |
| 2587 | | 2647 | |
| 2588 | vi.advanceTimersByTime(300); | 2648 | vi.advanceTimersByTime(300); |
| | 2649 | expect(serialize).not.toHaveBeenCalled(); |
| 2589 | expect(base.setItem).not.toHaveBeenCalled(); | 2650 | expect(base.setItem).not.toHaveBeenCalled(); |
| 2590 | expect(base.removeItem).toHaveBeenCalledWith("key"); | 2651 | expect(base.removeItem).toHaveBeenCalledWith("key"); |
| 2591 | }); | 2652 | }); |
| 2592 | | 2653 | |
| 2593 | it("flush writes the pending value immediately", () => { | 2654 | it("flush serializes and writes the pending value immediately", () => { |
| 2594 | const base = createMockStorage(); | 2655 | const base = createMockStorage(); |
| 2595 | const storage = createDebouncedStorage(base); | 2656 | const storage = createDeferredStorage(base, serialize); |
| 2596 | | 2657 | |
| 2597 | storage.setItem("key", "v1"); | 2658 | storage.setItem("key", "v1"); |
| 2598 | expect(base.setItem).not.toHaveBeenCalled(); | 2659 | expect(base.setItem).not.toHaveBeenCalled(); |
| 2599 | | 2660 | |
| 2600 | storage.flush(); | 2661 | storage.flush(); |
| 2601 | expect(base.setItem).toHaveBeenCalledWith("key", "v1"); | 2662 | expect(base.setItem).toHaveBeenCalledWith("key", "s:v1"); |
| 2602 | | 2663 | |
| 2603 | // Timer should be cancelled; no duplicate write. | 2664 | // Timer should be cancelled; no duplicate write. |
| 2604 | vi.advanceTimersByTime(300); | 2665 | vi.advanceTimersByTime(300); |
| | 2666 | expect(serialize).toHaveBeenCalledTimes(1); |
| 2605 | expect(base.setItem).toHaveBeenCalledTimes(1); | 2667 | expect(base.setItem).toHaveBeenCalledTimes(1); |
| 2606 | }); | 2668 | }); |
| 2607 | | 2669 | |
| 2608 | it("flush is a no-op when nothing is pending", () => { | 2670 | it("flush is a no-op when nothing is pending", () => { |
| 2609 | const base = createMockStorage(); | 2671 | const base = createMockStorage(); |
| 2610 | const storage = createDebouncedStorage(base); | 2672 | const storage = createDeferredStorage(base, serialize); |
| 2611 | | 2673 | |
| 2612 | storage.flush(); | 2674 | storage.flush(); |
| 2613 | expect(base.setItem).not.toHaveBeenCalled(); | 2675 | expect(base.setItem).not.toHaveBeenCalled(); |
| ... | @@ -2615,7 +2677,7 @@ describe("createDebouncedStorage", () => { | ... | @@ -2615,7 +2677,7 @@ describe("createDebouncedStorage", () => { |
| 2615 | | 2677 | |
| 2616 | it("flush after removeItem is a no-op", () => { | 2678 | it("flush after removeItem is a no-op", () => { |
| 2617 | const base = createMockStorage(); | 2679 | const base = createMockStorage(); |
| 2618 | const storage = createDebouncedStorage(base); | 2680 | const storage = createDeferredStorage(base, serialize); |
| 2619 | | 2681 | |
| 2620 | storage.setItem("key", "v1"); | 2682 | storage.setItem("key", "v1"); |
| 2621 | storage.removeItem("key"); | 2683 | storage.removeItem("key"); |
| ... | @@ -2626,7 +2688,7 @@ describe("createDebouncedStorage", () => { | ... | @@ -2626,7 +2688,7 @@ describe("createDebouncedStorage", () => { |
| 2626 | | 2688 | |
| 2627 | it("setItem works normally after removeItem cancels a pending write", () => { | 2689 | it("setItem works normally after removeItem cancels a pending write", () => { |
| 2628 | const base = createMockStorage(); | 2690 | const base = createMockStorage(); |
| 2629 | const storage = createDebouncedStorage(base); | 2691 | const storage = createDeferredStorage(base, serialize); |
| 2630 | | 2692 | |
| 2631 | storage.setItem("key", "v1"); | 2693 | storage.setItem("key", "v1"); |
| 2632 | storage.removeItem("key"); | 2694 | storage.removeItem("key"); |
| ... | @@ -2634,6 +2696,6 @@ describe("createDebouncedStorage", () => { | ... | @@ -2634,6 +2696,6 @@ describe("createDebouncedStorage", () => { |
| 2634 | | 2696 | |
| 2635 | vi.advanceTimersByTime(300); | 2697 | vi.advanceTimersByTime(300); |
| 2636 | expect(base.setItem).toHaveBeenCalledTimes(1); | 2698 | expect(base.setItem).toHaveBeenCalledTimes(1); |
| 2637 | expect(base.setItem).toHaveBeenCalledWith("key", "v2"); | 2699 | expect(base.setItem).toHaveBeenCalledWith("key", "s:v2"); |
| 2638 | }); | 2700 | }); |
| 2639 | }); | 2701 | }); |