| author | |
| committer | |
| log | 9a78c6f2bab95bdb899dffcefd50fa52f7b61c07 |
| tree | a3d48f1f49fe05b98c15766710ccbc7888c3cd61 |
| parent | aab72f10c1227b7bbf9cce76644e374e275a4e1e |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>8 files changed, 237 insertions(+), 38 deletions(-)
apps/web/src/clientPersistenceStorage.test.ts+18| ... | @@ -51,4 +51,22 @@ describe("clientPersistenceStorage", () => { | ... | @@ -51,4 +51,22 @@ describe("clientPersistenceStorage", () => { |
| 51 | 51 | ||
| 52 | expect(readBrowserClientSettings()).toEqual(settings); | 52 | expect(readBrowserClientSettings()).toEqual(settings); |
| 53 | }); | 53 | }); |
| 54 | |||
| 55 | it("reports structured decode failures while preserving the fallback", async () => { | ||
| 56 | const testWindow = getTestWindow(); | ||
| 57 | testWindow.localStorage.setItem("t3code:client-settings:v1", "not-json"); | ||
| 58 | const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); | ||
| 59 | const { readBrowserClientSettings } = await import("./clientPersistenceStorage"); | ||
| 60 | |||
| 61 | expect(readBrowserClientSettings()).toBeNull(); | ||
| 62 | expect(consoleError).toHaveBeenCalledWith( | ||
| 63 | "Could not read persisted client settings.", | ||
| 64 | expect.objectContaining({ | ||
| 65 | _tag: "LocalStorageOperationError", | ||
| 66 | operation: "decode", | ||
| 67 | storageKey: "t3code:client-settings:v1", | ||
| 68 | cause: expect.anything(), | ||
| 69 | }), | ||
| 70 | ); | ||
| 71 | }); | ||
| 54 | }); | 72 | }); |
apps/web/src/clientPersistenceStorage.ts+2-1| ... | @@ -15,7 +15,8 @@ export function readBrowserClientSettings(): ClientSettings | null { | ... | @@ -15,7 +15,8 @@ export function readBrowserClientSettings(): ClientSettings | null { |
| 15 | 15 | ||
| 16 | try { | 16 | try { |
| 17 | return getLocalStorageItem(CLIENT_SETTINGS_STORAGE_KEY, ClientSettingsSchema); | 17 | return getLocalStorageItem(CLIENT_SETTINGS_STORAGE_KEY, ClientSettingsSchema); |
| 18 | } catch { | 18 | } catch (error) { |
| 19 | console.error("Could not read persisted client settings.", error); | ||
| 19 | return null; | 20 | return null; |
| 20 | } | 21 | } |
| 21 | } | 22 | } |
apps/web/src/components/files/FilePreviewPanel.tsx+9-4| ... | @@ -12,12 +12,14 @@ import { | ... | @@ -12,12 +12,14 @@ import { |
| 12 | squashAtomCommandFailure, | 12 | squashAtomCommandFailure, |
| 13 | } from "@t3tools/client-runtime/state/runtime"; | 13 | } from "@t3tools/client-runtime/state/runtime"; |
| 14 | import { ChevronRight, Code2, Eye, FolderTree, Globe2, LoaderCircle } from "lucide-react"; | 14 | import { ChevronRight, Code2, Eye, FolderTree, Globe2, LoaderCircle } from "lucide-react"; |
| 15 | import * as Schema from "effect/Schema"; | ||
| 15 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 16 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 16 | 17 | ||
| 17 | import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; | 18 | import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; |
| 18 | import ChatMarkdown from "~/components/ChatMarkdown"; | 19 | import ChatMarkdown from "~/components/ChatMarkdown"; |
| 19 | import { OpenInPicker } from "~/components/chat/OpenInPicker"; | 20 | import { OpenInPicker } from "~/components/chat/OpenInPicker"; |
| 20 | import { useTheme } from "~/hooks/useTheme"; | 21 | import { useTheme } from "~/hooks/useTheme"; |
| 22 | import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage"; | ||
| 21 | import { resolveDiffThemeName } from "~/lib/diffRendering"; | 23 | import { resolveDiffThemeName } from "~/lib/diffRendering"; |
| 22 | import { cn } from "~/lib/utils"; | 24 | import { cn } from "~/lib/utils"; |
| 23 | import { isPreviewSupportedInRuntime } from "~/previewStateStore"; | 25 | import { isPreviewSupportedInRuntime } from "~/previewStateStore"; |
| ... | @@ -589,8 +591,9 @@ function RenderedMarkdownSurface({ | ... | @@ -589,8 +591,9 @@ function RenderedMarkdownSurface({ |
| 589 | 591 | ||
| 590 | function initialExplorerOpen(): boolean { | 592 | function initialExplorerOpen(): boolean { |
| 591 | try { | 593 | try { |
| 592 | return window.localStorage.getItem(FILE_EXPLORER_STORAGE_KEY) !== "false"; | 594 | return getLocalStorageItem(FILE_EXPLORER_STORAGE_KEY, Schema.Boolean) ?? true; |
| 593 | } catch { | 595 | } catch (error) { |
| 596 | console.error(error); | ||
| 594 | return true; | 597 | return true; |
| 595 | } | 598 | } |
| 596 | } | 599 | } |
| ... | @@ -650,8 +653,10 @@ export default function FilePreviewPanel({ | ... | @@ -650,8 +653,10 @@ export default function FilePreviewPanel({ |
| 650 | setExplorerOpen((current) => { | 653 | setExplorerOpen((current) => { |
| 651 | const next = !current; | 654 | const next = !current; |
| 652 | try { | 655 | try { |
| 653 | window.localStorage.setItem(FILE_EXPLORER_STORAGE_KEY, String(next)); | 656 | setLocalStorageItem(FILE_EXPLORER_STORAGE_KEY, next, Schema.Boolean); |
| 654 | } catch {} | 657 | } catch (error) { |
| 658 | console.error(error); | ||
| 659 | } | ||
| 655 | return next; | 660 | return next; |
| 656 | }); | 661 | }); |
| 657 | }; | 662 | }; |
apps/web/src/hooks/useLocalStorage.test.ts created+121| ... | @@ -0,0 +1,121 @@ | ||
| 1 | import * as Schema from "effect/Schema"; | ||
| 2 | import { afterEach, describe, expect, it, vi } from "vite-plus/test"; | ||
| 3 | |||
| 4 | function createStorage(overrides: Partial<Storage> = {}): Storage { | ||
| 5 | const store = new Map<string, string>(); | ||
| 6 | return { | ||
| 7 | clear: () => store.clear(), | ||
| 8 | getItem: (key) => store.get(key) ?? null, | ||
| 9 | key: (index) => [...store.keys()][index] ?? null, | ||
| 10 | get length() { | ||
| 11 | return store.size; | ||
| 12 | }, | ||
| 13 | removeItem: (key) => { | ||
| 14 | store.delete(key); | ||
| 15 | }, | ||
| 16 | setItem: (key, value) => { | ||
| 17 | store.set(key, value); | ||
| 18 | }, | ||
| 19 | ...overrides, | ||
| 20 | }; | ||
| 21 | } | ||
| 22 | |||
| 23 | async function loadWithStorage(storage: Storage) { | ||
| 24 | vi.stubGlobal("window", { localStorage: storage }); | ||
| 25 | vi.stubGlobal("localStorage", storage); | ||
| 26 | return import("./useLocalStorage"); | ||
| 27 | } | ||
| 28 | |||
| 29 | afterEach(() => { | ||
| 30 | vi.resetModules(); | ||
| 31 | vi.unstubAllGlobals(); | ||
| 32 | }); | ||
| 33 | |||
| 34 | describe("local storage errors", () => { | ||
| 35 | it("preserves read failure context", async () => { | ||
| 36 | const cause = new Error("storage unavailable"); | ||
| 37 | const { getLocalStorageItem, LocalStorageOperationError } = await loadWithStorage( | ||
| 38 | createStorage({ | ||
| 39 | getItem: () => { | ||
| 40 | throw cause; | ||
| 41 | }, | ||
| 42 | }), | ||
| 43 | ); | ||
| 44 | |||
| 45 | try { | ||
| 46 | getLocalStorageItem("read-key", Schema.String); | ||
| 47 | expect.unreachable("expected the read to fail"); | ||
| 48 | } catch (error) { | ||
| 49 | expect(error).toBeInstanceOf(LocalStorageOperationError); | ||
| 50 | expect(error).toMatchObject({ | ||
| 51 | operation: "read", | ||
| 52 | storageKey: "read-key", | ||
| 53 | cause, | ||
| 54 | }); | ||
| 55 | } | ||
| 56 | }); | ||
| 57 | |||
| 58 | it("preserves decode failure context", async () => { | ||
| 59 | const { getLocalStorageItem, LocalStorageOperationError } = await loadWithStorage( | ||
| 60 | createStorage({ getItem: () => "not-json" }), | ||
| 61 | ); | ||
| 62 | |||
| 63 | try { | ||
| 64 | getLocalStorageItem("decode-key", Schema.String); | ||
| 65 | expect.unreachable("expected decoding to fail"); | ||
| 66 | } catch (error) { | ||
| 67 | expect(error).toBeInstanceOf(LocalStorageOperationError); | ||
| 68 | expect(error).toMatchObject({ | ||
| 69 | operation: "decode", | ||
| 70 | storageKey: "decode-key", | ||
| 71 | cause: expect.anything(), | ||
| 72 | }); | ||
| 73 | } | ||
| 74 | }); | ||
| 75 | |||
| 76 | it("preserves write failure context", async () => { | ||
| 77 | const cause = new Error("storage quota exceeded"); | ||
| 78 | const { LocalStorageOperationError, setLocalStorageItem } = await loadWithStorage( | ||
| 79 | createStorage({ | ||
| 80 | setItem: () => { | ||
| 81 | throw cause; | ||
| 82 | }, | ||
| 83 | }), | ||
| 84 | ); | ||
| 85 | |||
| 86 | try { | ||
| 87 | setLocalStorageItem("write-key", "value", Schema.String); | ||
| 88 | expect.unreachable("expected the write to fail"); | ||
| 89 | } catch (error) { | ||
| 90 | expect(error).toBeInstanceOf(LocalStorageOperationError); | ||
| 91 | expect(error).toMatchObject({ | ||
| 92 | operation: "write", | ||
| 93 | storageKey: "write-key", | ||
| 94 | cause, | ||
| 95 | }); | ||
| 96 | } | ||
| 97 | }); | ||
| 98 | |||
| 99 | it("preserves removal failure context", async () => { | ||
| 100 | const cause = new Error("storage unavailable"); | ||
| 101 | const { LocalStorageOperationError, removeLocalStorageItem } = await loadWithStorage( | ||
| 102 | createStorage({ | ||
| 103 | removeItem: () => { | ||
| 104 | throw cause; | ||
| 105 | }, | ||
| 106 | }), | ||
| 107 | ); | ||
| 108 | |||
| 109 | try { | ||
| 110 | removeLocalStorageItem("remove-key"); | ||
| 111 | expect.unreachable("expected the removal to fail"); | ||
| 112 | } catch (error) { | ||
| 113 | expect(error).toBeInstanceOf(LocalStorageOperationError); | ||
| 114 | expect(error).toMatchObject({ | ||
| 115 | operation: "remove", | ||
| 116 | storageKey: "remove-key", | ||
| 117 | cause, | ||
| 118 | }); | ||
| 119 | } | ||
| 120 | }); | ||
| 121 | }); | ||
apps/web/src/hooks/useLocalStorage.ts+75-24| ... | @@ -2,6 +2,19 @@ import * as Schema from "effect/Schema"; | ... | @@ -2,6 +2,19 @@ import * as Schema from "effect/Schema"; |
| 2 | import * as Record from "effect/Record"; | 2 | import * as Record from "effect/Record"; |
| 3 | import { useCallback, useMemo, useSyncExternalStore } from "react"; | 3 | import { useCallback, useMemo, useSyncExternalStore } from "react"; |
| 4 | 4 | ||
| 5 | export class LocalStorageOperationError extends Schema.TaggedErrorClass<LocalStorageOperationError>()( | ||
| 6 | "LocalStorageOperationError", | ||
| 7 | { | ||
| 8 | operation: Schema.Literals(["read", "decode", "encode", "update", "write", "remove", "notify"]), | ||
| 9 | storageKey: Schema.String, | ||
| 10 | cause: Schema.Defect(), | ||
| 11 | }, | ||
| 12 | ) { | ||
| 13 | override get message(): string { | ||
| 14 | return `Failed to ${this.operation} local storage item ${this.storageKey}.`; | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 5 | const isomorphicLocalStorage: Storage = | 18 | const isomorphicLocalStorage: Storage = |
| 6 | typeof window !== "undefined" | 19 | typeof window !== "undefined" |
| 7 | ? window.localStorage | 20 | ? window.localStorage |
| ... | @@ -19,28 +32,50 @@ const isomorphicLocalStorage: Storage = | ... | @@ -19,28 +32,50 @@ const isomorphicLocalStorage: Storage = |
| 19 | }; | 32 | }; |
| 20 | })(); | 33 | })(); |
| 21 | 34 | ||
| 22 | const decode = <T, E>(schema: Schema.Codec<T, E>, value: string) => { | 35 | const read = (key: string) => { |
| 23 | const decodeJson = Schema.decodeSync(Schema.fromJsonString(schema)); | 36 | try { |
| 24 | return decodeJson(value); | 37 | return isomorphicLocalStorage.getItem(key); |
| 38 | } catch (cause) { | ||
| 39 | throw new LocalStorageOperationError({ operation: "read", storageKey: key, cause }); | ||
| 40 | } | ||
| 41 | }; | ||
| 42 | |||
| 43 | const decode = <T, E>(key: string, schema: Schema.Codec<T, E>, value: string) => { | ||
| 44 | try { | ||
| 45 | return Schema.decodeSync(Schema.fromJsonString(schema))(value); | ||
| 46 | } catch (cause) { | ||
| 47 | throw new LocalStorageOperationError({ operation: "decode", storageKey: key, cause }); | ||
| 48 | } | ||
| 25 | }; | 49 | }; |
| 26 | 50 | ||
| 27 | const encode = <T, E>(schema: Schema.Codec<T, E>, value: T) => { | 51 | const encode = <T, E>(key: string, schema: Schema.Codec<T, E>, value: T) => { |
| 28 | const encodeJson = Schema.encodeSync(Schema.fromJsonString(schema)); | 52 | try { |
| 29 | return encodeJson(value); | 53 | return Schema.encodeSync(Schema.fromJsonString(schema))(value); |
| 54 | } catch (cause) { | ||
| 55 | throw new LocalStorageOperationError({ operation: "encode", storageKey: key, cause }); | ||
| 56 | } | ||
| 30 | }; | 57 | }; |
| 31 | 58 | ||
| 32 | export const getLocalStorageItem = <T, E>(key: string, schema: Schema.Codec<T, E>): T | null => { | 59 | export const getLocalStorageItem = <T, E>(key: string, schema: Schema.Codec<T, E>): T | null => { |
| 33 | const item = isomorphicLocalStorage.getItem(key); | 60 | const item = read(key); |
| 34 | return item ? decode(schema, item) : null; | 61 | return item ? decode(key, schema, item) : null; |
| 35 | }; | 62 | }; |
| 36 | 63 | ||
| 37 | export const setLocalStorageItem = <T, E>(key: string, value: T, schema: Schema.Codec<T, E>) => { | 64 | export const setLocalStorageItem = <T, E>(key: string, value: T, schema: Schema.Codec<T, E>) => { |
| 38 | const valueToSet = encode(schema, value); | 65 | const valueToSet = encode(key, schema, value); |
| 39 | isomorphicLocalStorage.setItem(key, valueToSet); | 66 | try { |
| 67 | isomorphicLocalStorage.setItem(key, valueToSet); | ||
| 68 | } catch (cause) { | ||
| 69 | throw new LocalStorageOperationError({ operation: "write", storageKey: key, cause }); | ||
| 70 | } | ||
| 40 | }; | 71 | }; |
| 41 | 72 | ||
| 42 | export const removeLocalStorageItem = (key: string) => { | 73 | export const removeLocalStorageItem = (key: string) => { |
| 43 | isomorphicLocalStorage.removeItem(key); | 74 | try { |
| 75 | isomorphicLocalStorage.removeItem(key); | ||
| 76 | } catch (cause) { | ||
| 77 | throw new LocalStorageOperationError({ operation: "remove", storageKey: key, cause }); | ||
| 78 | } | ||
| 44 | }; | 79 | }; |
| 45 | 80 | ||
| 46 | const LOCAL_STORAGE_CHANGE_EVENT = "t3code:local_storage_change"; | 81 | const LOCAL_STORAGE_CHANGE_EVENT = "t3code:local_storage_change"; |
| ... | @@ -51,11 +86,15 @@ interface LocalStorageChangeDetail { | ... | @@ -51,11 +86,15 @@ interface LocalStorageChangeDetail { |
| 51 | 86 | ||
| 52 | function dispatchLocalStorageChange(key: string) { | 87 | function dispatchLocalStorageChange(key: string) { |
| 53 | if (typeof window === "undefined") return; | 88 | if (typeof window === "undefined") return; |
| 54 | window.dispatchEvent( | 89 | try { |
| 55 | new CustomEvent<LocalStorageChangeDetail>(LOCAL_STORAGE_CHANGE_EVENT, { | 90 | window.dispatchEvent( |
| 56 | detail: { key }, | 91 | new CustomEvent<LocalStorageChangeDetail>(LOCAL_STORAGE_CHANGE_EVENT, { |
| 57 | }), | 92 | detail: { key }, |
| 58 | ); | 93 | }), |
| 94 | ); | ||
| 95 | } catch (cause) { | ||
| 96 | throw new LocalStorageOperationError({ operation: "notify", storageKey: key, cause }); | ||
| 97 | } | ||
| 59 | } | 98 | } |
| 60 | 99 | ||
| 61 | export function useLocalStorage<T, E>( | 100 | export function useLocalStorage<T, E>( |
| ... | @@ -65,9 +104,9 @@ export function useLocalStorage<T, E>( | ... | @@ -65,9 +104,9 @@ export function useLocalStorage<T, E>( |
| 65 | ): [T, (value: T | ((val: T) => T)) => void] { | 104 | ): [T, (value: T | ((val: T) => T)) => void] { |
| 66 | const getSnapshot = useCallback(() => { | 105 | const getSnapshot = useCallback(() => { |
| 67 | try { | 106 | try { |
| 68 | return isomorphicLocalStorage.getItem(key); | 107 | return read(key); |
| 69 | } catch (error) { | 108 | } catch (error) { |
| 70 | console.error("[LOCALSTORAGE] Error:", error); | 109 | console.error("[LOCALSTORAGE] Could not read stored value.", error); |
| 71 | return null; | 110 | return null; |
| 72 | } | 111 | } |
| 73 | }, [key]); | 112 | }, [key]); |
| ... | @@ -101,19 +140,31 @@ export function useLocalStorage<T, E>( | ... | @@ -101,19 +140,31 @@ export function useLocalStorage<T, E>( |
| 101 | return initialValue; | 140 | return initialValue; |
| 102 | } | 141 | } |
| 103 | try { | 142 | try { |
| 104 | return decode(schema, serializedValue); | 143 | return decode(key, schema, serializedValue); |
| 105 | } catch (error) { | 144 | } catch (error) { |
| 106 | console.error("[LOCALSTORAGE] Error:", error); | 145 | console.error("[LOCALSTORAGE] Could not decode stored value.", error); |
| 107 | return initialValue; | 146 | return initialValue; |
| 108 | } | 147 | } |
| 109 | }, [initialValue, schema, serializedValue]); | 148 | }, [initialValue, key, schema, serializedValue]); |
| 110 | 149 | ||
| 111 | const setValue = useCallback( | 150 | const setValue = useCallback( |
| 112 | (value: T | ((val: T) => T)) => { | 151 | (value: T | ((val: T) => T)) => { |
| 113 | try { | 152 | try { |
| 114 | const currentValue = getLocalStorageItem(key, schema) ?? initialValue; | 153 | const currentValue = getLocalStorageItem(key, schema) ?? initialValue; |
| 115 | const valueToStore = | 154 | let valueToStore: T; |
| 116 | typeof value === "function" ? (value as (val: T) => T)(currentValue) : value; | 155 | if (typeof value === "function") { |
| 156 | try { | ||
| 157 | valueToStore = (value as (val: T) => T)(currentValue); | ||
| 158 | } catch (cause) { | ||
| 159 | throw new LocalStorageOperationError({ | ||
| 160 | operation: "update", | ||
| 161 | storageKey: key, | ||
| 162 | cause, | ||
| 163 | }); | ||
| 164 | } | ||
| 165 | } else { | ||
| 166 | valueToStore = value; | ||
| 167 | } | ||
| 117 | if (valueToStore === null) { | 168 | if (valueToStore === null) { |
| 118 | removeLocalStorageItem(key); | 169 | removeLocalStorageItem(key); |
| 119 | } else { | 170 | } else { |
| ... | @@ -121,7 +172,7 @@ export function useLocalStorage<T, E>( | ... | @@ -121,7 +172,7 @@ export function useLocalStorage<T, E>( |
| 121 | } | 172 | } |
| 122 | dispatchLocalStorageChange(key); | 173 | dispatchLocalStorageChange(key); |
| 123 | } catch (error) { | 174 | } catch (error) { |
| 124 | console.error("[LOCALSTORAGE] Error:", error); | 175 | console.error("[LOCALSTORAGE] Could not update stored value.", error); |
| 125 | } | 176 | } |
| 126 | }, | 177 | }, |
| 127 | [initialValue, key, schema], | 178 | [initialValue, key, schema], |
apps/web/src/hooks/useResizableWidth.ts+4-3| ... | @@ -55,7 +55,8 @@ export function useResizableWidth(options: UseResizableWidthOptions): { | ... | @@ -55,7 +55,8 @@ export function useResizableWidth(options: UseResizableWidthOptions): { |
| 55 | try { | 55 | try { |
| 56 | const stored = getLocalStorageItem(storageKey, WidthSchema); | 56 | const stored = getLocalStorageItem(storageKey, WidthSchema); |
| 57 | return clamp(stored ?? defaultWidth); | 57 | return clamp(stored ?? defaultWidth); |
| 58 | } catch { | 58 | } catch (error) { |
| 59 | console.error("Could not read persisted panel width.", error); | ||
| 59 | return defaultWidth; | 60 | return defaultWidth; |
| 60 | } | 61 | } |
| 61 | }); | 62 | }); |
| ... | @@ -141,8 +142,8 @@ export function useResizableWidth(options: UseResizableWidthOptions): { | ... | @@ -141,8 +142,8 @@ export function useResizableWidth(options: UseResizableWidthOptions): { |
| 141 | // Commit once at drag-end to avoid 60Hz localStorage writes. | 142 | // Commit once at drag-end to avoid 60Hz localStorage writes. |
| 142 | try { | 143 | try { |
| 143 | setLocalStorageItem(storageKey, finalWidth, WidthSchema); | 144 | setLocalStorageItem(storageKey, finalWidth, WidthSchema); |
| 144 | } catch { | 145 | } catch (error) { |
| 145 | // localStorage may be full / disabled; the in-memory state still wins. | 146 | console.error("Could not persist panel width.", error); |
| 146 | } | 147 | } |
| 147 | setWidth(finalWidth); | 148 | setWidth(finalWidth); |
| 148 | }, | 149 | }, |
apps/web/src/providerUpdateDismissal.ts+4-3| ... | @@ -21,7 +21,8 @@ function readProviderUpdateDismissals(): ProviderUpdateDismissals { | ... | @@ -21,7 +21,8 @@ function readProviderUpdateDismissals(): ProviderUpdateDismissals { |
| 21 | keys: [], | 21 | keys: [], |
| 22 | } | 22 | } |
| 23 | ); | 23 | ); |
| 24 | } catch { | 24 | } catch (error) { |
| 25 | console.error("Could not read provider-update dismissals.", error); | ||
| 25 | return { keys: [] }; | 26 | return { keys: [] }; |
| 26 | } | 27 | } |
| 27 | } | 28 | } |
| ... | @@ -33,8 +34,8 @@ function writeProviderUpdateDismissals(document: ProviderUpdateDismissals): void | ... | @@ -33,8 +34,8 @@ function writeProviderUpdateDismissals(document: ProviderUpdateDismissals): void |
| 33 | document, | 34 | document, |
| 34 | ProviderUpdateDismissalsSchema, | 35 | ProviderUpdateDismissalsSchema, |
| 35 | ); | 36 | ); |
| 36 | } catch { | 37 | } catch (error) { |
| 37 | // Dismissal state is best-effort UI state; a storage failure should not block the toast. | 38 | console.error("Could not persist provider-update dismissals.", error); |
| 38 | } | 39 | } |
| 39 | } | 40 | } |
| 40 | 41 |
apps/web/src/versionSkew.ts+4-3| ... | @@ -64,7 +64,8 @@ function readVersionMismatchDismissals(): VersionMismatchDismissals { | ... | @@ -64,7 +64,8 @@ function readVersionMismatchDismissals(): VersionMismatchDismissals { |
| 64 | VersionMismatchDismissalsSchema, | 64 | VersionMismatchDismissalsSchema, |
| 65 | ) ?? { keys: [] } | 65 | ) ?? { keys: [] } |
| 66 | ); | 66 | ); |
| 67 | } catch { | 67 | } catch (error) { |
| 68 | console.error("Could not read version-mismatch dismissals.", error); | ||
| 68 | return { keys: [] }; | 69 | return { keys: [] }; |
| 69 | } | 70 | } |
| 70 | } | 71 | } |
| ... | @@ -76,8 +77,8 @@ function writeVersionMismatchDismissals(document: VersionMismatchDismissals): vo | ... | @@ -76,8 +77,8 @@ function writeVersionMismatchDismissals(document: VersionMismatchDismissals): vo |
| 76 | document, | 77 | document, |
| 77 | VersionMismatchDismissalsSchema, | 78 | VersionMismatchDismissalsSchema, |
| 78 | ); | 79 | ); |
| 79 | } catch { | 80 | } catch (error) { |
| 80 | // Dismissal state is best-effort UI state; a storage failure should not block the banner. | 81 | console.error("Could not persist version-mismatch dismissals.", error); |
| 81 | } | 82 | } |
| 82 | } | 83 | } |
| 83 | 84 |