From e6edf2d707871028f2951f1a74da7eaf7e2fd8d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Oct 2025 20:09:42 +0000 Subject: [PATCH 1/5] feat: Add in-memory fallback for localStorage collections in SSR environments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevents LocalStorageCollectionError when localStorage collections are imported on the server by automatically falling back to an in-memory store. This addresses issue #691 where importing client code with localStorage collections in server environments would throw errors during module initialization. Changes: - Add createInMemoryStorage() to provide in-memory StorageApi fallback - Add createNoOpStorageEventApi() for server environments without window - Update localStorageCollectionOptions to use fallbacks instead of throwing errors - Remove unused NoStorageAvailableError and NoStorageEventApiError imports - Update tests to verify fallback behavior instead of expecting errors - Update JSDoc to document fallback behavior This allows isomorphic JavaScript applications to safely import localStorage collection modules without errors, though data will not persist across reloads or sync across tabs when using the in-memory fallback. Fixes #691 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/db/src/local-storage.ts | 65 ++++++++++++++++++++----- packages/db/tests/local-storage.test.ts | 44 +++++++++-------- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/packages/db/src/local-storage.ts b/packages/db/src/local-storage.ts index cb9e3e2ce..df3b1b2e5 100644 --- a/packages/db/src/local-storage.ts +++ b/packages/db/src/local-storage.ts @@ -1,8 +1,6 @@ import { InvalidStorageDataFormatError, InvalidStorageObjectFormatError, - NoStorageAvailableError, - NoStorageEventApiError, SerializationError, StorageKeyRequiredError, } from "./errors" @@ -138,12 +136,58 @@ function generateUuid(): string { return crypto.randomUUID() } +/** + * Creates an in-memory storage implementation that mimics the StorageApi interface + * Used as a fallback when localStorage is not available (e.g., server-side rendering) + * @returns An object implementing the StorageApi interface using an in-memory Map + */ +function createInMemoryStorage(): StorageApi { + const storage = new Map() + + return { + getItem(key: string): string | null { + return storage.get(key) ?? null + }, + setItem(key: string, value: string): void { + storage.set(key, value) + }, + removeItem(key: string): void { + storage.delete(key) + }, + } +} + +/** + * Creates a no-op storage event API for environments without window (e.g., server-side) + * This provides the required interface but doesn't actually listen to any events + * since cross-tab synchronization is not possible in server environments + * @returns An object implementing the StorageEventApi interface with no-op methods + */ +function createNoOpStorageEventApi(): StorageEventApi { + return { + addEventListener: () => { + // No-op: cannot listen to storage events without window + }, + removeEventListener: () => { + // No-op: cannot remove listeners without window + }, + } +} + /** * Creates localStorage collection options for use with a standard Collection * * This function creates a collection that persists data to localStorage/sessionStorage * and synchronizes changes across browser tabs using storage events. * + * **Fallback Behavior:** + * + * When localStorage is not available (e.g., in server-side rendering environments), + * this function automatically falls back to an in-memory storage implementation. + * This prevents errors during module initialization and allows the collection to + * work in any environment, though data will not persist across page reloads or + * be shared across tabs when using the in-memory fallback. + * * **Using with Manual Transactions:** * * For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn` @@ -257,21 +301,18 @@ export function localStorageCollectionOptions( } // Default to window.localStorage if no storage is provided + // Fall back to in-memory storage if localStorage is not available (e.g., server-side rendering) const storage = config.storage || - (typeof window !== `undefined` ? window.localStorage : null) - - if (!storage) { - throw new NoStorageAvailableError() - } + (typeof window !== `undefined` ? window.localStorage : null) || + createInMemoryStorage() // Default to window for storage events if not provided + // Fall back to no-op storage event API if window is not available (e.g., server-side rendering) const storageEventApi = - config.storageEventApi || (typeof window !== `undefined` ? window : null) - - if (!storageEventApi) { - throw new NoStorageEventApiError() - } + config.storageEventApi || + (typeof window !== `undefined` ? window : null) || + createNoOpStorageEventApi() // Track the last known state to detect changes const lastKnownData = new Map>() diff --git a/packages/db/tests/local-storage.test.ts b/packages/db/tests/local-storage.test.ts index f2a488d75..b991622dc 100644 --- a/packages/db/tests/local-storage.test.ts +++ b/packages/db/tests/local-storage.test.ts @@ -2,11 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { createCollection } from "../src/index" import { localStorageCollectionOptions } from "../src/local-storage" import { createTransaction } from "../src/transactions" -import { - NoStorageAvailableError, - NoStorageEventApiError, - StorageKeyRequiredError, -} from "../src/errors" +import { StorageKeyRequiredError } from "../src/errors" import type { StorageEventApi } from "../src/local-storage" // Mock storage implementation for testing that properly implements Storage interface @@ -138,37 +134,43 @@ describe(`localStorage collection`, () => { ).toThrow(StorageKeyRequiredError) }) - it(`should throw error when no storage is available`, () => { + it(`should fall back to in-memory storage when no storage is available`, () => { // Mock window to be undefined globally const originalWindow = globalThis.window // @ts-ignore - Temporarily delete window to test error condition delete globalThis.window - expect(() => - localStorageCollectionOptions({ - storageKey: `test`, - storageEventApi: mockStorageEventApi, - getKey: (item: any) => item.id, - }) - ).toThrow(NoStorageAvailableError) + // Should not throw - instead falls back to in-memory storage + const collectionOptions = localStorageCollectionOptions({ + storageKey: `test`, + storageEventApi: mockStorageEventApi, + getKey: (item: any) => item.id, + }) + + // Verify collection was created successfully + expect(collectionOptions).toBeDefined() + expect(collectionOptions.id).toBe(`local-collection:test`) // Restore window globalThis.window = originalWindow }) - it(`should throw error when no storage event API is available`, () => { + it(`should fall back to no-op event API when no storage event API is available`, () => { // Mock window to be undefined globally const originalWindow = globalThis.window // @ts-ignore - Temporarily delete window to test error condition delete globalThis.window - expect(() => - localStorageCollectionOptions({ - storageKey: `test`, - storage: mockStorage, - getKey: (item: any) => item.id, - }) - ).toThrow(NoStorageEventApiError) + // Should not throw - instead falls back to no-op storage event API + const collectionOptions = localStorageCollectionOptions({ + storageKey: `test`, + storage: mockStorage, + getKey: (item: any) => item.id, + }) + + // Verify collection was created successfully + expect(collectionOptions).toBeDefined() + expect(collectionOptions.id).toBe(`local-collection:test`) // Restore window globalThis.window = originalWindow From e0168654355e511ebfbe3e1c1aded8e05042950d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Oct 2025 20:53:29 +0000 Subject: [PATCH 2/5] chore: Remove unused NoStorageAvailableError and NoStorageEventApiError classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These error classes are no longer used after implementing the in-memory fallback for localStorage collections in SSR environments. Removes dead code to keep the codebase clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/db/src/errors.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 3858df178..7ec6dd478 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -521,22 +521,6 @@ export class StorageKeyRequiredError extends LocalStorageCollectionError { } } -export class NoStorageAvailableError extends LocalStorageCollectionError { - constructor() { - super( - `[LocalStorageCollection] No storage available. Please provide a storage option or ensure window.localStorage is available.` - ) - } -} - -export class NoStorageEventApiError extends LocalStorageCollectionError { - constructor() { - super( - `[LocalStorageCollection] No storage event API available. Please provide a storageEventApi option or ensure window is available.` - ) - } -} - export class InvalidStorageDataFormatError extends LocalStorageCollectionError { constructor(storageKey: string, key: string) { super( From fb2197d8b82fffaa356a1057b53439ea7f9879ac Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Oct 2025 20:57:48 +0000 Subject: [PATCH 3/5] chore: Add changeset for in-memory fallback feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/in-memory-fallback-for-ssr.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/in-memory-fallback-for-ssr.md diff --git a/.changeset/in-memory-fallback-for-ssr.md b/.changeset/in-memory-fallback-for-ssr.md new file mode 100644 index 000000000..73c244276 --- /dev/null +++ b/.changeset/in-memory-fallback-for-ssr.md @@ -0,0 +1,11 @@ +--- +"@tanstack/db": minor +--- + +Add in-memory fallback for localStorage collections in SSR environments + +Prevents `LocalStorageCollectionError` when localStorage collections are imported on the server by automatically falling back to an in-memory store. This allows isomorphic JavaScript applications to safely import localStorage collection modules without errors during module initialization. + +When localStorage is not available (e.g., in server-side rendering environments), the collection automatically uses an in-memory storage implementation. Data will not persist across page reloads or be shared across tabs when using the in-memory fallback, but the collection will function normally otherwise. + +Fixes #691 From 3ade4d599933208c079c8c5d9d4ba0d652a80cbd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Oct 2025 20:59:31 +0000 Subject: [PATCH 4/5] fix: Update changeset to patch version (pre-1.0 project) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/in-memory-fallback-for-ssr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/in-memory-fallback-for-ssr.md b/.changeset/in-memory-fallback-for-ssr.md index 73c244276..8f788996e 100644 --- a/.changeset/in-memory-fallback-for-ssr.md +++ b/.changeset/in-memory-fallback-for-ssr.md @@ -1,5 +1,5 @@ --- -"@tanstack/db": minor +"@tanstack/db": patch --- Add in-memory fallback for localStorage collections in SSR environments From acba6a9147c40e78d2bbc2f63fd938835f4d3f20 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Oct 2025 21:00:53 +0000 Subject: [PATCH 5/5] chore: Update changeset to not reference deleted error class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/in-memory-fallback-for-ssr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/in-memory-fallback-for-ssr.md b/.changeset/in-memory-fallback-for-ssr.md index 8f788996e..5afb61018 100644 --- a/.changeset/in-memory-fallback-for-ssr.md +++ b/.changeset/in-memory-fallback-for-ssr.md @@ -4,7 +4,7 @@ Add in-memory fallback for localStorage collections in SSR environments -Prevents `LocalStorageCollectionError` when localStorage collections are imported on the server by automatically falling back to an in-memory store. This allows isomorphic JavaScript applications to safely import localStorage collection modules without errors during module initialization. +Prevents errors when localStorage collections are imported on the server by automatically falling back to an in-memory store. This allows isomorphic JavaScript applications to safely import localStorage collection modules without errors during module initialization. When localStorage is not available (e.g., in server-side rendering environments), the collection automatically uses an in-memory storage implementation. Data will not persist across page reloads or be shared across tabs when using the in-memory fallback, but the collection will function normally otherwise.