Skip to content

Commit

Permalink
feat: Merge pull request #250 from bertmad3400/master
Browse files Browse the repository at this point in the history
Implementing beforeRead and beforeWrite
  • Loading branch information
joshnuss authored May 31, 2024
2 parents 08b3a42 + cb0a317 commit 1a83f84
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 33 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const preferences = persisted('local-storage-key', 'default-value', {
syncTabs: true, // choose whether to sync localStorage across tabs, default is true
onWriteError: (error) => {/* handle or rethrow */}, // Defaults to console.error with the error object
onParseError: (raw, error) => {/* handle or rethrow */}, // Defaults to console.error with the error object
beforeRead: (value) => {/* change value after serialization but before setting store to return value*/},
beforeWrite: (value) => {/* change value after writing to store, but before writing return value to local storage*/},
})
```

Expand Down
50 changes: 32 additions & 18 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,71 +21,85 @@ export interface Serializer<T> {

export type StorageType = 'local' | 'session'

export interface Options<T> {
serializer?: Serializer<T>
export interface Options<StoreType, SerializerType> {
serializer?: Serializer<SerializerType>
storage?: StorageType,
syncTabs?: boolean,
onError?: (e: unknown) => void
onWriteError?: (e: unknown) => void
onParseError?: (newValue: string | null, e: unknown) => void
beforeRead?: (val: SerializerType) => StoreType
beforeWrite?: (val: StoreType) => SerializerType
}

function getStorage(type: StorageType) {
return type === 'local' ? localStorage : sessionStorage
}

/** @deprecated `writable()` has been renamed to `persisted()` */
export function writable<T>(key: string, initialValue: T, options?: Options<T>): Writable<T> {
export function writable<StoreType, SerializerType = StoreType>(key: string, initialValue: StoreType, options?: Options<StoreType, SerializerType>): Writable<StoreType> {
console.warn("writable() has been deprecated. Please use persisted() instead.\n\nchange:\n\nimport { writable } from 'svelte-persisted-store'\n\nto:\n\nimport { persisted } from 'svelte-persisted-store'")
return persisted<T>(key, initialValue, options)
return persisted<StoreType, SerializerType>(key, initialValue, options)
}
export function persisted<T>(key: string, initialValue: T, options?: Options<T>): Writable<T> {
export function persisted<StoreType, SerializerType = StoreType>(key: string, initialValue: StoreType, options?: Options<StoreType, SerializerType>): Writable<StoreType> {
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 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: T) {
function updateStorage(key: string, value: StoreType) {
const newVal = beforeWrite(value)

try {
storage?.setItem(key, serializer.stringify(value))
storage?.setItem(key, serializer.stringify(newVal))
} catch (e) {
onWriteError(e)
}
}

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

if (json) {
function maybeLoadInitial(): StoreType {
function serialize(json: any) {
try {
return <T>serializer.parse(json)
return <SerializerType>serializer.parse(json)
} catch (e) {
onParseError(json, e)
}
}
const json = storage?.getItem(key)
if (json == null) return initialValue

return initialValue
const serialized = serialize(json)
if (serialized == null) return initialValue

const newVal = beforeRead(serialized)
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) {
if (event.key === key && event.newValue) {
let newVal: any
try {
newVal = event.newValue ? serializer.parse(event.newValue) : null
newVal = serializer.parse(event.newValue)
} catch (e) {
onParseError(event.newValue, e)
return
}
set(newVal)
const processedVal = beforeRead(newVal)

set(processedVal)
}
}

Expand All @@ -98,11 +112,11 @@ export function persisted<T>(key: string, initialValue: T, options?: Options<T>)
const { subscribe, set } = store

stores[storageType][key] = {
set(value: T) {
set(value: StoreType) {
set(value)
updateStorage(key, value)
},
update(callback: Updater<T>) {
update(callback: Updater<StoreType>) {
return store.update((last) => {
const value = callback(last)

Expand Down
60 changes: 45 additions & 15 deletions test/localStorageStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('persisted()', () => {
it('publishes updates', () => {
const store = persisted('myKey7', 123)
const values: number[] = []
const unsub = store.subscribe((value : number) => {
const unsub = store.subscribe((value: number) => {
if (value !== undefined) values.push(value)
})
store.set(456)
Expand Down Expand Up @@ -132,37 +132,67 @@ describe('persisted()', () => {
unsub2()
})

describe("beforeRead and beforeWrite", () => {
it("allows modifying initial value before reading", () => {
localStorage.setItem("beforeRead-init-test", JSON.stringify(2))
const store = persisted("beforeRead-init-test", 0, { beforeRead: (v: number) => v * 2 })
expect(get(store)).toEqual(4)
})
it("allows modifying value before reading upon event", () => {
const store = persisted("beforeRead-test", 0, { beforeRead: (v: number) => v * 2 })
const values: number[] = []

const unsub = store.subscribe((val: number) => {
values.push(val)
})

const event = new StorageEvent('storage', { key: 'beforeRead-test', newValue: "2" })
window.dispatchEvent(event)

expect(values).toEqual([0, 4])

unsub()
})

it("allows modifying value before writing", () => {
const store = persisted("beforeWrite-test", 0, { beforeWrite: (v) => v * 2 })
store.set(2)

expect(JSON.parse(localStorage.getItem("beforeWrite-test") as string)).toEqual(4)
})
})

describe('handles window.storage event', () => {
type NumberDict = { [key: string] : number }
type NumberDict = { [key: string]: number }

it('sets storage when key matches', () => {
const store = persisted('myKey8', {a: 1})
const store = persisted('myKey8', { a: 1 })
const values: NumberDict[] = []

const unsub = store.subscribe((value: NumberDict) => {
values.push(value)
})

const event = new StorageEvent('storage', {key: 'myKey8', newValue: '{"a": 1, "b": 2}'})
const event = new StorageEvent('storage', { key: 'myKey8', newValue: '{"a": 1, "b": 2}' })
window.dispatchEvent(event)

expect(values).toEqual([{a: 1}, {a: 1, b: 2}])
expect(values).toEqual([{ a: 1 }, { a: 1, b: 2 }])

unsub()
})

it('sets store to null when value is null', () => {
const store = persisted('myKey9', {a: 1})
it('ignores storages events when value is null', () => {
const store = persisted('myKey9', { a: 1 })
const values: NumberDict[] = []

const unsub = store.subscribe((value: NumberDict) => {
values.push(value)
})

const event = new StorageEvent('storage', {key: 'myKey9', newValue: null})
const event = new StorageEvent('storage', { key: 'myKey9', newValue: null })
window.dispatchEvent(event)

expect(values).toEqual([{a: 1}, null])
expect(values).toEqual([{ a: 1 }])

unsub()
})
Expand All @@ -175,7 +205,7 @@ describe('persisted()', () => {
values.push(value)
})

const event = new StorageEvent('storage', {key: 'unknownKey', newValue: '2'})
const event = new StorageEvent('storage', { key: 'unknownKey', newValue: '2' })
window.dispatchEvent(event)

expect(values).toEqual([1])
Expand All @@ -189,7 +219,7 @@ describe('persisted()', () => {
const store = persisted('myKeyb', 1)
const values: number[] = []

const event = new StorageEvent('storage', {key: 'myKeyb', newValue: '2'})
const event = new StorageEvent('storage', { key: 'myKeyb', newValue: '2' })
window.dispatchEvent(event)

const unsub = store.subscribe((value: number) => {
Expand All @@ -209,7 +239,7 @@ describe('persisted()', () => {
values.push(value)
})

const event = new StorageEvent('storage', {key: 'myKey10', newValue: '2'})
const event = new StorageEvent('storage', { key: 'myKey10', newValue: '2' })
window.dispatchEvent(event)

expect(values).toEqual([1])
Expand All @@ -219,13 +249,13 @@ describe('persisted()', () => {

it("doesn't update, when syncTabs option is disabled", () => {
const store = persisted('myKey13', 1, { syncTabs: false })
const values = []
const values: number[] = []

const unsub = store.subscribe((value) => {
values.push(value)
})

const event = new StorageEvent('storage', {key: 'myKey13', newValue: '2'})
const event = new StorageEvent('storage', { key: 'myKey13', newValue: '2' })
window.dispatchEvent(event)

expect(values).toEqual([1])
Expand All @@ -248,7 +278,7 @@ describe('persisted()', () => {
store.update(d => d.add(4))

expect(value).toEqual(testSet)
expect(localStorage.myKey11).toEqual(serializer.stringify(new Set([1,2,3,4])))
expect(localStorage.myKey11).toEqual(serializer.stringify(new Set([1, 2, 3, 4])))
})

it('lets you switch storage type', () => {
Expand Down

0 comments on commit 1a83f84

Please sign in to comment.