ultisuite-client/lib/stores/debounced-json-storage.ts
2026-05-15 23:51:57 +02:00

101 lines
2.7 KiB
TypeScript

"use client"
import {
createJSONStorage,
type PersistStorage,
type StateStorage,
type StorageValue,
} from "zustand/middleware"
const DEFAULT_DEBOUNCE_MS = 220
/** In-memory fallback when `localStorage` is missing (SSR) or throws (private mode, etc.). */
function createMemoryStateStorage(): StateStorage {
const data = new Map<string, string>()
return {
getItem: (name) => data.get(name) ?? null,
setItem: (name, value) => {
data.set(name, value)
},
removeItem: (name) => {
data.delete(name)
},
}
}
function getPersistBackingStorage(): StateStorage {
if (typeof window === "undefined") {
return createMemoryStateStorage()
}
try {
return window.localStorage
} catch {
return createMemoryStateStorage()
}
}
/**
* JSON persist storage that debounces writes to `localStorage` so rapid
* store updates do not block the main thread on every mutation.
* Flushes pending keys on a timer, `beforeunload`, and `pagehide`.
* Uses in-memory storage during SSR or when `localStorage` is unavailable.
*/
function buildDebouncedJsonStorage(): PersistStorage<unknown> {
const base =
createJSONStorage(getPersistBackingStorage) ??
createJSONStorage(() => createMemoryStateStorage())
if (!base) {
throw new Error("[debounced-json-storage] failed to create JSON storage")
}
const pending = new Map<string, StorageValue<unknown>>()
const timers = new Map<string, ReturnType<typeof globalThis.setTimeout>>()
const flushKey = (name: string) => {
const t = timers.get(name)
if (t !== undefined) {
globalThis.clearTimeout(t)
timers.delete(name)
}
const value = pending.get(name)
if (value === undefined) return
pending.delete(name)
base.setItem(name, value)
}
const flushAll = () => {
for (const name of [...pending.keys()]) flushKey(name)
}
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", flushAll)
window.addEventListener("pagehide", flushAll)
}
return {
getItem: (name) => base.getItem(name),
setItem: (name, value) => {
pending.set(name, value)
const existing = timers.get(name)
if (existing !== undefined) globalThis.clearTimeout(existing)
const id = globalThis.setTimeout(() => {
timers.delete(name)
flushKey(name)
}, DEFAULT_DEBOUNCE_MS)
timers.set(name, id)
},
removeItem: (name) => {
const existing = timers.get(name)
if (existing !== undefined) {
globalThis.clearTimeout(existing)
timers.delete(name)
}
pending.delete(name)
return base.removeItem(name)
},
}
}
/** Shared instance for all zustand `persist` stores in this app. */
export const debouncedPersistJSONStorage = buildDebouncedJsonStorage()