UNPKG

seitu

Version:
187 lines (186 loc) • 6.25 kB
import { createReadableSubscription, createSubscription } from "../../core/subscription/index.mjs"; import { n as validateSchema, r as validateSync } 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-table/index.ts var IndexedDbTableValidationError = class extends Error { issues; value; constructor(label, issues, value) { super(`[${label}] Row failed validation: ${issues.map((issue) => issue.message).join("; ")}`); this.name = "IndexedDbTableValidationError"; this.issues = issues; this.value = value; } }; function createTable(options, db, storeName) { const isSupported = typeof indexedDB !== "undefined"; const databaseName = db["~"].name; const label = `createIndexedDbTable:${databaseName}/${storeName}`; const { broadcast, listen } = createBroadcaster(`seitu:indexed-db:${databaseName}:${storeName}`); const { subscribe, notify } = createSubscription({ onFirstSubscribe: () => listen(() => notify()) }); const validateRow = (raw) => validateSchema(options.schema, raw, { defaultValue: void 0, label, onError: options.onValidationError }); const validateForWrite = (raw) => { const result = validateSync(options.schema, raw, label); if (result.issues) throw new IndexedDbTableValidationError(label, result.issues, raw); return result.value; }; const read = async (run) => { const store = (await db["~"].getDatabase()).transaction(storeName, "readonly").objectStore(storeName); return requestToPromise(run(store)); }; const write = async (run) => { const transaction = (await db["~"].getDatabase()).transaction(storeName, "readwrite"); run(transaction.objectStore(storeName)); await transactionDone(transaction); notify(); broadcast(); }; const createReader = (source) => ({ get: async (key) => validateRow(await read((store) => source(store).get(key))), getAll: async (query, count) => (await read((store) => source(store).getAll(query, count))).map(validateRow).filter((row) => row !== void 0), getAllKeys: (query, count) => read((store) => source(store).getAllKeys(query, count)), count: (query) => read((store) => source(store).count(query ?? void 0)) }); const reader = { ...createReader((store) => store), index: (name) => createReader((store) => store.index(name)) }; const query = (run, queryOptions) => { const initial = queryOptions?.initial; let cache = initial; let seq = 0; const querySubscription = createSubscription({ onFirstSubscribe: () => { const unsubscribe = subscribe(() => void refresh()); refresh(); return unsubscribe; } }); async function refresh() { if (!isSupported) return cache; const current = ++seq; let next; try { next = await run(reader); } catch (error) { console.warn(`[${label}] Query failed, keeping the cached value.`, error); return cache; } if (current !== seq) return cache; if (!deepEqual(cache, next)) { cache = next; querySubscription.notify(); } return cache; } return { ...createReadableSubscription(() => cache, querySubscription.subscribe, querySubscription.notify, () => initial), ready: refresh(), refresh }; }; return { ...reader, put: async (input, key) => { if (Array.isArray(input) && key !== void 0) throw new TypeError(`[${label}] An explicit key can only be used with a single row.`); const validated = (Array.isArray(input) ? input : [input]).map(validateForWrite); await write((store) => { for (const row of validated) store.put(row, key); }); }, delete: (keys) => write((store) => { for (const key of Array.isArray(keys) ? keys : [keys]) store.delete(key); }), clear: () => write((store) => store.clear()), query, db, storeName, "~": { schema: options.schema, subscribe, notify } }; } /** * Row store for `createIndexedDb({ stores })`: keyed rows, indexes, range * reads, validation. Use `query()` for a `Readable`/`Subscribable` that * re-runs on table changes. On the server, queries stay on `initial`. * * Key paths must name a schema field that can hold a key, and reads are typed * from that field. Compound (`['id', 'order']`) and nested (`'meta.slug'`) * paths fall back to `IDBValidKey`. * * @example Vanilla * ```ts twoslash title="todos.ts" * import { createIndexedDb, createIndexedDbTable } from 'seitu/web' * import * as z from 'zod' * * const db = createIndexedDb({ * name: 'app', * stores: { * todos: createIndexedDbTable({ * keyPath: 'id', * indexes: { status: 'status' }, * schema: z.object({ id: z.string(), title: z.string(), status: z.enum(['open', 'done']) }), * }), * }, * }) * const { todos } = db.stores * * await todos.put({ id: '1', title: 'Write docs', status: 'open' }) * await todos.get('1') * await todos.getAll() * await todos.index('status').getAll('open') // index names and keys are typed * await todos.delete('1') * * const open = todos.query(t => t.index('status').getAll('open'), { initial: [] }) * open.get() * open.subscribe(rows => console.log(rows)) * ``` * * @example React * ```tsx twoslash title="page.tsx" * 'use client' * * import { createIndexedDb, createIndexedDbTable } from 'seitu/web' * import { useSubscription } from 'seitu/react' * import * as z from 'zod' * * const db = createIndexedDb({ * name: 'app', * stores: { * todos: createIndexedDbTable({ * keyPath: 'id', * schema: z.object({ id: z.string(), title: z.string(), status: z.enum(['open', 'done']) }), * }), * }, * }) * * const allTodos = db.stores.todos.query(t => t.getAll(), { initial: [] }) * * export default function Page() { * const rows = useSubscription(allTodos) * return ( * <ul> * {rows.map(todo => <li key={todo.id}>{todo.title}</li>)} * </ul> * ) * } * ``` */ function createIndexedDbTable(options) { return { "~": { definition: { keyPath: options.keyPath, autoIncrement: options.autoIncrement, indexes: options.indexes }, create: (db, storeName) => createTable(options, db, storeName) } }; } //#endregion export { IndexedDbTableValidationError, createIndexedDbTable };