diff --git a/src/index.ts b/src/index.ts index 963520d..b5577dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,6 @@ export * from "./lib/binding/useBehaviorSubject" export * from "./lib/state/signal" export * from "./lib/state/useSignalValue" export * from "./lib/state/constants" -export * from "./lib/state/persistance/adapters/createSharedStoreAdapter" export * from "./lib/state/persistance/adapters/createLocalforageAdapter" export * from "./lib/state/persistance/adapters/createLocalStorageAdapter" export * from "./lib/state/persistance/usePersistSignals" diff --git a/src/lib/state/persistance/adapters/createLocalStorageAdapter.ts b/src/lib/state/persistance/adapters/createLocalStorageAdapter.ts index ea0206e..dfc7b14 100644 --- a/src/lib/state/persistance/adapters/createLocalStorageAdapter.ts +++ b/src/lib/state/persistance/adapters/createLocalStorageAdapter.ts @@ -1,13 +1,65 @@ -export const createLocalStorageAdapter = () => ({ - getItem: async (key: string) => { - const serializedValue = localStorage.getItem(key) +import { type Adapter } from "../types" - if (!serializedValue) return undefined +const normalizeStore = (store: unknown) => { + if (!store || typeof store !== "object") { + return undefined + } + + return store +} + +/** + * Create an adapter which use one unique store entry to store all + * state. When using many signals it can help with maintenance to keep things + * tidy. + */ +const createSharedStoreAdapter = ({ + adapter, + key +}: { + adapter: Adapter + key: string +}): Adapter => ({ + getItem: async (itemKey: string) => { + const unsafeStore = await adapter.getItem(key) + const store = normalizeStore(unsafeStore) ?? {} - return JSON.parse(serializedValue) + if (itemKey in store) { + return store[itemKey as keyof typeof store] + } + + return undefined }, - setItem: async (key: string, value: unknown) => { - localStorage.setItem(key, JSON.stringify(value)) + setItem: async (itemKey: string, value: unknown) => { + const unsafeStore = await adapter.getItem(key) + const store = normalizeStore(unsafeStore) ?? {} + + await adapter.setItem(key, { ...store, [itemKey]: value }) } }) + +export const createLocalStorageAdapter = ({ + key +}: { key?: string } = {}): Adapter => { + if (key) { + return createSharedStoreAdapter({ + adapter: createLocalStorageAdapter(), + key + }) + } + + return { + getItem: async (key: string) => { + const serializedValue = localStorage.getItem(key) + + if (!serializedValue) return undefined + + return JSON.parse(serializedValue) + }, + + setItem: async (key: string, value: unknown) => { + localStorage.setItem(key, JSON.stringify(value)) + } + } +} diff --git a/src/lib/state/persistance/adapters/createSharedStoreAdapter.ts b/src/lib/state/persistance/adapters/createSharedStoreAdapter.ts deleted file mode 100644 index ea8cad4..0000000 --- a/src/lib/state/persistance/adapters/createSharedStoreAdapter.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { type Adapter } from "../types" - -const normalizeStore = (store: unknown) => { - if (!store || typeof store !== "object") { - return undefined - } - - return store -} - -/** - * Create an adapter which use one unique store entry to store all - * state. When using many signals it can help with maintenance to keep things - * tidy. - */ -export const createSharedStoreAdapter = ({ - adapter, - key -}: { - adapter: Adapter - key: string -}) => ({ - getItem: async (itemKey: string) => { - const unsafeStore = await adapter.getItem(key) - const store = normalizeStore(unsafeStore) ?? {} - - if (itemKey in store) { - return store[itemKey as keyof typeof store] - } - - return undefined - }, - - setItem: async (itemKey: string, value: unknown) => { - const unsafeStore = await adapter.getItem(key) - const store = normalizeStore(unsafeStore) ?? {} - - await adapter.setItem(key, { ...store, [itemKey]: value }) - } -}) diff --git a/src/lib/state/persistance/usePersistSignals.tsx b/src/lib/state/persistance/usePersistSignals.tsx index ab0a241..0f5a2e2 100644 --- a/src/lib/state/persistance/usePersistSignals.tsx +++ b/src/lib/state/persistance/usePersistSignals.tsx @@ -7,6 +7,7 @@ import { from, map, merge, + mergeMap, of, switchMap, tap, @@ -20,7 +21,7 @@ import type { Signal } from "../signal" import { getNormalizedPersistanceValue } from "./getNormalizedPersistanceValue" import { IDENTIFIER_PERSISTANCE_KEY } from "./constants" -const persistValue = async ({ +const persistValue = ({ adapter, signal, version @@ -37,7 +38,13 @@ const persistValue = async ({ migrationVersion: version } satisfies PersistanceEntry - await adapter.setItem(signal.config.key, value) + return from(adapter.setItem(signal.config.key, value)).pipe( + catchError((e) => { + console.error(e) + + return of(null) + }) + ) } const hydrateValueToSignal = ({ @@ -69,14 +76,50 @@ const hydrateValueToSignal = ({ ) } +const useInvalidateStorage = ({ + adapter, + entries, + storageKey +}: { + entries: { + current: Array<{ version: number; signal: Signal }> + } + adapter: { + current: Adapter + } + storageKey?: string +}) => { + useSubscribe( + () => + !storageKey + ? EMPTY + : merge( + ...entries.current.map(({ signal, version }) => + persistValue({ + adapter: adapter.current, + signal, + version + }) + ) + ), + [storageKey] + ) +} + export const usePersistSignals = ({ entries = [], onReady, - adapter = createLocalStorageAdapter() + adapter = createLocalStorageAdapter(), + storageKey }: { entries?: Array<{ version: number; signal: Signal }> onReady?: () => void adapter?: Adapter + /** + * Can be used to invalidate current storage + * resulting on a re-persist of all values. + */ + storageKey?: string }) => { const entriesRef = useLiveRef(entries) const onReadyRef = useLiveRef(onReady) @@ -95,7 +138,15 @@ export const usePersistSignals = ({ adapter: adapterRef.current, signal, version - }) + }).pipe( + mergeMap(() => + persistValue({ + adapter: adapterRef.current, + signal, + version + }) + ) + ) ) ).pipe(map(() => true)) @@ -123,15 +174,17 @@ export const usePersistSignals = ({ throttleTime(500, asyncScheduler, { trailing: true }), - switchMap(() => - from( + switchMap(() => { + return from( persistValue({ adapter: adapterRef.current, signal, version }) ) - ) + }) ) ) ) }, [isHydrated, adapterRef]) + useInvalidateStorage({ adapter: adapterRef, entries: entriesRef, storageKey }) + return { isHydrated } }