Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 50 additions & 44 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,20 @@ export function writable<StoreType, SerializerType = StoreType>(key: string, ini
export function persisted<StoreType, SerializerType = StoreType>(key: string, initialValue: StoreType, options?: Options<StoreType, SerializerType>): Persisted<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 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) {
Expand Down Expand Up @@ -88,52 +92,54 @@ export function persisted<StoreType, SerializerType = StoreType>(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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If event.newValue is falsy, are you not interpreting that as a value deletion? I don't remember if this NPM package deletes values or not.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event.newValue is a JSON string, so if it's falsey, it's because it's undefined.
If memory serves, it's undefined when the key was deleted in another tab.

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<StoreType>) {
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<StoreType>) {
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
}
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
144 changes: 144 additions & 0 deletions test/localStorageStore.node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// @vitest-environment node
import { persisted, writable } from '../index'
import { get } from 'svelte/store'
import { expect, vi, 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<number>) => 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')
})
})
1 change: 1 addition & 0 deletions test/readDomExceptions.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import { persisted } from '../index'
import { expect, vi, beforeEach, describe, it } from 'vitest'

Expand Down
3 changes: 1 addition & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'jsdom'
globals: true
},
})
Loading