From aa7dbd7abad64729da4540d2fbef20055f4547f1 Mon Sep 17 00:00:00 2001 From: Kfir Amar Date: Fri, 13 Mar 2026 13:31:56 +0200 Subject: [PATCH] fix(web): guard terminal persistence when storage is unavailable --- apps/web/src/terminalStateStore.test.ts | 30 ++++++++++++++++++++++--- apps/web/src/terminalStateStore.ts | 30 ++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/apps/web/src/terminalStateStore.test.ts b/apps/web/src/terminalStateStore.test.ts index e7e240cf2..821b19c27 100644 --- a/apps/web/src/terminalStateStore.test.ts +++ b/apps/web/src/terminalStateStore.test.ts @@ -4,12 +4,36 @@ import { beforeEach, describe, expect, it } from "vitest"; import { selectThreadTerminalState, useTerminalStateStore } from "./terminalStateStore"; const THREAD_ID = ThreadId.makeUnsafe("thread-1"); +const storageBacking = new Map(); + +const storageMock: Storage = { + get length() { + return storageBacking.size; + }, + clear() { + storageBacking.clear(); + }, + getItem(key) { + return storageBacking.get(key) ?? null; + }, + key(index) { + return [...storageBacking.keys()][index] ?? null; + }, + removeItem(key) { + storageBacking.delete(key); + }, + setItem(key, value) { + storageBacking.set(key, value); + }, +}; describe("terminalStateStore actions", () => { beforeEach(() => { - if (typeof localStorage !== "undefined") { - localStorage.clear(); - } + Object.defineProperty(globalThis, "localStorage", { + configurable: true, + value: storageMock, + }); + localStorage.clear(); useTerminalStateStore.setState({ terminalStateByThreadId: {} }); }); diff --git a/apps/web/src/terminalStateStore.ts b/apps/web/src/terminalStateStore.ts index b2cea6d56..24b1a9c3c 100644 --- a/apps/web/src/terminalStateStore.ts +++ b/apps/web/src/terminalStateStore.ts @@ -27,6 +27,34 @@ interface ThreadTerminalState { const TERMINAL_STATE_STORAGE_KEY = "t3code:terminal-state:v1"; +const noopStorage: Storage = { + get length() { + return 0; + }, + clear() {}, + getItem() { + return null; + }, + key() { + return null; + }, + removeItem() {}, + setItem() {}, +}; + +function getTerminalStateStorage(): Storage { + const storage = globalThis.localStorage; + if ( + storage && + typeof storage.getItem === "function" && + typeof storage.setItem === "function" && + typeof storage.removeItem === "function" + ) { + return storage; + } + return noopStorage; +} + function normalizeTerminalIds(terminalIds: string[]): string[] { const ids = [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))]; return ids.length > 0 ? ids : [DEFAULT_THREAD_TERMINAL_ID]; @@ -542,7 +570,7 @@ export const useTerminalStateStore = create()( { name: TERMINAL_STATE_STORAGE_KEY, version: 1, - storage: createJSONStorage(() => localStorage), + storage: createJSONStorage(getTerminalStateStorage), partialize: (state) => ({ terminalStateByThreadId: state.terminalStateByThreadId, }),