Creates a persistentStore
This commit is contained in:
88
src/lib/persistentStore.test.ts
Normal file
88
src/lib/persistentStore.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, beforeAll, afterAll, afterEach } from 'vitest';
|
||||||
|
import { __setMocked, persistentWritable } from './persistentStore';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
describe('persistentWritable', () => {
|
||||||
|
let _storage: Record<string, string> = {};
|
||||||
|
const storageMock = {
|
||||||
|
setItem: vi.fn(),
|
||||||
|
getItem: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
vi.stubGlobal('localStorage', storageMock);
|
||||||
|
_storage = {};
|
||||||
|
storageMock.setItem.mockImplementation((key: string, value: string) => {
|
||||||
|
_storage[key] = value;
|
||||||
|
});
|
||||||
|
storageMock.getItem.mockImplementation((key: string) => {
|
||||||
|
const res = _storage[key];
|
||||||
|
if (res) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('in browser', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
__setMocked(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
__setMocked(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create multiple stores', () => {
|
||||||
|
const firstStore = persistentWritable<number>('browser-multiple', 0);
|
||||||
|
|
||||||
|
const secondStore = persistentWritable<number>('browser-multiple', 1);
|
||||||
|
|
||||||
|
expect(firstStore).toStrictEqual(secondStore);
|
||||||
|
expect(get(secondStore)).toEqual(0);
|
||||||
|
|
||||||
|
expect(storageMock.getItem).toHaveBeenCalledOnce();
|
||||||
|
expect(storageMock.getItem.mock.calls[0]).toEqual(['browser-multiple']);
|
||||||
|
expect(storageMock.setItem).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update localStorage', () => {
|
||||||
|
const store = persistentWritable<string>('browser-update', 'foo');
|
||||||
|
expect(storageMock.getItem).toHaveBeenCalledOnce();
|
||||||
|
store.set('bar');
|
||||||
|
expect(storageMock.setItem.mock.calls[0]).toEqual(['browser-update', '"bar"']);
|
||||||
|
expect(_storage['browser-update']).toEqual('"bar"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize from localStorage', () => {
|
||||||
|
_storage['browser-existing'] = '"bar"';
|
||||||
|
const store = persistentWritable<string>('browser-existing', 'foo');
|
||||||
|
expect(get(store)).toEqual('bar');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('in server', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
__setMocked(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
expect(storageMock.setItem).toHaveBeenCalledTimes(0);
|
||||||
|
expect(storageMock.getItem).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create multiple stores', () => {
|
||||||
|
const firstStore = persistentWritable<number>('server-multiple', 0);
|
||||||
|
const secondStore = persistentWritable<number>('server-multiple', 0);
|
||||||
|
expect(firstStore).toStrictEqual(secondStore);
|
||||||
|
expect(get(secondStore)).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update localStorage', () => {
|
||||||
|
const store = persistentWritable<string>('server-update', 'foo');
|
||||||
|
store.set('foobar');
|
||||||
|
expect(get(store)).toEqual('foobar');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
74
src/lib/persistentStore.ts
Normal file
74
src/lib/persistentStore.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
|
interface Storage {
|
||||||
|
getItem: (key: string) => string | null;
|
||||||
|
setItem: (key: string, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialValueFromStorage<Type>(storage: Storage, key: string, initialValue: Type): Type {
|
||||||
|
const serialized = storage.getItem(key);
|
||||||
|
if (serialized == null) {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(serialized);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`could not parse '${serialized}' from storage: ${err}`);
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStorage<Type>(storage: Storage, key: string, value: Type) {
|
||||||
|
storage.setItem(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
storage?: 'local' | 'session';
|
||||||
|
}
|
||||||
|
|
||||||
|
const stores: Record<string, Writable<unknown>> = {};
|
||||||
|
|
||||||
|
let _mocked = false;
|
||||||
|
|
||||||
|
// Only use it in unit tests.
|
||||||
|
export function __setMocked(mocked: boolean) {
|
||||||
|
_mocked = mocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistentWritable<Type>(
|
||||||
|
key: string,
|
||||||
|
initialValue: Type,
|
||||||
|
options?: Options
|
||||||
|
): Writable<Type> {
|
||||||
|
if (stores[key]) {
|
||||||
|
return stores[key] as Writable<Type>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let storage: Storage | undefined = undefined;
|
||||||
|
if (browser || _mocked) {
|
||||||
|
storage = (options?.storage || 'local') == 'local' ? localStorage : sessionStorage;
|
||||||
|
initialValue = initialValueFromStorage(storage, key, initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const store: Writable<Type> = writable<Type>(initialValue);
|
||||||
|
|
||||||
|
stores[key] = {
|
||||||
|
set(value: Type) {
|
||||||
|
if (storage) {
|
||||||
|
updateStorage(storage, key, value);
|
||||||
|
}
|
||||||
|
store.set(value);
|
||||||
|
},
|
||||||
|
update(updater: (value: Type) => Type) {
|
||||||
|
const value = updater(get(store));
|
||||||
|
if (storage) {
|
||||||
|
updateStorage(storage, key, value);
|
||||||
|
}
|
||||||
|
store.set(value);
|
||||||
|
},
|
||||||
|
subscribe: store.subscribe
|
||||||
|
};
|
||||||
|
|
||||||
|
return stores[key] as Writable<Type>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user