Skip to content

Commit

Permalink
Merge pull request #246 from bertmad3400/master
Browse files Browse the repository at this point in the history
Improve error handling
  • Loading branch information
joshnuss authored Apr 19, 2024
2 parents 60d2620 + 8aaa2e5 commit 636fc80
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 11 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
27 changes: 22 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface Options<T> {
storage?: StorageType,
syncTabs?: boolean,
onError?: (e: unknown) => void
onWriteError?: (e: unknown) => void
onParseError?: (newValue: string | null, e: unknown) => void
}

function getStorage(type: StorageType) {
Expand All @@ -38,26 +40,33 @@ export function writable<T>(key: string, initialValue: T, options?: Options<T>):
return persisted<T>(key, initialValue, options)
}
export function persisted<T>(key: string, initialValue: T, options?: Options<T>): Writable<T> {
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 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

function updateStorage(key: string, value: T) {
try {
storage?.setItem(key, serializer.stringify(value))
} catch (e) {
onError(e)
onWriteError(e)
}
}

function maybeLoadInitial(): T {
const json = storage?.getItem(key)

if (json) {
return <T>serializer.parse(json)
try {
return <T>serializer.parse(json)
} catch (e) {
onParseError(json, e)
}
}

return initialValue
Expand All @@ -68,8 +77,16 @@ export function persisted<T>(key: string, initialValue: T, options?: Options<T>)
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)
Expand Down
39 changes: 39 additions & 0 deletions test/readDomExceptions.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 636fc80

Please sign in to comment.