From 21d2a08f9054dcc2db940d7b8be88d5547900ad8 Mon Sep 17 00:00:00 2001 From: bertmad3400 Date: Thu, 18 Apr 2024 19:26:34 +0000 Subject: [PATCH 1/4] Rename onError to onWriteError Deprecate (but still allow) onError --- index.ts | 7 +++++-- test/domExceptions.test.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 0567870..22fd27f 100644 --- a/index.ts +++ b/index.ts @@ -26,6 +26,7 @@ export interface Options { storage?: StorageType, syncTabs?: boolean, onError?: (e: unknown) => void + onWriteError?: (e: unknown) => void } function getStorage(type: StorageType) { @@ -38,10 +39,12 @@ export function writable(key: string, initialValue: T, options?: Options): return persisted(key, initialValue, options) } export function persisted(key: string, initialValue: T, options?: Options): Writable { + if (options?.onError) console.warn("onError has been deprecated. Please use onWriteError instead") + const serializer = options?.serializer ?? JSON const storageType = options?.storage ?? 'local' const syncTabs = options?.syncTabs ?? true - const onError = options?.onError ?? ((e) => console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e)) + const onWriteError = options?.onWriteError ?? options?.onError ?? ((e) => console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e)) const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined' const storage = browser ? getStorage(storageType) : null @@ -49,7 +52,7 @@ export function persisted(key: string, initialValue: T, options?: Options) try { storage?.setItem(key, serializer.stringify(value)) } catch (e) { - onError(e) + onWriteError(e) } } diff --git a/test/domExceptions.test.ts b/test/domExceptions.test.ts index 27a1403..2d4c236 100644 --- a/test/domExceptions.test.ts +++ b/test/domExceptions.test.ts @@ -23,7 +23,7 @@ describe('persisted()', () => { it('calls custom error function', () => { const mockFunc = vi.fn() - const store = persisted('myKey2', 'myVal', { onError: mockFunc }) + const store = persisted('myKey2', 'myVal', { onWriteError: mockFunc }) store.set("myNewVal") expect(mockFunc).toHaveBeenCalledOnce() From 5f820e61f03d70f12937baf24413f6538a3263b9 Mon Sep 17 00:00:00 2001 From: bertmad3400 Date: Thu, 18 Apr 2024 19:34:54 +0000 Subject: [PATCH 2/4] Add onParseError handler --- index.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 22fd27f..3e11ca2 100644 --- a/index.ts +++ b/index.ts @@ -27,6 +27,7 @@ export interface Options { syncTabs?: boolean, onError?: (e: unknown) => void onWriteError?: (e: unknown) => void + onParseError?: (newValue: string | null, e: unknown) => void } function getStorage(type: StorageType) { @@ -45,6 +46,7 @@ export function persisted(key: string, initialValue: T, options?: Options) const storageType = options?.storage ?? 'local' 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 browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined' const storage = browser ? getStorage(storageType) : null @@ -60,7 +62,11 @@ export function persisted(key: string, initialValue: T, options?: Options) const json = storage?.getItem(key) if (json) { - return serializer.parse(json) + try { + return serializer.parse(json) + } catch (e) { + onParseError(json, e) + } } return initialValue @@ -71,8 +77,16 @@ export function persisted(key: string, initialValue: T, options?: Options) const store = internal(initial, (set) => { if (browser && storageType == 'local' && syncTabs) { const handleStorage = (event: StorageEvent) => { - if (event.key === key) - set(event.newValue ? serializer.parse(event.newValue) : null) + if (event.key === key) { + let newVal: any + try { + newVal = event.newValue ? serializer.parse(event.newValue) : null + } catch (e) { + onParseError(event.newValue, e) + return + } + set(newVal) + } } window.addEventListener("storage", handleStorage) From ce89846bca7daacb0343e1b66881442c0658923b Mon Sep 17 00:00:00 2001 From: bertmad3400 Date: Thu, 18 Apr 2024 19:38:22 +0000 Subject: [PATCH 3/4] Update readme to cover changes --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ab9225d..5fab726 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get(preferences) // read value $preferences // read value with automatic subscription ``` -You can also optionally set the `serializer`, `storage` and `onError` type: +You can also optionally set the `serializer`, `storage`, `onWriteError` and `onParseError` type: ```javascript import * as devalue from 'devalue' @@ -48,21 +48,27 @@ import * as devalue from 'devalue' export const preferences = persisted('local-storage-key', 'default-value', { serializer: devalue, // defaults to `JSON` storage: 'session', // 'session' for sessionStorage, defaults to 'local' - syncTabs: true // choose wether to sync localStorage across tabs, default is true - onError: (e) => {/* Do something */} // Defaults to console.error with the error object + syncTabs: true, // choose wether to sync localStorage across tabs, default is true + onWriteError: (e) => {/* Do something */}, // Defaults to console.error with the error object + onParseError: (newVal, e) => {/* Do something */}, // Defaults to console.error with the error object }) ``` -As the library will swallow errors encountered when reading from browser storage it is possible to specify a custom function to handle the error. Should the swallowing not be desirable, it is possible to re-throw the error like the following example (not recommended): +As the library will swallow errors encountered when writing to the browser storage, or parsing the string value gotten from browser storage, it is possible to specify a custom function to handle the error. Should the swallowing not be desirable, it is possible to re-throw the error like the following example (not recommended): ```javascript export const preferences = persisted('local-storage-key', 'default-value', { - onError: (e) => { + onWriteError: (e) => { + throw e + }, + onParseError: (newVal, e) => { throw e } }) ``` +The newVal parameter passed to the onParseError handler is the string value which was attempted (but failed) to serialize + ## License MIT From 8aaa2e5ef5469cb2209cb65b1904fc51a3503bd8 Mon Sep 17 00:00:00 2001 From: bertmad3400 Date: Thu, 18 Apr 2024 20:06:07 +0000 Subject: [PATCH 4/4] Add tests for read dom exceptions --- test/readDomExceptions.test.ts | 39 +++++++++++++++++++ ...ons.test.ts => writeDomExceptions.test.ts} | 0 2 files changed, 39 insertions(+) create mode 100644 test/readDomExceptions.test.ts rename test/{domExceptions.test.ts => writeDomExceptions.test.ts} (100%) diff --git a/test/readDomExceptions.test.ts b/test/readDomExceptions.test.ts new file mode 100644 index 0000000..0ace835 --- /dev/null +++ b/test/readDomExceptions.test.ts @@ -0,0 +1,39 @@ +import { persisted } from '../index' +import { expect, vi, beforeEach, describe, it } from 'vitest' + +beforeEach(() => localStorage.clear()) + +describe('persisted()', () => { + + it('logs error encountered when reading from local storage', () => { + const consoleMock = vi.spyOn(console, 'error').mockImplementation(() => undefined); + localStorage.setItem("myKey", "INVALID JSON") + persisted('myKey', '') + + expect(consoleMock).toHaveBeenCalledWith('Error when parsing "INVALID JSON" from persisted store "myKey"', expect.any(SyntaxError)) + consoleMock.mockReset(); + }) + + it('calls custom error function upon init', () => { + const mockFunc = vi.fn() + + localStorage.setItem("myKey2", "INVALID JSON") + persisted('myKey2', '', { onParseError: mockFunc }) + + expect(mockFunc).toHaveBeenCalledOnce() + }) + + it('calls custom error function upon external write', () => { + const mockFunc = vi.fn() + + const store = persisted('myKey3', '', { onParseError: mockFunc }) + const unsub = store.subscribe(() => undefined) + + const event = new StorageEvent('storage', { key: 'myKey3', newValue: 'INVALID JSON' }) + window.dispatchEvent(event) + + expect(mockFunc).toHaveBeenCalledOnce() + + unsub() + }) +}) diff --git a/test/domExceptions.test.ts b/test/writeDomExceptions.test.ts similarity index 100% rename from test/domExceptions.test.ts rename to test/writeDomExceptions.test.ts