UNPKG

seitu

Version:
191 lines (190 loc) • 5.9 kB
import { createReadableSubscription, createSubscription } from "../../core/subscription/index.mjs"; import { n as validateSchema } from "../../schema-store-Dfbr678J.mjs"; import { n as requestToPromise, r as transactionDone, t as createBroadcaster } from "../../indexed-db-request-MnNCk_ou.mjs"; import { deepEqual } from "fast-equals"; //#region src/web/indexed-db-storage/index.ts function createStorage(options, db, storeName) { const defaultValues = { ...options.defaultValues }; const keys = Object.keys(options.defaultValues); const isSupported = typeof indexedDB !== "undefined"; const databaseName = db["~"].name; const label = `createIndexedDbStorage:${databaseName}/${storeName}`; const { broadcast, listen } = createBroadcaster(`seitu:indexed-db:${databaseName}:${storeName}`); const resolveKey = (key) => String(options.keyTransform ? options.keyTransform(key) : key); let cache = { ...options.defaultValues }; let writeVersion = 0; const keyWriteVersion = /* @__PURE__ */ new Map(); let hydrateSeq = 0; const markWritten = (written) => { writeVersion++; for (const key of written) keyWriteVersion.set(key, writeVersion); }; let triggerHydrate = () => {}; const { subscribe, notify } = createSubscription({ onFirstSubscribe: () => { triggerHydrate(); return listen(() => triggerHydrate()); } }); const validateValue = (key, raw) => { if (raw === void 0) return options.defaultValues[key]; return validateSchema(options.schemas[key], raw, { defaultValue: options.defaultValues[key], label: `createIndexedDbStorage:${String(key)}`, key, onError: options.onValidationError }); }; const hydrate = async () => { if (!isSupported) return cache; const seq = ++hydrateSeq; const startVersion = writeVersion; const store = (await db["~"].getDatabase()).transaction(storeName, "readonly").objectStore(storeName); const persisted = {}; await Promise.all(keys.map(async (key) => { persisted[key] = validateValue(key, await requestToPromise(store.get(resolveKey(key)))); })); if (seq !== hydrateSeq) return cache; const next = {}; for (const key of keys) next[key] = (keyWriteVersion.get(key) ?? 0) > startVersion ? cache[key] : persisted[key]; if (!deepEqual(cache, next)) { cache = next; notify(); } return cache; }; const safeHydrate = () => hydrate().catch((error) => { console.warn(`[${label}] Failed to read from IndexedDB, using cached values.`, error); return cache; }); let ready; let inFlight; const rehydrate = () => { if (!inFlight) { const run = safeHydrate().finally(() => { if (inFlight === run) inFlight = void 0; }); inFlight = run; ready = run; } return inFlight; }; const ensureReady = () => ready ?? rehydrate(); triggerHydrate = () => void rehydrate(); const get = () => { ensureReady(); return cache; }; const readable = createReadableSubscription(get, subscribe, notify, () => defaultValues); const persist = async (mutate) => { if (!isSupported) return; try { const transaction = (await db["~"].getDatabase()).transaction(storeName, "readwrite"); mutate(transaction.objectStore(storeName)); await transactionDone(transaction); broadcast(); } catch (error) { console.warn(`[${label}] Failed to write to IndexedDB.`, error); } }; return { ...readable, set: (value) => { ensureReady(); const resolved = typeof value === "function" ? value(cache) : value; markWritten(Object.keys(resolved)); cache = { ...cache, ...resolved }; notify(); return persist((store) => { for (const [key, entry] of Object.entries(resolved)) store.put(entry, resolveKey(key)); }); }, clear: () => { ensureReady(); markWritten(keys); cache = { ...options.defaultValues }; notify(); return persist((store) => { for (const key of keys) store.delete(resolveKey(key)); }); }, hydrate: rehydrate, db, storeName, "~": { ...readable["~"], getDefaultValue: (key) => defaultValues[key], getSchema: (key) => options.schemas[key], transformKey: resolveKey } }; } /** * Key/value store for `createIndexedDb({ stores })`. `get()` reads an * in-memory cache hydrated from IndexedDB (`await db.ready`). `set`/`clear` * update the cache now and persist later. SSR snapshot is `defaultValues`. * * @example Vanilla * ```ts twoslash title="settings-storage.ts" * import { createIndexedDb, createIndexedDbStorage } from 'seitu/web' * import * as z from 'zod' * * const db = createIndexedDb({ * name: 'app', * stores: { * settings: createIndexedDbStorage({ * schemas: { * token: z.string().nullable(), * preferences: z.object({ theme: z.enum(['light', 'dark']) }), * }, * defaultValues: { token: null, preferences: { theme: 'light' } }, * }), * }, * }) * const { settings } = db.stores * * settings.get() * await db.ready * await settings.set({ token: 'abc' }) * settings.get() // { token: 'abc', preferences: { theme: 'light' } } * settings.subscribe(console.log) * ``` * * @example React * ```tsx twoslash title="page.tsx" * 'use client' * * import { createIndexedDb, createIndexedDbStorage } from 'seitu/web' * import { useSubscription } from 'seitu/react' * import * as z from 'zod' * * const db = createIndexedDb({ * name: 'app', * stores: { * settings: createIndexedDbStorage({ * schemas: { count: z.number(), name: z.string() }, * defaultValues: { count: 0, name: '' }, * }), * }, * }) * * export default function Page() { * const value = useSubscription(db.stores.settings) * return ( * <div> * <span>{value.count}</span> * <span>{value.name}</span> * </div> * ) * } * ``` */ function createIndexedDbStorage(options) { return { "~": { definition: {}, create: (db, storeName) => createStorage(options, db, storeName) } }; } //#endregion export { createIndexedDbStorage };