1import { Debouncer } from "@tanstack/react-pacer";
2
3export interface StateStorage<R = unknown> {
4 getItem: (name: string) => string | null | Promise<string | null>;
5 setItem: (name: string, value: string) => R;
6 removeItem: (name: string) => R;
7}
8
9export interface DeferredStorage<TValue> {
10 getItem: (name: string) => string | null | Promise<string | null>;
11 setItem: (name: string, value: TValue) => void;
12 removeItem: (name: string) => void;
13 flush: () => void;
14}
15
16export function createMemoryStorage(): StateStorage {
17 const store = new Map<string, string>();
18 return {
19 getItem: (name) => store.get(name) ?? null,
20 setItem: (name, value) => {
21 store.set(name, value);
22 },
23 removeItem: (name) => {
24 store.delete(name);
25 },
26 };
27}
28
29function isStateStorage(
30 storage: Partial<StateStorage> | null | undefined,
31): storage is StateStorage {
32 return (
33 storage !== null &&
34 storage !== undefined &&
35 typeof storage.getItem === "function" &&
36 typeof storage.setItem === "function" &&
37 typeof storage.removeItem === "function"
38 );
39}
40
41export function resolveStorage(storage: Partial<StateStorage> | null | undefined): StateStorage {
42 return isStateStorage(storage) ? storage : createMemoryStorage();
43}
44
45/** Keep the latest value and serialize it when the debounce fires or `flush` runs. */
46export function createDeferredStorage<TValue>(
47 baseStorage: Partial<StateStorage> | null | undefined,
48 serialize: (value: TValue) => string,
49 debounceMs: number = 300,
50): DeferredStorage<TValue> {
51 const resolvedStorage = resolveStorage(baseStorage);
52 const debouncedSetItem = new Debouncer(
53 (name: string, value: TValue) => {
54 resolvedStorage.setItem(name, serialize(value));
55 },
56 { wait: debounceMs },
57 );
58
59 return {
60 getItem: (name) => resolvedStorage.getItem(name),
61 setItem: (name, value) => {
62 debouncedSetItem.maybeExecute(name, value);
63 },
64 removeItem: (name) => {
65 debouncedSetItem.cancel();
66 // cancel() leaves the captured value in Pacer's lastArgs.
67 debouncedSetItem.reset();
68 resolvedStorage.removeItem(name);
69 },
70 flush: () => {
71 debouncedSetItem.flush();
72 },
73 };
74}