@vielzeug/vault 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +74 -0
  2. package/dist/_dev.cjs +2 -0
  3. package/dist/_dev.cjs.map +1 -0
  4. package/dist/_dev.d.ts +2 -0
  5. package/dist/_dev.d.ts.map +1 -0
  6. package/dist/_dev.js +12 -0
  7. package/dist/_dev.js.map +1 -0
  8. package/dist/adapter-core.cjs +2 -0
  9. package/dist/adapter-core.cjs.map +1 -0
  10. package/dist/adapter-core.d.ts +8 -0
  11. package/dist/adapter-core.d.ts.map +1 -0
  12. package/dist/adapter-core.js +269 -0
  13. package/dist/adapter-core.js.map +1 -0
  14. package/dist/adapters/indexeddb.cjs +2 -0
  15. package/dist/adapters/indexeddb.cjs.map +1 -0
  16. package/dist/adapters/indexeddb.d.ts +13 -0
  17. package/dist/adapters/indexeddb.d.ts.map +1 -0
  18. package/dist/adapters/indexeddb.js +336 -0
  19. package/dist/adapters/indexeddb.js.map +1 -0
  20. package/dist/adapters/memory.cjs +2 -0
  21. package/dist/adapters/memory.cjs.map +1 -0
  22. package/dist/adapters/memory.d.ts +14 -0
  23. package/dist/adapters/memory.d.ts.map +1 -0
  24. package/dist/adapters/memory.js +200 -0
  25. package/dist/adapters/memory.js.map +1 -0
  26. package/dist/adapters/webstorage.cjs +2 -0
  27. package/dist/adapters/webstorage.cjs.map +1 -0
  28. package/dist/adapters/webstorage.d.ts +14 -0
  29. package/dist/adapters/webstorage.d.ts.map +1 -0
  30. package/dist/adapters/webstorage.js +182 -0
  31. package/dist/adapters/webstorage.js.map +1 -0
  32. package/dist/errors.cjs +2 -0
  33. package/dist/errors.cjs.map +1 -0
  34. package/dist/errors.d.ts +22 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +17 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/index.cjs +1 -0
  39. package/dist/index.d.ts +17 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +11 -0
  42. package/dist/internal.cjs +2 -0
  43. package/dist/internal.cjs.map +1 -0
  44. package/dist/internal.d.ts +50 -0
  45. package/dist/internal.d.ts.map +1 -0
  46. package/dist/internal.js +125 -0
  47. package/dist/internal.js.map +1 -0
  48. package/dist/migration.cjs +2 -0
  49. package/dist/migration.cjs.map +1 -0
  50. package/dist/migration.d.ts +37 -0
  51. package/dist/migration.d.ts.map +1 -0
  52. package/dist/migration.js +27 -0
  53. package/dist/migration.js.map +1 -0
  54. package/dist/prune.cjs +2 -0
  55. package/dist/prune.cjs.map +1 -0
  56. package/dist/prune.d.ts +32 -0
  57. package/dist/prune.d.ts.map +1 -0
  58. package/dist/prune.js +20 -0
  59. package/dist/prune.js.map +1 -0
  60. package/dist/query.cjs +2 -0
  61. package/dist/query.cjs.map +1 -0
  62. package/dist/query.d.ts +113 -0
  63. package/dist/query.d.ts.map +1 -0
  64. package/dist/query.js +126 -0
  65. package/dist/query.js.map +1 -0
  66. package/dist/streaming.cjs +2 -0
  67. package/dist/streaming.cjs.map +1 -0
  68. package/dist/streaming.d.ts +22 -0
  69. package/dist/streaming.d.ts.map +1 -0
  70. package/dist/streaming.js +21 -0
  71. package/dist/streaming.js.map +1 -0
  72. package/dist/ttl.cjs +2 -0
  73. package/dist/ttl.cjs.map +1 -0
  74. package/dist/ttl.d.ts +74 -0
  75. package/dist/ttl.d.ts.map +1 -0
  76. package/dist/ttl.js +32 -0
  77. package/dist/ttl.js.map +1 -0
  78. package/dist/types.cjs +2 -0
  79. package/dist/types.cjs.map +1 -0
  80. package/dist/types.d.ts +409 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +27 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/vault.cjs +2 -0
  85. package/dist/vault.cjs.map +1 -0
  86. package/dist/vault.iife.js +2 -0
  87. package/dist/vault.iife.js.map +1 -0
  88. package/dist/vault.js +2 -0
  89. package/dist/vault.js.map +1 -0
  90. package/dist/versioned-codec.cjs +2 -0
  91. package/dist/versioned-codec.cjs.map +1 -0
  92. package/dist/versioned-codec.d.ts +44 -0
  93. package/dist/versioned-codec.d.ts.map +1 -0
  94. package/dist/versioned-codec.js +32 -0
  95. package/dist/versioned-codec.js.map +1 -0
  96. package/package.json +41 -0
