UNPKG

@livetl/svelte-webext-stores

Version:

Svelte stores that synchronizes to WebExtension storage.

180 lines (163 loc) 4.97 kB
import type { IStorageBackend } from '../storage'; import { writable, Readable, Subscriber, Unsubscriber } from 'svelte/store'; /** Interface for stores that is synchronized to storage. */ export interface ISyncStore<T> extends Readable<T> { /** Get current value after updating from backend. */ get: () => Promise<T>; /** * Set value, inform subscribers, and push to storage. * @param value to set */ set: (value: T) => Promise<void>; /** * Set value, inform subscribers without pushing to storage. * @param value to set */ setRaw: (value: T) => void; /** * Get current value without updating from backend. * * Used for comparing storage changes when syncing from storage. */ getCurrent: () => T; /** * Whether store should be updated when storage value is updated externally, * e.g. storage value is changed by another page. */ readonly syncFromExternal: boolean; /** Storage item key. */ readonly key: string; } export interface VersionedOptions<T> { /** Current version number. Do not use `-1`. */ version: number; /** Separator between key and version. */ seperator: string; /** * Map for migrating values. * * Keys are the old version to match against, and values are callbacks that * accepts the value found from the given old version and returns the * corresponding value for the current version. * * Use the key `-1` to migrate from a versionless store. */ migrations?: Map<number, (oldValue: any) => T>; } /** Store that is synchronized to the storage backend. */ export interface SyncStore<T> extends ISyncStore<T> { /** * Ensure that any async initialization process (such as initial update * from backend) has been completed. * * You typically don't need to manually await unless you wish to sync * the store to the storage backend before any of `get()`, `set()` or * `subscribe()` is called. */ ready: () => Promise<void>; /** Reset store value to default value. */ reset: () => Promise<void>; /** * Update value using callback and inform subscribers. * @param updater callback */ update: (updater: (value: T) => T) => Promise<void>; } /** * Create a store that is synchronized to the storage backend. * @param key Storage key. * @param defaultValue Item default value. * @param backend Storage backend. * @param syncFromExternal Whether store should be updated when storage value * is updated externally. * @param versionedOptions Enables options for migrating storage values from an * older version to a newer version. */ export function syncStore<T>( key: string, defaultValue: T, backend: IStorageBackend, syncFromExternal: boolean, versionedOptions?: VersionedOptions<T> ): SyncStore<T> { let currentValue: T = defaultValue; let isReady = false; const store = writable(defaultValue); const keyPure = key; if (versionedOptions != null) { key = `${keyPure}${versionedOptions.seperator}${versionedOptions.version}`; } async function ready(): Promise<void> { if (isReady) return; await updateFromBackend(); if (versionedOptions?.migrations != null) { for (const [oldVersion, migrate] of versionedOptions.migrations.entries()) { const oldKey = oldVersion === -1 ? keyPure : `${keyPure}${versionedOptions.seperator}${oldVersion}`; const oldValue = await backend.get(oldKey); if (oldValue === undefined) continue; const newValue = migrate(oldValue); await set(newValue); await backend.remove(oldKey); } } isReady = true; } function setStore(value: T): void { store.set(value); currentValue = value; } async function get(): Promise<T> { await ready(); return currentValue; } function getCurrent(): T { return currentValue; } function setRaw(value: T): void { if (value === currentValue) return; setStore(value); } async function set(value: T): Promise<void> { if (value === currentValue) return; setStore(value); await backend.set(key, value); } async function updateFromBackend(): Promise<void> { const value = await backend.get<T>(key); if (value === undefined) { await backend.set(key, defaultValue); return; } setStore(value); } /** Reset store value to default value. */ async function reset(): Promise<void> { await set(defaultValue); } function subscribe(run: Subscriber<T>): Unsubscriber { ready() .then(() => { if (typeof run !== 'function') console.log(run); run(currentValue); }) .catch((e) => console.error(e)); return store.subscribe(run); } async function update(updater: (value: T) => T): Promise<void> { return await set(updater(currentValue)); } return { subscribe, get, set, setRaw, getCurrent, reset, ready, syncFromExternal, key, update }; }