From caea8dd2181e70d2cee9fb0680ab8f63117ba0ca Mon Sep 17 00:00:00 2001 From: Josh Nussbaum Date: Sat, 21 Dec 2024 21:10:51 -0500 Subject: [PATCH 1/2] Removes store caching during SSR --- index.ts | 94 ++++++------ ...est.ts => localStorageStore.jsdom.test.ts} | 1 + test/localStorageStore.node.test.ts | 144 ++++++++++++++++++ test/readDomExceptions.test.ts | 1 + vite.config.ts | 3 +- 5 files changed, 197 insertions(+), 46 deletions(-) rename test/{localStorageStore.test.ts => localStorageStore.jsdom.test.ts} (99%) create mode 100644 test/localStorageStore.node.test.ts diff --git a/index.ts b/index.ts index b1f247f..1e5a6fc 100644 --- a/index.ts +++ b/index.ts @@ -48,16 +48,20 @@ export function writable(key: string, ini export function persisted(key: string, initialValue: StoreType, options?: Options): Persisted { if (options?.onError) console.warn("onError has been deprecated. Please use onWriteError instead") - const serializer = options?.serializer ?? JSON const storageType = options?.storage ?? 'local' + const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined' + + if (browser && stores[storageType][key]) { + return stores[storageType][key] + } + + const serializer = options?.serializer ?? JSON const syncTabs = options?.syncTabs ?? true const onWriteError = options?.onWriteError ?? options?.onError ?? ((e) => console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e)) const onParseError = options?.onParseError ?? ((newVal, e) => console.error(`Error when parsing ${newVal ? '"' + newVal + '"' : "value"} from persisted store "${key}"`, e)) - const beforeRead = options?.beforeRead ?? ((val) => val as unknown as StoreType) const beforeWrite = options?.beforeWrite ?? ((val) => val as unknown as SerializerType) - const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined' const storage = browser ? getStorage(storageType) : null function updateStorage(key: string, value: StoreType) { @@ -88,52 +92,54 @@ export function persisted(key: string, in return newVal } - if (!stores[storageType][key]) { - const initial = maybeLoadInitial() - const store = internal(initial, (set) => { - if (browser && storageType == 'local' && syncTabs) { - const handleStorage = (event: StorageEvent) => { - if (event.key === key && event.newValue) { - let newVal: any - try { - newVal = serializer.parse(event.newValue) - } catch (e) { - onParseError(event.newValue, e) - return - } - const processedVal = beforeRead(newVal) - - set(processedVal) + const initial = maybeLoadInitial() + const store = internal(initial, (set) => { + if (browser && storageType == 'local' && syncTabs) { + const handleStorage = (event: StorageEvent) => { + if (event.key === key && event.newValue) { + let newVal: any + try { + newVal = serializer.parse(event.newValue) + } catch (e) { + onParseError(event.newValue, e) + return } - } + const processedVal = beforeRead(newVal) - window.addEventListener("storage", handleStorage) - - return () => window.removeEventListener("storage", handleStorage) + set(processedVal) + } } - }) - const { subscribe, set } = store + window.addEventListener("storage", handleStorage) - stores[storageType][key] = { - set(value: StoreType) { - set(value) - updateStorage(key, value) - }, - update(callback: Updater) { - return store.update((last) => { - const value = callback(last) - - updateStorage(key, value) - - return value - }) - }, - reset() { - this.set(initialValue) - }, - subscribe + return () => window.removeEventListener("storage", handleStorage) } + }) + + const { subscribe, set } = store + const persistedStore = { + set(value: StoreType) { + set(value) + updateStorage(key, value) + }, + update(callback: Updater) { + return store.update((last) => { + const value = callback(last) + + updateStorage(key, value) + + return value + }) + }, + reset() { + this.set(initialValue) + }, + subscribe + } + + if (browser) { + stores[storageType][key] = persistedStore } - return stores[storageType][key] + + return persistedStore } diff --git a/test/localStorageStore.test.ts b/test/localStorageStore.jsdom.test.ts similarity index 99% rename from test/localStorageStore.test.ts rename to test/localStorageStore.jsdom.test.ts index 7fc2b6f..ee29f53 100644 --- a/test/localStorageStore.test.ts +++ b/test/localStorageStore.jsdom.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { persisted, writable } from '../index' import { get } from 'svelte/store' import { expect, vi, beforeEach, describe, test, it } from 'vitest' diff --git a/test/localStorageStore.node.test.ts b/test/localStorageStore.node.test.ts new file mode 100644 index 0000000..aebb5fa --- /dev/null +++ b/test/localStorageStore.node.test.ts @@ -0,0 +1,144 @@ +// @vitest-environment node +import { persisted, writable } from '../index' +import { get } from 'svelte/store' +import { expect, vi, beforeEach, describe, test, it } from 'vitest' + +describe('writable()', () => { + test('it works, but raises deprecation warning', () => { + console.warn = vi.fn() + + const store = writable('myKey2', 'initial') + const value = get(store) + + expect(value).toEqual('initial') + expect(console.warn).toHaveBeenCalledWith(expect.stringMatching(/deprecated/)) + }) +}) + +describe('persisted()', () => { + test('uses initial value if nothing in local storage', () => { + const store = persisted('myKey', 123) + const value = get(store) + + expect(value).toEqual(123) + }) + + describe('set()', () => { + test('replaces old value', () => { + const store = persisted('myKey3', '') + store.set('new-value') + const value = get(store) + + expect(value).toEqual('new-value') + }) + + test('adds new value', () => { + const store = persisted('myKey4', '') + store.set('new-value') + const value = get(store) + + expect(value).toEqual('new-value') + }) + }) + + describe('update()', () => { + test('replaces old value', () => { + const store = persisted('myKey5', 123) + store.update(n => n + 1) + const value = get(store) + + expect(value).toEqual(124) + }) + + test('adds new value', () => { + const store = persisted('myKey6', 123) + store.update(n => n + 1) + const value = get(store) + + expect(value).toEqual(124) + }) + }) + + describe('reset', () => { + it('resets to initial value', () => { + const store = persisted('myKey14', 123); + store.set(456); + store.reset(); + const value = get(store); + + expect(value).toEqual(123); + }); + }); + + describe('subscribe()', () => { + it('publishes updates', () => { + const store = persisted('myKey7', 123) + const values: number[] = [] + const unsub = store.subscribe((value: number) => { + if (value !== undefined) values.push(value) + }) + store.set(456) + store.set(999) + + expect(values).toEqual([123, 456, 999]) + + unsub() + }) + }) + + it("doesn't handle duplicate stores with the same key", () => { + const store1 = persisted('same-key', 1) + const values1: number[] = [] + + const unsub1 = store1.subscribe(value => { + values1.push(value) + }) + + store1.set(2) + + const store2 = persisted('same-key', 99) + const values2: number[] = [] + + const unsub2 = store2.subscribe(value => { + values2.push(value) + }) + + store1.set(3) + store2.set(4) + + expect(values1).toEqual([1, 2, 3]) + expect(values2).toEqual([99, 4]) + expect(get(store1)).not.toEqual(get(store2)) + + expect(store1).not.toEqual(store2) + + unsub1() + unsub2() + }) + + it('allows custom serialize/deserialize functions', () => { + const serializer = { + stringify: (set: Set) => JSON.stringify(Array.from(set)), + parse: (json: string) => new Set(JSON.parse(json)), + } + + const testSet = new Set([1, 2, 3]) + + const store = persisted('myKey11', testSet, { serializer }) + const value = get(store) + + store.update(d => d.add(4)) + + expect(value).toEqual(testSet) + }) + + it('lets you switch storage type', () => { + const store = persisted('myKey12', 'foo', { + storage: 'session' + }) + + store.set('bar') + + expect(get(store)).toEqual('bar') + }) +}) diff --git a/test/readDomExceptions.test.ts b/test/readDomExceptions.test.ts index 0ace835..4954f58 100644 --- a/test/readDomExceptions.test.ts +++ b/test/readDomExceptions.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { persisted } from '../index' import { expect, vi, beforeEach, describe, it } from 'vitest' diff --git a/vite.config.ts b/vite.config.ts index 3947faf..93cb781 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - globals: true, - environment: 'jsdom' + globals: true }, }) From 986d28fc2afcd46a84b63874e6cf5d571601a9cc Mon Sep 17 00:00:00 2001 From: Josh Nussbaum Date: Sat, 21 Dec 2024 21:16:45 -0500 Subject: [PATCH 2/2] Removes unused import --- test/localStorageStore.node.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/localStorageStore.node.test.ts b/test/localStorageStore.node.test.ts index aebb5fa..32aad37 100644 --- a/test/localStorageStore.node.test.ts +++ b/test/localStorageStore.node.test.ts @@ -1,7 +1,7 @@ // @vitest-environment node import { persisted, writable } from '../index' import { get } from 'svelte/store' -import { expect, vi, beforeEach, describe, test, it } from 'vitest' +import { expect, vi, describe, test, it } from 'vitest' describe('writable()', () => { test('it works, but raises deprecation warning', () => {