@@ -0,0 +1,2 @@
1
+ const e=require("./_dev.cjs"),t=require("./errors.cjs");var n=`\0`;function r(e){return`${encodeURIComponent(e)}${n}`}function i(e,t,r){return`${encodeURIComponent(e)}${n}${encodeURIComponent(t)}${n}${encodeURIComponent(r)}`}function a(e,t){return`${encodeURIComponent(e)}${n}${encodeURIComponent(t)}${n}`}function o(e,t){if(!t)return;let i=r(e);if(!t.startsWith(i))return;let a=t.slice(i.length),o=a.indexOf(n);if(o!==-1)try{return decodeURIComponent(a.slice(0,o))}catch{return}}function s(n,r){let i=new Map,a=!1,o=t=>{r?r(t):e.error(`observer notification failed`,t)},s=e=>{if(a)return;let t=i.get(e);!t||t.size===0||n(e).then(t=>{if(a)return;let n=i.get(e);if(!(!n||n.size===0))for(let e of n)try{e(t)}catch(e){o(e)}}).catch(e=>o(e))};return{dispose:()=>{a=!0,i.clear()},notify:s,observe:(e,n,{immediate:r=!0,signal:o}={})=>{if(a)throw new t.VaultDisposedError(`observer hub is disposed`);if(o?.aborted)return()=>{};let c=e,l=n,u=i.get(c);u||(u=new Set,i.set(c,u)),u.add(l),r&&s(e);let d=()=>{let e=i.get(c);e&&(e.delete(l),e.size===0&&i.delete(c))};return o?.addEventListener(`abort`,d,{once:!0}),d}}}function c(e,t=`latest`,n){return{[Symbol.asyncIterator](){let r=[],i=null,a=n?.aborted??!1,o=null,s=()=>{a||(a=!0,o?.(),o=null,i&&=(i({done:!0,value:void 0}),null))};return n?.addEventListener(`abort`,s,{once:!0}),a||(o=e(e=>{if(!a)if(i){let t=i;i=null,t({done:!1,value:e})}else t===`all`?r.push(e):r=[e]})),{async next(){return a?{done:!0,value:void 0}:r.length>0?{done:!1,value:r.shift()}:new Promise(e=>{i=e})},async return(){return s(),{done:!0,value:void 0}},async throw(e){return s(),Promise.reject(e)}}}}}function l(n){return function(r,i,{eager:a=!1,signal:o}={}){let s=[...new Set(r)];if(s.length===0)throw new t.VaultScopeError(`observeMany requires at least one table`);if(s.length<r.length&&e.warn(`observeMany: duplicate table names were ignored`),o?.aborted)return()=>{};let c=new Map,l=!1,u=!1,d=()=>Object.fromEntries(r.map(e=>[e,c.get(e)??[]])),f=()=>{l||!a&&c.size<s.length||(l=!0,queueMicrotask(()=>{l=!1,u||i(d())}))},p=s.map(e=>n.observe(e,t=>{c.set(e,t),f()})),m=()=>{u=!0;for(let e of p)e()};return o?.addEventListener(`abort`,m,{once:!0}),m}}function u(e,n,r){let i=String(e[n].key),a=r[i];if(a==null)throw new t.VaultError(`key field "${i}" in table "${String(n)}" must be a non-null value, got ${String(a)}`);return a}exports.createObserveMany=l,exports.createObserverHub=s,exports.createWatchIterable=c,exports.decodeStorageTableFromKey=o,exports.encodeDbPrefix=r,exports.encodeStorageKey=i,exports.encodeStorageTablePrefix=a,exports.getRecordKey=u;
2
+ //# sourceMappingURL=internal.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.cjs","names":[],"sources":["../src/internal.ts"],"sourcesContent":["import type { AnySchema, KeyOf, RecordOf } from './types';\n\nimport { error as logError, warn } from './_dev';\nimport { VaultDisposedError, VaultError, VaultScopeError } from './errors';\n\ntype ObserverListener<T> = (records: T[]) => void;\n\n/**\n * Separator used between encoded key segments. The null byte (\\x00) is safe\n * because `encodeURIComponent` always percent-encodes it to `%00` (a 3-character\n * string), so the raw `\\x00` byte can never appear inside an encoded segment —\n * eliminating key-collision risk that a URL-safe character like `~` would carry.\n */\nconst SEP = '\\x00';\n\nexport function encodeDbPrefix(dbName: string): string {\n return `${encodeURIComponent(dbName)}${SEP}`;\n}\n\nexport function encodeStorageKey(dbName: string, table: string, key: string): string {\n return `${encodeURIComponent(dbName)}${SEP}${encodeURIComponent(table)}${SEP}${encodeURIComponent(key)}`;\n}\n\nexport function encodeStorageTablePrefix(dbName: string, table: string): string {\n return `${encodeURIComponent(dbName)}${SEP}${encodeURIComponent(table)}${SEP}`;\n}\n\nexport function decodeStorageTableFromKey(dbName: string, storageKey: string | null): string | undefined {\n if (!storageKey) return undefined;\n\n const prefix = encodeDbPrefix(dbName);\n\n if (!storageKey.startsWith(prefix)) return undefined;\n\n const tail = storageKey.slice(prefix.length);\n const end = tail.indexOf(SEP);\n\n if (end === -1) return undefined;\n\n try {\n return decodeURIComponent(tail.slice(0, end));\n } catch {\n return undefined;\n }\n}\n\nexport function createObserverHub<S extends AnySchema>(\n getAll: <K extends keyof S & string>(table: K) => Promise<RecordOf<S, K>[]>,\n onError?: (error: unknown) => void,\n) {\n const observers = new Map<string, Set<ObserverListener<unknown>>>();\n let disposed = false;\n\n const reportObserverError = (error: unknown): void => {\n if (onError) {\n onError(error);\n } else {\n logError('observer notification failed', error);\n }\n };\n\n const notify = <K extends keyof S & string>(table: K): void => {\n if (disposed) return;\n\n const listeners = observers.get(table);\n\n if (!listeners || listeners.size === 0) return;\n\n void getAll(table)\n .then((records) => {\n if (disposed) return;\n\n const current = observers.get(table);\n\n if (!current || current.size === 0) return;\n\n for (const listener of current) {\n try {\n listener(records as unknown[]);\n } catch (error) {\n reportObserverError(error);\n }\n }\n })\n .catch((error) => reportObserverError(error));\n };\n\n const observe = <K extends keyof S & string>(\n table: K,\n listener: (records: RecordOf<S, K>[]) => void,\n { immediate = true, signal }: { immediate?: boolean; signal?: AbortSignal } = {},\n ): (() => void) => {\n if (disposed) {\n throw new VaultDisposedError('observer hub is disposed');\n }\n\n if (signal?.aborted) return () => {};\n\n const key = table;\n const wrapped = listener as ObserverListener<unknown>;\n let listeners = observers.get(key);\n\n if (!listeners) {\n listeners = new Set();\n observers.set(key, listeners);\n }\n\n listeners.add(wrapped);\n\n // Fire an initial snapshot unless the caller opted out.\n if (immediate) notify(table);\n\n const stop = (): void => {\n const current = observers.get(key);\n\n if (!current) return;\n\n current.delete(wrapped);\n\n if (current.size === 0) observers.delete(key);\n };\n\n signal?.addEventListener('abort', stop, { once: true });\n\n return stop;\n };\n\n const dispose = (): void => {\n disposed = true;\n observers.clear();\n };\n\n return { dispose, notify, observe };\n}\n\n/**\n * Creates an AsyncIterable that yields a snapshot on every mutation.\n * The first snapshot is emitted immediately (on the first `next()` call).\n *\n * `mode` controls back-pressure:\n * - `'latest'` (default): if the consumer lags, only the most recent snapshot is retained.\n * Intermediate states are discarded. Best for rendering/display use-cases.\n * - `'all'`: every snapshot is queued and delivered in order. Use when every intermediate\n * state matters (audit trails, animation).\n *\n * Pass an `AbortSignal` to cancel the iteration externally:\n * ```ts\n * const controller = new AbortController();\n * for await (const users of db.watch('users', { signal: controller.signal })) { ... }\n * controller.abort(); // stops the loop\n * ```\n *\n * **Resource note:** The observer subscription is created eagerly on `[Symbol.asyncIterator]()` to\n * avoid missing mutations before the first `next()` call. Always consume the iterator to completion,\n * `break`/`return` out of `for await`, or pass an `AbortSignal` — otherwise the subscription\n * will remain active until the adapter is disposed.\n */\nexport function createWatchIterable<T>(\n subscribe: (listener: (snapshot: T[]) => void, signal?: AbortSignal) => () => void,\n mode: 'all' | 'latest' = 'latest',\n signal?: AbortSignal,\n): AsyncIterable<T[]> {\n return {\n [Symbol.asyncIterator]() {\n let pending: T[][] = [];\n let waiting: ((value: IteratorResult<T[]>) => void) | null = null;\n let done = signal?.aborted ?? false;\n let unsubscribe: (() => void) | null = null;\n\n const finish = (): void => {\n if (done) return;\n\n done = true;\n unsubscribe?.();\n unsubscribe = null;\n\n if (waiting) {\n waiting({ done: true, value: undefined });\n waiting = null;\n }\n };\n\n signal?.addEventListener('abort', finish, { once: true });\n\n // Eager registration: subscribe immediately so mutations between iterator\n // creation and first next() are not silently lost.\n if (!done) {\n unsubscribe = subscribe((snapshot) => {\n if (done) return;\n\n if (waiting) {\n const resolve = waiting;\n\n waiting = null;\n resolve({ done: false, value: snapshot });\n } else if (mode === 'all') {\n pending.push(snapshot);\n } else {\n pending = [snapshot];\n }\n });\n }\n\n return {\n async next(): Promise<IteratorResult<T[]>> {\n if (done) return { done: true, value: undefined };\n\n if (pending.length > 0) {\n const value = pending.shift()!;\n\n return { done: false, value };\n }\n\n return new Promise<IteratorResult<T[]>>((resolve) => {\n waiting = resolve;\n });\n },\n\n async return(): Promise<IteratorResult<T[]>> {\n finish();\n\n return { done: true, value: undefined };\n },\n\n async throw(err?: unknown): Promise<IteratorResult<T[]>> {\n finish();\n\n return Promise.reject(err);\n },\n };\n },\n };\n}\n\n/**\n * Observes multiple tables simultaneously. The listener receives a combined snapshot\n * `{ [tableName]: RecordOf<S, T>[] }` and fires once after all tables have delivered\n * their first snapshot. Subsequent firings are coalesced per microtask.\n *\n * Since `observe()` always fires immediately, each per-table observer populates the\n * snapshotMap on registration. No separate prefetch step is needed.\n */\nexport function createObserveMany<S extends AnySchema>(hub: ReturnType<typeof createObserverHub<S>>) {\n return function observeMany<K extends keyof S & string>(\n tables: readonly K[],\n listener: (snapshots: { [T in K]: RecordOf<S, T>[] }) => void,\n { eager = false, signal }: { eager?: boolean; signal?: AbortSignal } = {},\n ): () => void {\n const distinctTables = [...new Set(tables)] as K[];\n\n if (distinctTables.length === 0) throw new VaultScopeError('observeMany requires at least one table');\n\n if (distinctTables.length < tables.length) {\n warn('observeMany: duplicate table names were ignored');\n }\n\n if (signal?.aborted) return () => {};\n\n const snapshotMap = new Map<string, RecordOf<S, K>[]>();\n let microtaskQueued = false;\n let stopped = false;\n\n const buildCombined = (): { [T in K]: RecordOf<S, T>[] } =>\n Object.fromEntries(tables.map((t) => [t, snapshotMap.get(t) ?? []])) as { [T in K]: RecordOf<S, T>[] };\n\n const scheduleFlush = (): void => {\n // In eager mode fire as soon as any table has delivered; in default mode wait for all.\n if (microtaskQueued || (!eager && snapshotMap.size < distinctTables.length)) return;\n\n microtaskQueued = true;\n queueMicrotask(() => {\n microtaskQueued = false;\n\n if (!stopped) listener(buildCombined());\n });\n };\n\n // Register observers — each fires an immediate snapshot that populates snapshotMap.\n // Once all tables have delivered, scheduleFlush emits the first combined snapshot.\n // Signal is NOT passed to inner hub.observe() calls — the outer stop() is the single\n // cleanup path, avoiding O(tables) redundant abort listeners per signal.\n const stopFns = distinctTables.map((t) =>\n hub.observe(t, (records) => {\n snapshotMap.set(t, records as RecordOf<S, K>[]);\n scheduleFlush();\n }),\n );\n\n const stop = (): void => {\n stopped = true;\n\n for (const s of stopFns) s();\n };\n\n signal?.addEventListener('abort', stop, { once: true });\n\n return stop;\n };\n}\n\nexport function getRecordKey<S extends AnySchema, K extends keyof S>(\n schema: S,\n table: K,\n value: RecordOf<S, K>,\n): KeyOf<S, K> {\n const keyField = String(schema[table].key);\n const keyValue = (value as Record<string, unknown>)[keyField];\n\n if (keyValue === undefined || keyValue === null) {\n throw new VaultError(\n `key field \"${keyField}\" in table \"${String(table)}\" must be a non-null value, got ${String(keyValue)}`,\n );\n }\n\n return keyValue as KeyOf<S, K>;\n}\n"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import type { AnySchema, KeyOf, RecordOf } from './types';
2
+ export declare function encodeDbPrefix(dbName: string): string;
3
+ export declare function encodeStorageKey(dbName: string, table: string, key: string): string;
4
+ export declare function encodeStorageTablePrefix(dbName: string, table: string): string;
5
+ export declare function decodeStorageTableFromKey(dbName: string, storageKey: string | null): string | undefined;
6
+ export declare function createObserverHub<S extends AnySchema>(getAll: <K extends keyof S & string>(table: K) => Promise<RecordOf<S, K>[]>, onError?: (error: unknown) => void): {
7
+ dispose: () => void;
8
+ notify: <K extends keyof S & string>(table: K) => void;
9
+ observe: <K extends keyof S & string>(table: K, listener: (records: RecordOf<S, K>[]) => void, { immediate, signal }?: {
10
+ immediate?: boolean;
11
+ signal?: AbortSignal;
12
+ }) => (() => void);
13
+ };
14
+ /**
15
+ * Creates an AsyncIterable that yields a snapshot on every mutation.
16
+ * The first snapshot is emitted immediately (on the first `next()` call).
17
+ *
18
+ * `mode` controls back-pressure:
19
+ * - `'latest'` (default): if the consumer lags, only the most recent snapshot is retained.
20
+ * Intermediate states are discarded. Best for rendering/display use-cases.
21
+ * - `'all'`: every snapshot is queued and delivered in order. Use when every intermediate
22
+ * state matters (audit trails, animation).
23
+ *
24
+ * Pass an `AbortSignal` to cancel the iteration externally:
25
+ * ```ts
26
+ * const controller = new AbortController();
27
+ * for await (const users of db.watch('users', { signal: controller.signal })) { ... }
28
+ * controller.abort(); // stops the loop
29
+ * ```
30
+ *
31
+ * **Resource note:** The observer subscription is created eagerly on `[Symbol.asyncIterator]()` to
32
+ * avoid missing mutations before the first `next()` call. Always consume the iterator to completion,
33
+ * `break`/`return` out of `for await`, or pass an `AbortSignal` — otherwise the subscription
34
+ * will remain active until the adapter is disposed.
35
+ */
36
+ export declare function createWatchIterable<T>(subscribe: (listener: (snapshot: T[]) => void, signal?: AbortSignal) => () => void, mode?: 'all' | 'latest', signal?: AbortSignal): AsyncIterable<T[]>;
37
+ /**
38
+ * Observes multiple tables simultaneously. The listener receives a combined snapshot
39
+ * `{ [tableName]: RecordOf<S, T>[] }` and fires once after all tables have delivered
40
+ * their first snapshot. Subsequent firings are coalesced per microtask.
41
+ *
42
+ * Since `observe()` always fires immediately, each per-table observer populates the
43
+ * snapshotMap on registration. No separate prefetch step is needed.
44
+ */
45
+ export declare function createObserveMany<S extends AnySchema>(hub: ReturnType<typeof createObserverHub<S>>): <K extends keyof S & string>(tables: readonly K[], listener: (snapshots: { [T in K]: RecordOf<S, T>[]; }) => void, { eager, signal }?: {
46
+ eager?: boolean;
47
+ signal?: AbortSignal;
48
+ }) => () => void;
49
+ export declare function getRecordKey<S extends AnySchema, K extends keyof S>(schema: S, table: K, value: RecordOf<S, K>): KeyOf<S, K>;
50
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAe1D,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAiBvG;AAED,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,SAAS,EACnD,MAAM,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAC3E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI;mBA+Ed,IAAI;aAlER,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,SAAS,CAAC,KAAG,IAAI;cA0B1C,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,SAClC,CAAC,YACE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,IAAI,0BACf;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAC1E,CAAC,MAAM,IAAI,CAAC;EA0ChB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,KAAK,IAAI,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,MAAM,IAAI,EAClF,IAAI,GAAE,KAAK,GAAG,QAAmB,EACjC,MAAM,CAAC,EAAE,WAAW,GACnB,aAAa,CAAC,CAAC,EAAE,CAAC,CAuEpB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,SAAS,EAAE,GAAG,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,IACrE,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EACpD,QAAQ,SAAS,CAAC,EAAE,EACpB,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAE,KAAK,IAAI,EAC7D,oBAA2B;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,KACxE,MAAM,IAAI,CAmDd;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,MAAM,CAAC,EACjE,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GACpB,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAWb"}
@@ -0,0 +1,125 @@
1
+ import { error as e, warn as t } from "./_dev.js";
2
+ import { VaultDisposedError as n, VaultError as r, VaultScopeError as i } from "./errors.js";
3
+ //#region src/internal.ts
4
+ var a = "\0";
5
+ function o(e) {
6
+ return `${encodeURIComponent(e)}${a}`;
7
+ }
8
+ function s(e, t, n) {
9
+ return `${encodeURIComponent(e)}${a}${encodeURIComponent(t)}${a}${encodeURIComponent(n)}`;
10
+ }
11
+ function c(e, t) {
12
+ return `${encodeURIComponent(e)}${a}${encodeURIComponent(t)}${a}`;
13
+ }
14
+ function l(e, t) {
15
+ if (!t) return;
16
+ let n = o(e);
17
+ if (!t.startsWith(n)) return;
18
+ let r = t.slice(n.length), i = r.indexOf(a);
19
+ if (i !== -1) try {
20
+ return decodeURIComponent(r.slice(0, i));
21
+ } catch {
22
+ return;
23
+ }
24
+ }
25
+ function u(t, r) {
26
+ let i = /* @__PURE__ */ new Map(), a = !1, o = (t) => {
27
+ r ? r(t) : e("observer notification failed", t);
28
+ }, s = (e) => {
29
+ if (a) return;
30
+ let n = i.get(e);
31
+ !n || n.size === 0 || t(e).then((t) => {
32
+ if (a) return;
33
+ let n = i.get(e);
34
+ if (!(!n || n.size === 0)) for (let e of n) try {
35
+ e(t);
36
+ } catch (e) {
37
+ o(e);
38
+ }
39
+ }).catch((e) => o(e));
40
+ };
41
+ return {
42
+ dispose: () => {
43
+ a = !0, i.clear();
44
+ },
45
+ notify: s,
46
+ observe: (e, t, { immediate: r = !0, signal: o } = {}) => {
47
+ if (a) throw new n("observer hub is disposed");
48
+ if (o?.aborted) return () => {};
49
+ let c = e, l = t, u = i.get(c);
50
+ u || (u = /* @__PURE__ */ new Set(), i.set(c, u)), u.add(l), r && s(e);
51
+ let d = () => {
52
+ let e = i.get(c);
53
+ e && (e.delete(l), e.size === 0 && i.delete(c));
54
+ };
55
+ return o?.addEventListener("abort", d, { once: !0 }), d;
56
+ }
57
+ };
58
+ }
59
+ function d(e, t = "latest", n) {
60
+ return { [Symbol.asyncIterator]() {
61
+ let r = [], i = null, a = n?.aborted ?? !1, o = null, s = () => {
62
+ a || (a = !0, o?.(), o = null, i &&= (i({
63
+ done: !0,
64
+ value: void 0
65
+ }), null));
66
+ };
67
+ return n?.addEventListener("abort", s, { once: !0 }), a || (o = e((e) => {
68
+ if (!a) if (i) {
69
+ let t = i;
70
+ i = null, t({
71
+ done: !1,
72
+ value: e
73
+ });
74
+ } else t === "all" ? r.push(e) : r = [e];
75
+ })), {
76
+ async next() {
77
+ return a ? {
78
+ done: !0,
79
+ value: void 0
80
+ } : r.length > 0 ? {
81
+ done: !1,
82
+ value: r.shift()
83
+ } : new Promise((e) => {
84
+ i = e;
85
+ });
86
+ },
87
+ async return() {
88
+ return s(), {
89
+ done: !0,
90
+ value: void 0
91
+ };
92
+ },
93
+ async throw(e) {
94
+ return s(), Promise.reject(e);
95
+ }
96
+ };
97
+ } };
98
+ }
99
+ function f(e) {
100
+ return function(n, r, { eager: a = !1, signal: o } = {}) {
101
+ let s = [...new Set(n)];
102
+ if (s.length === 0) throw new i("observeMany requires at least one table");
103
+ if (s.length < n.length && t("observeMany: duplicate table names were ignored"), o?.aborted) return () => {};
104
+ let c = /* @__PURE__ */ new Map(), l = !1, u = !1, d = () => Object.fromEntries(n.map((e) => [e, c.get(e) ?? []])), f = () => {
105
+ l || !a && c.size < s.length || (l = !0, queueMicrotask(() => {
106
+ l = !1, u || r(d());
107
+ }));
108
+ }, p = s.map((t) => e.observe(t, (e) => {
109
+ c.set(t, e), f();
110
+ })), m = () => {
111
+ u = !0;
112
+ for (let e of p) e();
113
+ };
114
+ return o?.addEventListener("abort", m, { once: !0 }), m;
115
+ };
116
+ }
117
+ function p(e, t, n) {
118
+ let i = String(e[t].key), a = n[i];
119
+ if (a == null) throw new r(`key field "${i}" in table "${String(t)}" must be a non-null value, got ${String(a)}`);
120
+ return a;
121
+ }
122
+ //#endregion
123
+ export { f as createObserveMany, u as createObserverHub, d as createWatchIterable, l as decodeStorageTableFromKey, o as encodeDbPrefix, s as encodeStorageKey, c as encodeStorageTablePrefix, p as getRecordKey };
124
+
125
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.js","names":[],"sources":["../src/internal.ts"],"sourcesContent":["import type { AnySchema, KeyOf, RecordOf } from './types';\n\nimport { error as logError, warn } from './_dev';\nimport { VaultDisposedError, VaultError, VaultScopeError } from './errors';\n\ntype ObserverListener<T> = (records: T[]) => void;\n\n/**\n * Separator used between encoded key segments. The null byte (\\x00) is safe\n * because `encodeURIComponent` always percent-encodes it to `%00` (a 3-character\n * string), so the raw `\\x00` byte can never appear inside an encoded segment —\n * eliminating key-collision risk that a URL-safe character like `~` would carry.\n */\nconst SEP = '\\x00';\n\nexport function encodeDbPrefix(dbName: string): string {\n return `${encodeURIComponent(dbName)}${SEP}`;\n}\n\nexport function encodeStorageKey(dbName: string, table: string, key: string): string {\n return `${encodeURIComponent(dbName)}${SEP}${encodeURIComponent(table)}${SEP}${encodeURIComponent(key)}`;\n}\n\nexport function encodeStorageTablePrefix(dbName: string, table: string): string {\n return `${encodeURIComponent(dbName)}${SEP}${encodeURIComponent(table)}${SEP}`;\n}\n\nexport function decodeStorageTableFromKey(dbName: string, storageKey: string | null): string | undefined {\n if (!storageKey) return undefined;\n\n const prefix = encodeDbPrefix(dbName);\n\n if (!storageKey.startsWith(prefix)) return undefined;\n\n const tail = storageKey.slice(prefix.length);\n const end = tail.indexOf(SEP);\n\n if (end === -1) return undefined;\n\n try {\n return decodeURIComponent(tail.slice(0, end));\n } catch {\n return undefined;\n }\n}\n\nexport function createObserverHub<S extends AnySchema>(\n getAll: <K extends keyof S & string>(table: K) => Promise<RecordOf<S, K>[]>,\n onError?: (error: unknown) => void,\n) {\n const observers = new Map<string, Set<ObserverListener<unknown>>>();\n let disposed = false;\n\n const reportObserverError = (error: unknown): void => {\n if (onError) {\n onError(error);\n } else {\n logError('observer notification failed', error);\n }\n };\n\n const notify = <K extends keyof S & string>(table: K): void => {\n if (disposed) return;\n\n const listeners = observers.get(table);\n\n if (!listeners || listeners.size === 0) return;\n\n void getAll(table)\n .then((records) => {\n if (disposed) return;\n\n const current = observers.get(table);\n\n if (!current || current.size === 0) return;\n\n for (const listener of current) {\n try {\n listener(records as unknown[]);\n } catch (error) {\n reportObserverError(error);\n }\n }\n })\n .catch((error) => reportObserverError(error));\n };\n\n const observe = <K extends keyof S & string>(\n table: K,\n listener: (records: RecordOf<S, K>[]) => void,\n { immediate = true, signal }: { immediate?: boolean; signal?: AbortSignal } = {},\n ): (() => void) => {\n if (disposed) {\n throw new VaultDisposedError('observer hub is disposed');\n }\n\n if (signal?.aborted) return () => {};\n\n const key = table;\n const wrapped = listener as ObserverListener<unknown>;\n let listeners = observers.get(key);\n\n if (!listeners) {\n listeners = new Set();\n observers.set(key, listeners);\n }\n\n listeners.add(wrapped);\n\n // Fire an initial snapshot unless the caller opted out.\n if (immediate) notify(table);\n\n const stop = (): void => {\n const current = observers.get(key);\n\n if (!current) return;\n\n current.delete(wrapped);\n\n if (current.size === 0) observers.delete(key);\n };\n\n signal?.addEventListener('abort', stop, { once: true });\n\n return stop;\n };\n\n const dispose = (): void => {\n disposed = true;\n observers.clear();\n };\n\n return { dispose, notify, observe };\n}\n\n/**\n * Creates an AsyncIterable that yields a snapshot on every mutation.\n * The first snapshot is emitted immediately (on the first `next()` call).\n *\n * `mode` controls back-pressure:\n * - `'latest'` (default): if the consumer lags, only the most recent snapshot is retained.\n * Intermediate states are discarded. Best for rendering/display use-cases.\n * - `'all'`: every snapshot is queued and delivered in order. Use when every intermediate\n * state matters (audit trails, animation).\n *\n * Pass an `AbortSignal` to cancel the iteration externally:\n * ```ts\n * const controller = new AbortController();\n * for await (const users of db.watch('users', { signal: controller.signal })) { ... }\n * controller.abort(); // stops the loop\n * ```\n *\n * **Resource note:** The observer subscription is created eagerly on `[Symbol.asyncIterator]()` to\n * avoid missing mutations before the first `next()` call. Always consume the iterator to completion,\n * `break`/`return` out of `for await`, or pass an `AbortSignal` — otherwise the subscription\n * will remain active until the adapter is disposed.\n */\nexport function createWatchIterable<T>(\n subscribe: (listener: (snapshot: T[]) => void, signal?: AbortSignal) => () => void,\n mode: 'all' | 'latest' = 'latest',\n signal?: AbortSignal,\n): AsyncIterable<T[]> {\n return {\n [Symbol.asyncIterator]() {\n let pending: T[][] = [];\n let waiting: ((value: IteratorResult<T[]>) => void) | null = null;\n let done = signal?.aborted ?? false;\n let unsubscribe: (() => void) | null = null;\n\n const finish = (): void => {\n if (done) return;\n\n done = true;\n unsubscribe?.();\n unsubscribe = null;\n\n if (waiting) {\n waiting({ done: true, value: undefined });\n waiting = null;\n }\n };\n\n signal?.addEventListener('abort', finish, { once: true });\n\n // Eager registration: subscribe immediately so mutations between iterator\n // creation and first next() are not silently lost.\n if (!done) {\n unsubscribe = subscribe((snapshot) => {\n if (done) return;\n\n if (waiting) {\n const resolve = waiting;\n\n waiting = null;\n resolve({ done: false, value: snapshot });\n } else if (mode === 'all') {\n pending.push(snapshot);\n } else {\n pending = [snapshot];\n }\n });\n }\n\n return {\n async next(): Promise<IteratorResult<T[]>> {\n if (done) return { done: true, value: undefined };\n\n if (pending.length > 0) {\n const value = pending.shift()!;\n\n return { done: false, value };\n }\n\n return new Promise<IteratorResult<T[]>>((resolve) => {\n waiting = resolve;\n });\n },\n\n async return(): Promise<IteratorResult<T[]>> {\n finish();\n\n return { done: true, value: undefined };\n },\n\n async throw(err?: unknown): Promise<IteratorResult<T[]>> {\n finish();\n\n return Promise.reject(err);\n },\n };\n },\n };\n}\n\n/**\n * Observes multiple tables simultaneously. The listener receives a combined snapshot\n * `{ [tableName]: RecordOf<S, T>[] }` and fires once after all tables have delivered\n * their first snapshot. Subsequent firings are coalesced per microtask.\n *\n * Since `observe()` always fires immediately, each per-table observer populates the\n * snapshotMap on registration. No separate prefetch step is needed.\n */\nexport function createObserveMany<S extends AnySchema>(hub: ReturnType<typeof createObserverHub<S>>) {\n return function observeMany<K extends keyof S & string>(\n tables: readonly K[],\n listener: (snapshots: { [T in K]: RecordOf<S, T>[] }) => void,\n { eager = false, signal }: { eager?: boolean; signal?: AbortSignal } = {},\n ): () => void {\n const distinctTables = [...new Set(tables)] as K[];\n\n if (distinctTables.length === 0) throw new VaultScopeError('observeMany requires at least one table');\n\n if (distinctTables.length < tables.length) {\n warn('observeMany: duplicate table names were ignored');\n }\n\n if (signal?.aborted) return () => {};\n\n const snapshotMap = new Map<string, RecordOf<S, K>[]>();\n let microtaskQueued = false;\n let stopped = false;\n\n const buildCombined = (): { [T in K]: RecordOf<S, T>[] } =>\n Object.fromEntries(tables.map((t) => [t, snapshotMap.get(t) ?? []])) as { [T in K]: RecordOf<S, T>[] };\n\n const scheduleFlush = (): void => {\n // In eager mode fire as soon as any table has delivered; in default mode wait for all.\n if (microtaskQueued || (!eager && snapshotMap.size < distinctTables.length)) return;\n\n microtaskQueued = true;\n queueMicrotask(() => {\n microtaskQueued = false;\n\n if (!stopped) listener(buildCombined());\n });\n };\n\n // Register observers — each fires an immediate snapshot that populates snapshotMap.\n // Once all tables have delivered, scheduleFlush emits the first combined snapshot.\n // Signal is NOT passed to inner hub.observe() calls — the outer stop() is the single\n // cleanup path, avoiding O(tables) redundant abort listeners per signal.\n const stopFns = distinctTables.map((t) =>\n hub.observe(t, (records) => {\n snapshotMap.set(t, records as RecordOf<S, K>[]);\n scheduleFlush();\n }),\n );\n\n const stop = (): void => {\n stopped = true;\n\n for (const s of stopFns) s();\n };\n\n signal?.addEventListener('abort', stop, { once: true });\n\n return stop;\n };\n}\n\nexport function getRecordKey<S extends AnySchema, K extends keyof S>(\n schema: S,\n table: K,\n value: RecordOf<S, K>,\n): KeyOf<S, K> {\n const keyField = String(schema[table].key);\n const keyValue = (value as Record<string, unknown>)[keyField];\n\n if (keyValue === undefined || keyValue === null) {\n throw new VaultError(\n `key field \"${keyField}\" in table \"${String(table)}\" must be a non-null value, got ${String(keyValue)}`,\n );\n }\n\n return keyValue as KeyOf<S, K>;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ function e(e){return({db:t,tx:n})=>{for(let r of e)switch(r.type){case`addIndex`:{let e=n.objectStore(r.table);e.indexNames.contains(r.field)||e.createIndex(r.field,`value.${r.field}`);break}case`addTable`:t.objectStoreNames.contains(r.name)||t.createObjectStore(r.name);break;case`removeIndex`:{let e=n.objectStore(r.table);e.indexNames.contains(r.field)&&e.deleteIndex(r.field);break}case`removeTable`:t.objectStoreNames.contains(r.name)&&t.deleteObjectStore(r.name);break}}}exports.defineMigration=e;
2
+ //# sourceMappingURL=migration.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.cjs","names":[],"sources":["../src/migration.ts"],"sourcesContent":["import type { MigrationFn } from './types';\n\n/**\n * A single step in a typed migration definition.\n * Compose multiple steps to describe the full schema change between two versions.\n */\nexport type MigrationStep =\n | { field: string; table: string; type: 'addIndex' }\n | { field: string; table: string; type: 'removeIndex' }\n | { name: string; type: 'addTable' }\n | { name: string; type: 'removeTable' };\n\n/**\n * Builds a typed `MigrationFn` from a declarative list of schema change steps.\n * Each step is applied in order and is idempotent (safe to run when the target\n * already exists or has already been removed).\n *\n * ```ts\n * const migrate = defineMigration([\n * { type: 'addTable', name: 'sessions' },\n * { type: 'addIndex', table: 'users', field: 'email' },\n * { type: 'removeTable', name: 'legacyTokens' },\n * ]);\n *\n * const db = createIndexedDB({ name: 'app', version: 2, schema, migrate });\n * ```\n */\nexport function defineMigration(steps: MigrationStep[]): MigrationFn {\n return ({ db, tx }) => {\n for (const step of steps) {\n switch (step.type) {\n case 'addIndex': {\n const store = tx.objectStore(step.table);\n\n // keyPath mirrors the vault storage envelope: { value: T, expiresAt?: number }\n if (!store.indexNames.contains(step.field)) {\n store.createIndex(step.field, `value.${step.field}`);\n }\n\n break;\n }\n case 'addTable':\n if (!db.objectStoreNames.contains(step.name)) {\n db.createObjectStore(step.name);\n }\n\n break;\n case 'removeIndex': {\n const store = tx.objectStore(step.table);\n\n if (store.indexNames.contains(step.field)) {\n store.deleteIndex(step.field);\n }\n\n break;\n }\n case 'removeTable':\n if (db.objectStoreNames.contains(step.name)) {\n db.deleteObjectStore(step.name);\n }\n\n break;\n }\n }\n };\n}\n"],"mappings":"AA2BA,SAAgB,EAAgB,EAAqC,CACnE,OAAQ,CAAE,KAAI,QAAS,CACrB,IAAK,IAAM,KAAQ,EACjB,OAAQ,EAAK,KAAb,CACE,IAAK,WAAY,CACf,IAAM,EAAQ,EAAG,YAAY,EAAK,KAAK,EAGlC,EAAM,WAAW,SAAS,EAAK,KAAK,GACvC,EAAM,YAAY,EAAK,MAAO,SAAS,EAAK,OAAO,EAGrD,KACF,CACA,IAAK,WACE,EAAG,iBAAiB,SAAS,EAAK,IAAI,GACzC,EAAG,kBAAkB,EAAK,IAAI,EAGhC,MACF,IAAK,cAAe,CAClB,IAAM,EAAQ,EAAG,YAAY,EAAK,KAAK,EAEnC,EAAM,WAAW,SAAS,EAAK,KAAK,GACtC,EAAM,YAAY,EAAK,KAAK,EAG9B,KACF,CACA,IAAK,cACC,EAAG,iBAAiB,SAAS,EAAK,IAAI,GACxC,EAAG,kBAAkB,EAAK,IAAI,EAGhC,KACJ,CAEJ,CACF"}
@@ -0,0 +1,37 @@
1
+ import type { MigrationFn } from './types';
2
+ /**
3
+ * A single step in a typed migration definition.
4
+ * Compose multiple steps to describe the full schema change between two versions.
5
+ */
6
+ export type MigrationStep = {
7
+ field: string;
8
+ table: string;
9
+ type: 'addIndex';
10
+ } | {
11
+ field: string;
12
+ table: string;
13
+ type: 'removeIndex';
14
+ } | {
15
+ name: string;
16
+ type: 'addTable';
17
+ } | {
18
+ name: string;
19
+ type: 'removeTable';
20
+ };
21
+ /**
22
+ * Builds a typed `MigrationFn` from a declarative list of schema change steps.
23
+ * Each step is applied in order and is idempotent (safe to run when the target
24
+ * already exists or has already been removed).
25
+ *
26
+ * ```ts
27
+ * const migrate = defineMigration([
28
+ * { type: 'addTable', name: 'sessions' },
29
+ * { type: 'addIndex', table: 'users', field: 'email' },
30
+ * { type: 'removeTable', name: 'legacyTokens' },
31
+ * ]);
32
+ *
33
+ * const db = createIndexedDB({ name: 'app', version: 2, schema, migrate });
34
+ * ```
35
+ */
36
+ export declare function defineMigration(steps: MigrationStep[]): MigrationFn;
37
+ //# sourceMappingURL=migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAClD;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAE1C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,WAAW,CAsCnE"}
@@ -0,0 +1,27 @@
1
+ //#region src/migration.ts
2
+ function e(e) {
3
+ return ({ db: t, tx: n }) => {
4
+ for (let r of e) switch (r.type) {
5
+ case "addIndex": {
6
+ let e = n.objectStore(r.table);
7
+ e.indexNames.contains(r.field) || e.createIndex(r.field, `value.${r.field}`);
8
+ break;
9
+ }
10
+ case "addTable":
11
+ t.objectStoreNames.contains(r.name) || t.createObjectStore(r.name);
12
+ break;
13
+ case "removeIndex": {
14
+ let e = n.objectStore(r.table);
15
+ e.indexNames.contains(r.field) && e.deleteIndex(r.field);
16
+ break;
17
+ }
18
+ case "removeTable":
19
+ t.objectStoreNames.contains(r.name) && t.deleteObjectStore(r.name);
20
+ break;
21
+ }
22
+ };
23
+ }
24
+ //#endregion
25
+ export { e as defineMigration };
26
+
27
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.js","names":[],"sources":["../src/migration.ts"],"sourcesContent":["import type { MigrationFn } from './types';\n\n/**\n * A single step in a typed migration definition.\n * Compose multiple steps to describe the full schema change between two versions.\n */\nexport type MigrationStep =\n | { field: string; table: string; type: 'addIndex' }\n | { field: string; table: string; type: 'removeIndex' }\n | { name: string; type: 'addTable' }\n | { name: string; type: 'removeTable' };\n\n/**\n * Builds a typed `MigrationFn` from a declarative list of schema change steps.\n * Each step is applied in order and is idempotent (safe to run when the target\n * already exists or has already been removed).\n *\n * ```ts\n * const migrate = defineMigration([\n * { type: 'addTable', name: 'sessions' },\n * { type: 'addIndex', table: 'users', field: 'email' },\n * { type: 'removeTable', name: 'legacyTokens' },\n * ]);\n *\n * const db = createIndexedDB({ name: 'app', version: 2, schema, migrate });\n * ```\n */\nexport function defineMigration(steps: MigrationStep[]): MigrationFn {\n return ({ db, tx }) => {\n for (const step of steps) {\n switch (step.type) {\n case 'addIndex': {\n const store = tx.objectStore(step.table);\n\n // keyPath mirrors the vault storage envelope: { value: T, expiresAt?: number }\n if (!store.indexNames.contains(step.field)) {\n store.createIndex(step.field, `value.${step.field}`);\n }\n\n break;\n }\n case 'addTable':\n if (!db.objectStoreNames.contains(step.name)) {\n db.createObjectStore(step.name);\n }\n\n break;\n case 'removeIndex': {\n const store = tx.objectStore(step.table);\n\n if (store.indexNames.contains(step.field)) {\n store.deleteIndex(step.field);\n }\n\n break;\n }\n case 'removeTable':\n if (db.objectStoreNames.contains(step.name)) {\n db.deleteObjectStore(step.name);\n }\n\n break;\n }\n }\n };\n}\n"],"mappings":";AA2BA,SAAgB,EAAgB,GAAqC;CACnE,QAAQ,EAAE,OAAI,YAAS;EACrB,KAAK,IAAM,KAAQ,GACjB,QAAQ,EAAK,MAAb;GACE,KAAK,YAAY;IACf,IAAM,IAAQ,EAAG,YAAY,EAAK,KAAK;IAGvC,AAAK,EAAM,WAAW,SAAS,EAAK,KAAK,KACvC,EAAM,YAAY,EAAK,OAAO,SAAS,EAAK,OAAO;IAGrD;GACF;GACA,KAAK;IACH,AAAK,EAAG,iBAAiB,SAAS,EAAK,IAAI,KACzC,EAAG,kBAAkB,EAAK,IAAI;IAGhC;GACF,KAAK,eAAe;IAClB,IAAM,IAAQ,EAAG,YAAY,EAAK,KAAK;IAEvC,AAAI,EAAM,WAAW,SAAS,EAAK,KAAK,KACtC,EAAM,YAAY,EAAK,KAAK;IAG9B;GACF;GACA,KAAK;IACH,AAAI,EAAG,iBAAiB,SAAS,EAAK,IAAI,KACxC,EAAG,kBAAkB,EAAK,IAAI;IAGhC;EACJ;CAEJ;AACF"}
package/dist/prune.cjs ADDED
@@ -0,0 +1,2 @@
1
+ const e=require("./_dev.cjs"),t=require("./errors.cjs");function n(n,r){if(!Number.isFinite(r.interval)||r.interval<=0)throw new t.VaultError(`scheduleExpiredPrune: interval must be a finite positive number`);let i=!0,a=()=>{i=!1,clearInterval(o)};r.signal?.addEventListener(`abort`,a,{once:!0});let o=setInterval(()=>{i&&n.pruneExpired().catch(n=>{n instanceof t.VaultDisposedError?(i=!1,clearInterval(o)):r.onError?r.onError(n):e.warn(`scheduleExpiredPrune: pruneExpired() threw — pass onError to handle this. ${String(n)}`)})},r.interval);return a}exports.scheduleExpiredPrune=n;
2
+ //# sourceMappingURL=prune.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.cjs","names":[],"sources":["../src/prune.ts"],"sourcesContent":["import type { Adapter, AnySchema } from './types';\n\nimport { warn } from './_dev';\nimport { VaultDisposedError, VaultError } from './errors';\n\n/**\n * Schedules periodic calls to `adapter.pruneExpired()` using `setInterval`.\n * Returns a `stop` function to cancel the schedule.\n *\n * The schedule auto-cancels if the adapter is disposed — no need to call `stop()`\n * in a dispose handler when the adapter lifetime matches the schedule lifetime.\n *\n * ```ts\n * const stopPrune = scheduleExpiredPrune(db, { interval: ttl.hours(1) });\n * // later...\n * stopPrune();\n * ```\n */\nexport function scheduleExpiredPrune<S extends AnySchema>(\n adapter: Pick<Adapter<S>, 'pruneExpired'>,\n options: {\n interval: number;\n /**\n * Called when `pruneExpired()` throws an error that is NOT a `VaultDisposedError`.\n * `VaultDisposedError` always stops the schedule automatically.\n * Without this callback, non-disposal errors are silently swallowed.\n */\n onError?: (err: unknown) => void;\n /**\n * When aborted, stops the schedule. Useful for tying the schedule lifetime\n * to an adapter's `disposalSignal`:\n * ```ts\n * scheduleExpiredPrune(db, { interval: ttl.hours(1), signal: db.disposalSignal });\n * ```\n */\n signal?: AbortSignal;\n },\n): () => void {\n if (!Number.isFinite(options.interval) || options.interval <= 0) {\n throw new VaultError('scheduleExpiredPrune: interval must be a finite positive number');\n }\n\n let active = true;\n\n const stop = (): void => {\n active = false;\n clearInterval(id);\n };\n\n options.signal?.addEventListener('abort', stop, { once: true });\n\n const id = setInterval(() => {\n if (!active) return;\n\n void adapter.pruneExpired().catch((err) => {\n if (err instanceof VaultDisposedError) {\n active = false;\n clearInterval(id);\n } else if (options.onError) {\n options.onError(err);\n } else {\n warn(`scheduleExpiredPrune: pruneExpired() threw — pass onError to handle this. ${String(err)}`);\n }\n });\n }, options.interval);\n\n return stop;\n}\n"],"mappings":"wDAkBA,SAAgB,EACd,EACA,EAiBY,CACZ,GAAI,CAAC,OAAO,SAAS,EAAQ,QAAQ,GAAK,EAAQ,UAAY,EAC5D,MAAM,IAAI,EAAA,WAAW,iEAAiE,EAGxF,IAAI,EAAS,GAEP,MAAmB,CACvB,EAAS,GACT,cAAc,CAAE,CAClB,EAEA,EAAQ,QAAQ,iBAAiB,QAAS,EAAM,CAAE,KAAM,EAAK,CAAC,EAE9D,IAAM,EAAK,gBAAkB,CACtB,GAEL,EAAa,aAAa,CAAC,CAAC,MAAO,GAAQ,CACrC,aAAe,EAAA,oBACjB,EAAS,GACT,cAAc,CAAE,GACP,EAAQ,QACjB,EAAQ,QAAQ,CAAG,EAEnB,EAAA,KAAK,6EAA6E,OAAO,CAAG,GAAG,CAEnG,CAAC,CACH,EAAG,EAAQ,QAAQ,EAEnB,OAAO,CACT"}
@@ -0,0 +1,32 @@
1
+ import type { Adapter, AnySchema } from './types';
2
+ /**
3
+ * Schedules periodic calls to `adapter.pruneExpired()` using `setInterval`.
4
+ * Returns a `stop` function to cancel the schedule.
5
+ *
6
+ * The schedule auto-cancels if the adapter is disposed — no need to call `stop()`
7
+ * in a dispose handler when the adapter lifetime matches the schedule lifetime.
8
+ *
9
+ * ```ts
10
+ * const stopPrune = scheduleExpiredPrune(db, { interval: ttl.hours(1) });
11
+ * // later...
12
+ * stopPrune();
13
+ * ```
14
+ */
15
+ export declare function scheduleExpiredPrune<S extends AnySchema>(adapter: Pick<Adapter<S>, 'pruneExpired'>, options: {
16
+ interval: number;
17
+ /**
18
+ * Called when `pruneExpired()` throws an error that is NOT a `VaultDisposedError`.
19
+ * `VaultDisposedError` always stops the schedule automatically.
20
+ * Without this callback, non-disposal errors are silently swallowed.
21
+ */
22
+ onError?: (err: unknown) => void;
23
+ /**
24
+ * When aborted, stops the schedule. Useful for tying the schedule lifetime
25
+ * to an adapter's `disposalSignal`:
26
+ * ```ts
27
+ * scheduleExpiredPrune(db, { interval: ttl.hours(1), signal: db.disposalSignal });
28
+ * ```
29
+ */
30
+ signal?: AbortSignal;
31
+ }): () => void;
32
+ //# sourceMappingURL=prune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../src/prune.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAKlD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,SAAS,EACtD,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,EACzC,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GACA,MAAM,IAAI,CA8BZ"}
package/dist/prune.js ADDED
@@ -0,0 +1,20 @@
1
+ import { warn as e } from "./_dev.js";
2
+ import { VaultDisposedError as t, VaultError as n } from "./errors.js";
3
+ //#region src/prune.ts
4
+ function r(r, i) {
5
+ if (!Number.isFinite(i.interval) || i.interval <= 0) throw new n("scheduleExpiredPrune: interval must be a finite positive number");
6
+ let a = !0, o = () => {
7
+ a = !1, clearInterval(s);
8
+ };
9
+ i.signal?.addEventListener("abort", o, { once: !0 });
10
+ let s = setInterval(() => {
11
+ a && r.pruneExpired().catch((n) => {
12
+ n instanceof t ? (a = !1, clearInterval(s)) : i.onError ? i.onError(n) : e(`scheduleExpiredPrune: pruneExpired() threw — pass onError to handle this. ${String(n)}`);
13
+ });
14
+ }, i.interval);
15
+ return o;
16
+ }
17
+ //#endregion
18
+ export { r as scheduleExpiredPrune };
19
+
20
+ //# sourceMappingURL=prune.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.js","names":[],"sources":["../src/prune.ts"],"sourcesContent":["import type { Adapter, AnySchema } from './types';\n\nimport { warn } from './_dev';\nimport { VaultDisposedError, VaultError } from './errors';\n\n/**\n * Schedules periodic calls to `adapter.pruneExpired()` using `setInterval`.\n * Returns a `stop` function to cancel the schedule.\n *\n * The schedule auto-cancels if the adapter is disposed — no need to call `stop()`\n * in a dispose handler when the adapter lifetime matches the schedule lifetime.\n *\n * ```ts\n * const stopPrune = scheduleExpiredPrune(db, { interval: ttl.hours(1) });\n * // later...\n * stopPrune();\n * ```\n */\nexport function scheduleExpiredPrune<S extends AnySchema>(\n adapter: Pick<Adapter<S>, 'pruneExpired'>,\n options: {\n interval: number;\n /**\n * Called when `pruneExpired()` throws an error that is NOT a `VaultDisposedError`.\n * `VaultDisposedError` always stops the schedule automatically.\n * Without this callback, non-disposal errors are silently swallowed.\n */\n onError?: (err: unknown) => void;\n /**\n * When aborted, stops the schedule. Useful for tying the schedule lifetime\n * to an adapter's `disposalSignal`:\n * ```ts\n * scheduleExpiredPrune(db, { interval: ttl.hours(1), signal: db.disposalSignal });\n * ```\n */\n signal?: AbortSignal;\n },\n): () => void {\n if (!Number.isFinite(options.interval) || options.interval <= 0) {\n throw new VaultError('scheduleExpiredPrune: interval must be a finite positive number');\n }\n\n let active = true;\n\n const stop = (): void => {\n active = false;\n clearInterval(id);\n };\n\n options.signal?.addEventListener('abort', stop, { once: true });\n\n const id = setInterval(() => {\n if (!active) return;\n\n void adapter.pruneExpired().catch((err) => {\n if (err instanceof VaultDisposedError) {\n active = false;\n clearInterval(id);\n } else if (options.onError) {\n options.onError(err);\n } else {\n warn(`scheduleExpiredPrune: pruneExpired() threw — pass onError to handle this. ${String(err)}`);\n }\n });\n }, options.interval);\n\n return stop;\n}\n"],"mappings":";;;AAkBA,SAAgB,EACd,GACA,GAiBY;CACZ,IAAI,CAAC,OAAO,SAAS,EAAQ,QAAQ,KAAK,EAAQ,YAAY,GAC5D,MAAM,IAAI,EAAW,iEAAiE;CAGxF,IAAI,IAAS,IAEP,UAAmB;EAEvB,AADA,IAAS,IACT,cAAc,CAAE;CAClB;CAEA,EAAQ,QAAQ,iBAAiB,SAAS,GAAM,EAAE,MAAM,GAAK,CAAC;CAE9D,IAAM,IAAK,kBAAkB;EACtB,KAEL,EAAa,aAAa,CAAC,CAAC,OAAO,MAAQ;GACzC,AAAI,aAAe,KACjB,IAAS,IACT,cAAc,CAAE,KACP,EAAQ,UACjB,EAAQ,QAAQ,CAAG,IAEnB,EAAK,6EAA6E,OAAO,CAAG,GAAG;EAEnG,CAAC;CACH,GAAG,EAAQ,QAAQ;CAEnB,OAAO;AACT"}
package/dist/query.cjs ADDED
@@ -0,0 +1,2 @@
1
+ const e=require("./errors.cjs");async function t(e,t){let n=await e.source();for(let e of t)n=e.apply(n);return n}function n(t,n){if(!Number.isInteger(t)||t<0)throw new e.VaultError(`${n} must be a non-negative integer`);return t}function r(e,t){return{deleteMany:e.deleteMany,source:t}}function i(a,o=[]){let s=e=>i(a,[...o,e]);return{between(e,t,n){let c=String(e);if(o.length===0){if(a.getRange&&a.keyField===c)return i(r(a,()=>a.getRange({lower:t,type:`between`,upper:n})),o);if(a.getIndexRange&&a.indexedFields?.has(c))return i(r(a,()=>a.getIndexRange(c,{lower:t,type:`between`,upper:n})),o)}return s({apply:r=>r.filter(r=>{let i=r[e];return i>=t&&i<=n})})},count(){return t(a,o).then(e=>e.length)},async delete(){if(!a.deleteMany)throw new e.VaultError(`query.delete is not available for this adapter context`);let n=await t(a,o);return a.deleteMany(n)},equals(e,t){let n=String(e);if(o.length===0){if(a.getRange&&a.keyField===n)return i(r(a,()=>a.getRange({type:`eq`,value:t})));if(a.getIndexRange&&a.indexedFields?.has(n))return i(r(a,()=>a.getIndexRange(n,{type:`eq`,value:t})))}return i(a,[...o,{apply:n=>n.filter(n=>n[e]===t)}])},async exists(){return o.length===0?a.source().then(e=>e.length>0):t(a,o).then(e=>e.length>0)},filter(e){return s({apply:t=>t.filter(e)})},first(){return o.length===0?a.source().then(e=>e[0]):t(a,o).then(e=>e[0])},limit(e){let t=n(e,`query.limit`);return s({apply:e=>e.slice(0,t),isNonFilter:!0})},offset(e){let t=n(e,`query.offset`);return s({apply:e=>e.slice(t),isNonFilter:!0})},orderBy(e,t=`asc`){return s({apply:n=>{let r=t===`asc`?1:-1;return[...n].sort((t,n)=>{let i=t[e],a=n[e];return i===a?0:i>a?r:-r})},isNonFilter:!0})},startsWith(e,t,{ignoreCase:n=!1}={}){let c=String(e);if(!n&&t.length>0&&o.length===0){if(a.getRange&&a.keyField===c)return i(r(a,()=>a.getRange({prefix:t,type:`starts`})),o);if(a.getIndexRange&&a.indexedFields?.has(c))return i(r(a,()=>a.getIndexRange(c,{prefix:t,type:`starts`})),o)}let l=n?t.toLowerCase():t;return s({apply:t=>t.filter(t=>{let r=t[e];return typeof r==`string`?(n?r.toLowerCase():r).startsWith(l):!1})})},toArray(){return t(a,o)},totalCount(){return t(a,o.filter(e=>!e.isNonFilter)).then(e=>e.length)}}}exports.createQueryBuilder=i;
2
+ //# sourceMappingURL=query.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.cjs","names":[],"sources":["../src/query.ts"],"sourcesContent":["import { VaultError } from './errors';\n\ntype Predicate<T> = (value: T, index: number, array: T[]) => boolean;\n/**\n * `isNonFilter`: when true this op is excluded from `totalCount()` — it does not restrict\n * *which* records match (limit, offset, orderBy), only how results are presented.\n */\ntype QueryOp<T> = { apply: (data: T[]) => T[]; isNonFilter?: boolean };\ntype ComparableFieldKeys<T extends Record<string, unknown>> = {\n [K in keyof T]-?: Extract<NonNullable<T[K]>, number | string> extends never ? never : K;\n}[keyof T];\n\n/**\n * A primary-key range hint that can be pushed down to native storage backends (e.g. IndexedDB).\n * When `QueryContext.getRange` and `keyField` are present, a matching first filter op replaces\n * the full-table `source()` scan with a targeted range fetch.\n */\nexport type NativeRange =\n | { type: 'eq'; value: unknown }\n | { lower: unknown; type: 'between'; upper: unknown }\n | { prefix: string; type: 'starts' };\n\nexport type QueryContext<T extends Record<string, unknown>> = {\n deleteMany?: (records: T[]) => Promise<number>;\n /**\n * When present alongside `indexedFields`, replaces `source()` for secondary-index filter ops.\n * Only activated when the first op is `equals`, `between`, or `startsWith` on a field that\n * has an index registered in the schema. IndexedDB uses `IDBIndex.getAll(range)` under the hood.\n */\n getIndexRange?: (field: string, range: NativeRange) => Promise<T[]>;\n /**\n * When present alongside `keyField`, replaces `source()` for primary-key filter ops.\n * Only activated when the first op is an `equals`, `between`, or case-sensitive `startsWith`\n * on `keyField`. All remaining ops still run in-memory against the range result.\n */\n getRange?: (range: NativeRange) => Promise<T[]>;\n /** Fields that have secondary indexes — used to detect when a filter op can use `getIndexRange`. */\n indexedFields?: ReadonlySet<string>;\n /** Primary key field name — used to detect when a filter op can be pushed to `getRange`. */\n keyField?: string;\n source: () => Promise<T[]>;\n};\n\n/* -------------------- Public interfaces -------------------- */\n\n/**\n * Shared query methods. `T` is the base record type; `N` is the progressively-narrowed type\n * accumulated by `equals()` calls.\n */\ntype ChainedQuery<T extends Record<string, unknown>, N extends T, Self extends ChainedQuery<T, N, Self>> = {\n /**\n * Filter records where `field` is between `lower` and `upper` (inclusive).\n * Preserves any type narrowing already accumulated by prior `equals()` calls.\n */\n between<K extends ComparableFieldKeys<T>>(\n field: K,\n lower: Extract<NonNullable<T[K]>, number | string>,\n upper: Extract<NonNullable<T[K]>, number | string>,\n ): QueryBuilder<T, N>;\n /**\n * Returns the number of records matching all applied operations, including `limit` and `offset`.\n * To get the full filtered set size regardless of pagination (e.g. for \"page X of N\" UIs),\n * use `totalCount()` instead.\n */\n count(): Promise<number>;\n /**\n * Filter records where `field` exactly equals `value`.\n * The return type is narrowed to `QueryBuilder<T & Record<K, V>>` so that subsequent\n * `.toArray()`, `.first()`, etc. reflect the equality constraint in their result type.\n *\n * ```ts\n * // result: (User & { role: 'admin' })[]\n * const admins = await db.query('users').equals('role', 'admin').toArray();\n * ```\n */\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>>;\n /**\n * Returns `true` if at least one record matches all applied filter operations.\n * Equivalent to `(await query.first()) !== undefined` but makes the intent explicit.\n * Presentation-only ops (`limit`, `offset`, `orderBy`) are respected before checking.\n */\n exists(): Promise<boolean>;\n filter(fn: Predicate<N>): Self;\n first(): Promise<N | undefined>;\n limit(n: number): Self;\n offset(n: number): Self;\n orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): Self;\n /**\n * Filter records where `field` starts with `prefix`.\n * Preserves any type narrowing already accumulated by prior `equals()` calls.\n *\n * **Index push-down** is only active when `ignoreCase` is `false` (default) and this\n * is the first operation in the chain. With `ignoreCase: true`, a full-table scan is\n * always performed regardless of whether the field has an index.\n */\n startsWith<K extends keyof T>(field: K, prefix: string, options?: { ignoreCase?: boolean }): QueryBuilder<T, N>;\n toArray(): Promise<N[]>;\n /**\n * Returns the number of records matching the applied filter predicates.\n * Presentation-only ops (`limit`, `offset`, `orderBy`) are intentionally ignored — this\n * always counts the full filtered set, making paginated total-count queries possible\n * without a second query.\n */\n totalCount(): Promise<number>;\n};\n\n/** Extends the shared query API with `delete()`. Available on both `Adapter.query()` and inside `batch()` callbacks. */\nexport interface QueryBuilder<T extends Record<string, unknown>, N extends T = T> extends ChainedQuery<\n T,\n N,\n QueryBuilder<T, N>\n> {\n delete(): Promise<number>;\n}\n\n/* -------------------- Helpers -------------------- */\n\nasync function applyOps<T extends Record<string, unknown>>(\n ctx: QueryContext<T>,\n ops: readonly QueryOp<T>[],\n): Promise<T[]> {\n let data = await ctx.source();\n\n for (const op of ops) {\n data = op.apply(data);\n }\n\n return data;\n}\n\nfunction assertNonNegativeInteger(value: number, name: string): number {\n if (!Number.isInteger(value) || value < 0) {\n throw new VaultError(`${name} must be a non-negative integer`);\n }\n\n return value;\n}\n\n/* -------------------- Push-down helpers -------------------- */\n\n/** Build a new QueryContext with source replaced by a range/index fetch. Drops range hints since push-down is done. */\nfunction pushDownContext<T extends Record<string, unknown>>(\n ctx: QueryContext<T>,\n newSource: () => Promise<T[]>,\n): QueryContext<T> {\n return { deleteMany: ctx.deleteMany, source: newSource };\n}\n\n/* -------------------- Factory -------------------- */\n\nexport function createQueryBuilder<T extends Record<string, unknown>, N extends T = T>(\n ctx: QueryContext<T>,\n ops: readonly QueryOp<T>[] = [],\n): QueryBuilder<T, N> {\n const append = (op: QueryOp<T>): QueryBuilder<T, N> => createQueryBuilder<T, N>(ctx, [...ops, op]);\n\n return {\n between(field, lower, upper) {\n const fieldStr = String(field);\n\n if (ops.length === 0) {\n if (ctx.getRange && ctx.keyField === fieldStr) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getRange!({ lower, type: 'between', upper })),\n ops,\n );\n }\n\n if (ctx.getIndexRange && ctx.indexedFields?.has(fieldStr)) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getIndexRange!(fieldStr, { lower, type: 'between', upper })),\n ops,\n );\n }\n }\n\n return append({\n apply: (data) =>\n data.filter((r) => {\n const v = r[field] as number | string;\n\n return v >= lower && v <= upper;\n }),\n }) as unknown as QueryBuilder<T, N>;\n },\n count(): Promise<number> {\n return applyOps(ctx, ops).then((r) => r.length);\n },\n async delete(): Promise<number> {\n if (!ctx.deleteMany) {\n throw new VaultError('query.delete is not available for this adapter context');\n }\n\n const records = await applyOps(ctx, ops);\n\n return ctx.deleteMany(records);\n },\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>> {\n const fieldStr = String(field);\n\n if (ops.length === 0) {\n if (ctx.getRange && ctx.keyField === fieldStr) {\n return createQueryBuilder<T & Record<K, V>>(\n pushDownContext(\n ctx as unknown as QueryContext<T & Record<K, V>>,\n () => ctx.getRange!({ type: 'eq', value }) as Promise<(T & Record<K, V>)[]>,\n ),\n ) as QueryBuilder<T & Record<K, V>>;\n }\n\n if (ctx.getIndexRange && ctx.indexedFields?.has(fieldStr)) {\n return createQueryBuilder<T & Record<K, V>>(\n pushDownContext(\n ctx as unknown as QueryContext<T & Record<K, V>>,\n () => ctx.getIndexRange!(fieldStr, { type: 'eq', value }) as Promise<(T & Record<K, V>)[]>,\n ),\n ) as QueryBuilder<T & Record<K, V>>;\n }\n }\n\n return createQueryBuilder<T & Record<K, V>>(ctx as unknown as QueryContext<T & Record<K, V>>, [\n ...(ops as unknown as QueryOp<T & Record<K, V>>[]),\n { apply: (data) => data.filter((r) => r[field] === value) as (T & Record<K, V>)[] },\n ]);\n },\n async exists(): Promise<boolean> {\n if (ops.length === 0) return ctx.source().then((r) => r.length > 0);\n\n return applyOps(ctx, ops).then((r) => r.length > 0);\n },\n filter(fn) {\n return append({ apply: (data) => data.filter(fn as Predicate<T>) });\n },\n first(): Promise<N | undefined> {\n if (ops.length === 0) return ctx.source().then((r) => r[0] as N | undefined);\n\n return applyOps(ctx, ops).then((r) => r[0] as N | undefined);\n },\n limit(n) {\n const safeN = assertNonNegativeInteger(n, 'query.limit');\n\n return append({ apply: (data) => data.slice(0, safeN), isNonFilter: true });\n },\n offset(n) {\n const safeN = assertNonNegativeInteger(n, 'query.offset');\n\n return append({ apply: (data) => data.slice(safeN), isNonFilter: true });\n },\n orderBy(field, direction = 'asc') {\n return append({\n apply: (data) => {\n const sign = direction === 'asc' ? 1 : -1;\n\n return [...data].sort((a, b) => {\n const av = a[field] as number | string;\n const bv = b[field] as number | string;\n\n if (av === bv) return 0;\n\n return av > bv ? sign : -sign;\n });\n },\n isNonFilter: true,\n });\n },\n startsWith(field, prefix, { ignoreCase = false } = {}) {\n const fieldStr = String(field);\n\n if (!ignoreCase && prefix.length > 0 && ops.length === 0) {\n if (ctx.getRange && ctx.keyField === fieldStr) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getRange!({ prefix, type: 'starts' })),\n ops,\n );\n }\n\n if (ctx.getIndexRange && ctx.indexedFields?.has(fieldStr)) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getIndexRange!(fieldStr, { prefix, type: 'starts' })),\n ops,\n );\n }\n }\n\n const needle = ignoreCase ? prefix.toLowerCase() : prefix;\n\n return append({\n apply: (data) =>\n data.filter((r) => {\n const v = r[field];\n\n if (typeof v !== 'string') return false;\n\n const haystack = ignoreCase ? v.toLowerCase() : v;\n\n return haystack.startsWith(needle);\n }),\n }) as unknown as QueryBuilder<T, N>;\n },\n toArray(): Promise<N[]> {\n return applyOps(ctx, ops) as Promise<N[]>;\n },\n totalCount(): Promise<number> {\n const filterOps = ops.filter((op) => !op.isNonFilter);\n\n return applyOps(ctx, filterOps).then((r) => r.length);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ type Predicate<T> = (value: T, index: number, array: T[]) => boolean;
2
+ /**
3
+ * `isNonFilter`: when true this op is excluded from `totalCount()` — it does not restrict
4
+ * *which* records match (limit, offset, orderBy), only how results are presented.
5
+ */
6
+ type QueryOp<T> = {
7
+ apply: (data: T[]) => T[];
8
+ isNonFilter?: boolean;
9
+ };
10
+ type ComparableFieldKeys<T extends Record<string, unknown>> = {
11
+ [K in keyof T]-?: Extract<NonNullable<T[K]>, number | string> extends never ? never : K;
12
+ }[keyof T];
13
+ /**
14
+ * A primary-key range hint that can be pushed down to native storage backends (e.g. IndexedDB).
15
+ * When `QueryContext.getRange` and `keyField` are present, a matching first filter op replaces
16
+ * the full-table `source()` scan with a targeted range fetch.
17
+ */
18
+ export type NativeRange = {
19
+ type: 'eq';
20
+ value: unknown;
21
+ } | {
22
+ lower: unknown;
23
+ type: 'between';
24
+ upper: unknown;
25
+ } | {
26
+ prefix: string;
27
+ type: 'starts';
28
+ };
29
+ export type QueryContext<T extends Record<string, unknown>> = {
30
+ deleteMany?: (records: T[]) => Promise<number>;
31
+ /**
32
+ * When present alongside `indexedFields`, replaces `source()` for secondary-index filter ops.
33
+ * Only activated when the first op is `equals`, `between`, or `startsWith` on a field that
34
+ * has an index registered in the schema. IndexedDB uses `IDBIndex.getAll(range)` under the hood.
35
+ */
36
+ getIndexRange?: (field: string, range: NativeRange) => Promise<T[]>;
37
+ /**
38
+ * When present alongside `keyField`, replaces `source()` for primary-key filter ops.
39
+ * Only activated when the first op is an `equals`, `between`, or case-sensitive `startsWith`
40
+ * on `keyField`. All remaining ops still run in-memory against the range result.
41
+ */
42
+ getRange?: (range: NativeRange) => Promise<T[]>;
43
+ /** Fields that have secondary indexes — used to detect when a filter op can use `getIndexRange`. */
44
+ indexedFields?: ReadonlySet<string>;
45
+ /** Primary key field name — used to detect when a filter op can be pushed to `getRange`. */
46
+ keyField?: string;
47
+ source: () => Promise<T[]>;
48
+ };
49
+ /**
50
+ * Shared query methods. `T` is the base record type; `N` is the progressively-narrowed type
51
+ * accumulated by `equals()` calls.
52
+ */
53
+ type ChainedQuery<T extends Record<string, unknown>, N extends T, Self extends ChainedQuery<T, N, Self>> = {
54
+ /**
55
+ * Filter records where `field` is between `lower` and `upper` (inclusive).
56
+ * Preserves any type narrowing already accumulated by prior `equals()` calls.
57
+ */
58
+ between<K extends ComparableFieldKeys<T>>(field: K, lower: Extract<NonNullable<T[K]>, number | string>, upper: Extract<NonNullable<T[K]>, number | string>): QueryBuilder<T, N>;
59
+ /**
60
+ * Returns the number of records matching all applied operations, including `limit` and `offset`.
61
+ * To get the full filtered set size regardless of pagination (e.g. for "page X of N" UIs),
62
+ * use `totalCount()` instead.
63
+ */
64
+ count(): Promise<number>;
65
+ /**
66
+ * Filter records where `field` exactly equals `value`.
67
+ * The return type is narrowed to `QueryBuilder<T & Record<K, V>>` so that subsequent
68
+ * `.toArray()`, `.first()`, etc. reflect the equality constraint in their result type.
69
+ *
70
+ * ```ts
71
+ * // result: (User & { role: 'admin' })[]
72
+ * const admins = await db.query('users').equals('role', 'admin').toArray();
73
+ * ```
74
+ */
75
+ equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>>;
76
+ /**
77
+ * Returns `true` if at least one record matches all applied filter operations.
78
+ * Equivalent to `(await query.first()) !== undefined` but makes the intent explicit.
79
+ * Presentation-only ops (`limit`, `offset`, `orderBy`) are respected before checking.
80
+ */
81
+ exists(): Promise<boolean>;
82
+ filter(fn: Predicate<N>): Self;
83
+ first(): Promise<N | undefined>;
84
+ limit(n: number): Self;
85
+ offset(n: number): Self;
86
+ orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): Self;
87
+ /**
88
+ * Filter records where `field` starts with `prefix`.
89
+ * Preserves any type narrowing already accumulated by prior `equals()` calls.
90
+ *
91
+ * **Index push-down** is only active when `ignoreCase` is `false` (default) and this
92
+ * is the first operation in the chain. With `ignoreCase: true`, a full-table scan is
93
+ * always performed regardless of whether the field has an index.
94
+ */
95
+ startsWith<K extends keyof T>(field: K, prefix: string, options?: {
96
+ ignoreCase?: boolean;
97
+ }): QueryBuilder<T, N>;
98
+ toArray(): Promise<N[]>;
99
+ /**
100
+ * Returns the number of records matching the applied filter predicates.
101
+ * Presentation-only ops (`limit`, `offset`, `orderBy`) are intentionally ignored — this
102
+ * always counts the full filtered set, making paginated total-count queries possible
103
+ * without a second query.
104
+ */
105
+ totalCount(): Promise<number>;
106
+ };
107
+ /** Extends the shared query API with `delete()`. Available on both `Adapter.query()` and inside `batch()` callbacks. */
108
+ export interface QueryBuilder<T extends Record<string, unknown>, N extends T = T> extends ChainedQuery<T, N, QueryBuilder<T, N>> {
109
+ delete(): Promise<number>;
110
+ }
111
+ export declare function createQueryBuilder<T extends Record<string, unknown>, N extends T = T>(ctx: QueryContext<T>, ops?: readonly QueryOp<T>[]): QueryBuilder<T, N>;
112
+ export {};
113
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}