@vielzeug/vault 2.1.0 → 2.3.0

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 (87) hide show
  1. package/README.md +0 -19
  2. package/dist/_dev.cjs +1 -1
  3. package/dist/_dev.cjs.map +1 -1
  4. package/dist/_dev.js +2 -3
  5. package/dist/_dev.js.map +1 -1
  6. package/dist/adapter-core.cjs +1 -1
  7. package/dist/adapter-core.cjs.map +1 -1
  8. package/dist/adapter-core.d.ts.map +1 -1
  9. package/dist/adapter-core.js +2 -2
  10. package/dist/adapter-core.js.map +1 -1
  11. package/dist/adapters/indexeddb.cjs +1 -1
  12. package/dist/adapters/indexeddb.cjs.map +1 -1
  13. package/dist/adapters/indexeddb.d.ts +46 -2
  14. package/dist/adapters/indexeddb.d.ts.map +1 -1
  15. package/dist/adapters/indexeddb.js +76 -56
  16. package/dist/adapters/indexeddb.js.map +1 -1
  17. package/dist/adapters/memory.cjs.map +1 -1
  18. package/dist/adapters/memory.d.ts.map +1 -1
  19. package/dist/adapters/memory.js.map +1 -1
  20. package/dist/adapters/sqlite.cjs +1 -1
  21. package/dist/adapters/sqlite.cjs.map +1 -1
  22. package/dist/adapters/sqlite.d.ts +2 -2
  23. package/dist/adapters/sqlite.d.ts.map +1 -1
  24. package/dist/adapters/sqlite.js +1 -1
  25. package/dist/adapters/sqlite.js.map +1 -1
  26. package/dist/adapters/webstorage.cjs +1 -1
  27. package/dist/adapters/webstorage.cjs.map +1 -1
  28. package/dist/adapters/webstorage.d.ts +1 -1
  29. package/dist/adapters/webstorage.d.ts.map +1 -1
  30. package/dist/adapters/webstorage.js +1 -1
  31. package/dist/adapters/webstorage.js.map +1 -1
  32. package/dist/errors.cjs +1 -1
  33. package/dist/errors.cjs.map +1 -1
  34. package/dist/errors.d.ts +0 -1
  35. package/dist/errors.d.ts.map +1 -1
  36. package/dist/errors.js +1 -4
  37. package/dist/errors.js.map +1 -1
  38. package/dist/index.cjs +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -3
  42. package/dist/indexeddb.cjs +1 -1
  43. package/dist/indexeddb.d.ts +3 -4
  44. package/dist/indexeddb.d.ts.map +1 -1
  45. package/dist/indexeddb.js +1 -2
  46. package/dist/internal.cjs.map +1 -1
  47. package/dist/internal.d.ts.map +1 -1
  48. package/dist/internal.js.map +1 -1
  49. package/dist/prune.cjs +1 -1
  50. package/dist/prune.cjs.map +1 -1
  51. package/dist/prune.d.ts +8 -19
  52. package/dist/prune.d.ts.map +1 -1
  53. package/dist/prune.js +9 -13
  54. package/dist/prune.js.map +1 -1
  55. package/dist/query.cjs +1 -1
  56. package/dist/query.cjs.map +1 -1
  57. package/dist/query.d.ts +5 -11
  58. package/dist/query.d.ts.map +1 -1
  59. package/dist/query.js +1 -4
  60. package/dist/query.js.map +1 -1
  61. package/dist/sqlite.d.ts +1 -1
  62. package/dist/sqlite.d.ts.map +1 -1
  63. package/dist/ttl.cjs +1 -1
  64. package/dist/ttl.cjs.map +1 -1
  65. package/dist/ttl.d.ts +8 -19
  66. package/dist/ttl.d.ts.map +1 -1
  67. package/dist/ttl.js +5 -5
  68. package/dist/ttl.js.map +1 -1
  69. package/dist/types.cjs +1 -1
  70. package/dist/types.cjs.map +1 -1
  71. package/dist/types.d.ts +28 -40
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js +14 -19
  74. package/dist/types.js.map +1 -1
  75. package/dist/vault.cjs +1 -1
  76. package/dist/vault.cjs.map +1 -1
  77. package/dist/vault.iife.js +1 -1
  78. package/dist/vault.iife.js.map +1 -1
  79. package/dist/vault.js +1 -1
  80. package/dist/vault.js.map +1 -1
  81. package/package.json +44 -39
  82. package/dist/migration.cjs +0 -2
  83. package/dist/migration.cjs.map +0 -1
  84. package/dist/migration.d.ts +0 -37
  85. package/dist/migration.d.ts.map +0 -1
  86. package/dist/migration.js +0 -25
  87. package/dist/migration.js.map +0 -1
package/dist/prune.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs");require("./_dev.cjs");function t(t,n){if(!Number.isFinite(n.interval)||n.interval<=0)throw new e.VaultError(`scheduleExpiredPrune: interval must be a finite positive number`);let r=!0,i=()=>{r=!1,clearInterval(a)};n.signal?.addEventListener(`abort`,i,{once:!0});let a=setInterval(()=>{r&&t.pruneExpired().catch(t=>{t instanceof e.VaultDisposedError?(r=!1,clearInterval(a)):n.onError?n.onError(t):`${String(t)}`})},n.interval);return i}exports.scheduleExpiredPrune=t;
1
+ require("./_dev.cjs");const e=require("./ttl.cjs");function t(t,n){e.assertPositiveFinite(n.interval,`scheduleExpiredPrune: interval`);let r=setInterval(()=>{t.pruneExpired().catch(e=>{n.onError&&n.onError(e)})},n.interval),i=()=>clearInterval(r);return n.signal?.addEventListener(`abort`,i,{once:!0}),i}exports.scheduleExpiredPrune=t;
2
2
  //# sourceMappingURL=prune.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"prune.cjs","names":[],"sources":["../src/prune.ts"],"sourcesContent":["import type { AnySchema, VaultStore } 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<VaultStore<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":"sDAkBA,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,EAEd,GAA6E,OAAO,CAAG,EAAvF,CAET,CAAC,CACH,EAAG,EAAQ,QAAQ,EAEnB,OAAO,CACT"}
1
+ {"version":3,"file":"prune.cjs","names":[],"sources":["../src/prune.ts"],"sourcesContent":["import { error as logError } from './_dev';\nimport { assertPositiveFinite } from './ttl';\nimport type { AnySchema, VaultStore } from './types';\n\n/**\n * Schedules periodic `pruneExpired()` calls. Returns a `stop` function.\n *\n * Pass `signal: store.disposalSignal` to auto-cancel when the store is torn down.\n * Pass `onError` to handle non-disposal failures explicitly; without it, errors\n * are logged via the dev channel and the schedule continues.\n *\n * ```ts\n * const stop = scheduleExpiredPrune(db, {\n * interval: ttl.hours(1),\n * signal: db.disposalSignal,\n * });\n * ```\n */\nexport function scheduleExpiredPrune<S extends AnySchema>(\n adapter: Pick<VaultStore<S>, 'pruneExpired'>,\n options: {\n interval: number;\n onError?: (err: unknown) => void;\n signal?: AbortSignal;\n },\n): () => void {\n assertPositiveFinite(options.interval, 'scheduleExpiredPrune: interval');\n\n const id = setInterval(() => {\n void adapter.pruneExpired().catch((err) => {\n if (options.onError) options.onError(err);\n else logError('scheduleExpiredPrune: pruneExpired() threw — pass onError to handle this.', err);\n });\n }, options.interval);\n\n const stop = (): void => clearInterval(id);\n\n options.signal?.addEventListener('abort', stop, { once: true });\n\n return stop;\n}\n"],"mappings":"mDAkBA,SAAgB,EACd,EACA,EAKY,CACZ,EAAA,qBAAqB,EAAQ,SAAU,gCAAgC,EAEvE,IAAM,EAAK,gBAAkB,CAC3B,EAAa,aAAa,CAAC,CAAC,MAAO,GAAQ,CACrC,EAAQ,SAAS,EAAQ,QAAQ,CAAG,CAE1C,CAAC,CACH,EAAG,EAAQ,QAAQ,EAEb,MAAmB,cAAc,CAAE,EAIzC,OAFA,EAAQ,QAAQ,iBAAiB,QAAS,EAAM,CAAE,KAAM,EAAK,CAAC,EAEvD,CACT"}
package/dist/prune.d.ts CHANGED
@@ -1,32 +1,21 @@
1
1
  import type { AnySchema, VaultStore } from './types';
2
2
  /**
3
- * Schedules periodic calls to `adapter.pruneExpired()` using `setInterval`.
4
- * Returns a `stop` function to cancel the schedule.
3
+ * Schedules periodic `pruneExpired()` calls. Returns a `stop` function.
5
4
  *
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.
5
+ * Pass `signal: store.disposalSignal` to auto-cancel when the store is torn down.
6
+ * Pass `onError` to handle non-disposal failures explicitly; without it, errors
7
+ * are logged via the dev channel and the schedule continues.
8
8
  *
9
9
  * ```ts
10
- * const stopPrune = scheduleExpiredPrune(db, { interval: ttl.hours(1) });
11
- * // later...
12
- * stopPrune();
10
+ * const stop = scheduleExpiredPrune(db, {
11
+ * interval: ttl.hours(1),
12
+ * signal: db.disposalSignal,
13
+ * });
13
14
  * ```
14
15
  */
15
16
  export declare function scheduleExpiredPrune<S extends AnySchema>(adapter: Pick<VaultStore<S>, 'pruneExpired'>, options: {
16
17
  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
18
  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
19
  signal?: AbortSignal;
31
20
  }): () => void;
32
21
  //# sourceMappingURL=prune.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../src/prune.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAKrD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,SAAS,EACtD,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,EAC5C,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"}
1
+ {"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../src/prune.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,SAAS,EACtD,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,EAC5C,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GACA,MAAM,IAAI,CAeZ"}
package/dist/prune.js CHANGED
@@ -1,20 +1,16 @@
1
- import { VaultDisposedError as e, VaultError as t } from "./errors.js";
2
1
  import "./_dev.js";
2
+ import { assertPositiveFinite as e } from "./ttl.js";
3
3
  //#region src/prune.ts
4
- function n(n, r) {
5
- if (!Number.isFinite(r.interval) || r.interval <= 0) throw new t("scheduleExpiredPrune: interval must be a finite positive number");
6
- let i = !0, a = () => {
7
- i = !1, clearInterval(o);
8
- };
9
- r.signal?.addEventListener("abort", a, { once: !0 });
10
- let o = setInterval(() => {
11
- i && n.pruneExpired().catch((t) => {
12
- t instanceof e ? (i = !1, clearInterval(o)) : r.onError ? r.onError(t) : `${String(t)}`;
4
+ function t(t, n) {
5
+ e(n.interval, "scheduleExpiredPrune: interval");
6
+ let r = setInterval(() => {
7
+ t.pruneExpired().catch((e) => {
8
+ n.onError && n.onError(e);
13
9
  });
14
- }, r.interval);
15
- return a;
10
+ }, n.interval), i = () => clearInterval(r);
11
+ return n.signal?.addEventListener("abort", i, { once: !0 }), i;
16
12
  }
17
13
  //#endregion
18
- export { n as scheduleExpiredPrune };
14
+ export { t as scheduleExpiredPrune };
19
15
 
20
16
  //# sourceMappingURL=prune.js.map
package/dist/prune.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"prune.js","names":[],"sources":["../src/prune.ts"],"sourcesContent":["import type { AnySchema, VaultStore } 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<VaultStore<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,IAEd,GAA6E,OAAO,CAAG,EAAvF;EAET,CAAC;CACH,GAAG,EAAQ,QAAQ;CAEnB,OAAO;AACT"}
1
+ {"version":3,"file":"prune.js","names":[],"sources":["../src/prune.ts"],"sourcesContent":["import { error as logError } from './_dev';\nimport { assertPositiveFinite } from './ttl';\nimport type { AnySchema, VaultStore } from './types';\n\n/**\n * Schedules periodic `pruneExpired()` calls. Returns a `stop` function.\n *\n * Pass `signal: store.disposalSignal` to auto-cancel when the store is torn down.\n * Pass `onError` to handle non-disposal failures explicitly; without it, errors\n * are logged via the dev channel and the schedule continues.\n *\n * ```ts\n * const stop = scheduleExpiredPrune(db, {\n * interval: ttl.hours(1),\n * signal: db.disposalSignal,\n * });\n * ```\n */\nexport function scheduleExpiredPrune<S extends AnySchema>(\n adapter: Pick<VaultStore<S>, 'pruneExpired'>,\n options: {\n interval: number;\n onError?: (err: unknown) => void;\n signal?: AbortSignal;\n },\n): () => void {\n assertPositiveFinite(options.interval, 'scheduleExpiredPrune: interval');\n\n const id = setInterval(() => {\n void adapter.pruneExpired().catch((err) => {\n if (options.onError) options.onError(err);\n else logError('scheduleExpiredPrune: pruneExpired() threw — pass onError to handle this.', err);\n });\n }, options.interval);\n\n const stop = (): void => clearInterval(id);\n\n options.signal?.addEventListener('abort', stop, { once: true });\n\n return stop;\n}\n"],"mappings":";;;AAkBA,SAAgB,EACd,GACA,GAKY;CACZ,EAAqB,EAAQ,UAAU,gCAAgC;CAEvE,IAAM,IAAK,kBAAkB;EAC3B,EAAa,aAAa,CAAC,CAAC,OAAO,MAAQ;GACzC,AAAI,EAAQ,WAAS,EAAQ,QAAQ,CAAG;EAE1C,CAAC;CACH,GAAG,EAAQ,QAAQ,GAEb,UAAmB,cAAc,CAAE;CAIzC,OAFA,EAAQ,QAAQ,iBAAiB,SAAS,GAAM,EAAE,MAAM,GAAK,CAAC,GAEvD;AACT"}
package/dist/query.cjs CHANGED
@@ -1,2 +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)})})},toArray(){return t(a,o)},totalCount(){return t(a,o.filter(e=>!e.isNonFilter)).then(e=>e.length)}}}exports.createQueryBuilder=i;
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.filter(e=>!e.isNonFilter)).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)})})},toArray(){return t(a,o)}}}exports.createQueryBuilder=i;
2
2
  //# sourceMappingURL=query.cjs.map
@@ -1 +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 object> = {\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 object> = {\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 object, 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 stores and IndexedDB transaction callbacks. */\nexport interface QueryBuilder<T extends object, N extends T = T> extends ChainedQuery<T, N, QueryBuilder<T, N>> {\n delete(): Promise<number>;\n}\n\n/* -------------------- Helpers -------------------- */\n\nasync function applyOps<T extends object>(ctx: QueryContext<T>, ops: readonly QueryOp<T>[]): 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 object>(ctx: QueryContext<T>, newSource: () => Promise<T[]>): QueryContext<T> {\n return { deleteMany: ctx.deleteMany, source: newSource };\n}\n\n/* -------------------- Factory -------------------- */\n\nexport function createQueryBuilder<T extends object, 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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"}
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 `count()` — 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 object> = {\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 object> = {\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 object, 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 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 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\n/** Extends the shared query API with `delete()`. Available on stores and IndexedDB transaction callbacks. */\nexport interface QueryBuilder<T extends object, N extends T = T> extends ChainedQuery<T, N, QueryBuilder<T, N>> {\n delete(): Promise<number>;\n}\n\n/* -------------------- Helpers -------------------- */\n\nasync function applyOps<T extends object>(ctx: QueryContext<T>, ops: readonly QueryOp<T>[]): 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 object>(ctx: QueryContext<T>, newSource: () => Promise<T[]>): QueryContext<T> {\n return { deleteMany: ctx.deleteMany, source: newSource };\n}\n\n/* -------------------- Factory -------------------- */\n\nexport function createQueryBuilder<T extends object, 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 const filterOps = ops.filter((op) => !op.isNonFilter);\n\n return applyOps(ctx, filterOps).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 };\n}\n"],"mappings":"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"}
package/dist/query.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  type Predicate<T> = (value: T, index: number, array: T[]) => boolean;
2
2
  /**
3
- * `isNonFilter`: when true this op is excluded from `totalCount()` — it does not restrict
3
+ * `isNonFilter`: when true this op is excluded from `count()` — it does not restrict
4
4
  * *which* records match (limit, offset, orderBy), only how results are presented.
5
5
  */
6
6
  type QueryOp<T> = {
@@ -57,9 +57,10 @@ type ChainedQuery<T extends object, N extends T, Self extends ChainedQuery<T, N,
57
57
  */
58
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
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.
60
+ * Returns the number of records matching the applied filter predicates.
61
+ * Presentation-only ops (`limit`, `offset`, `orderBy`) are intentionally ignored this
62
+ * always counts the full filtered set, making paginated total-count queries possible
63
+ * without a second query.
63
64
  */
64
65
  count(): Promise<number>;
65
66
  /**
@@ -96,13 +97,6 @@ type ChainedQuery<T extends object, N extends T, Self extends ChainedQuery<T, N,
96
97
  ignoreCase?: boolean;
97
98
  }): QueryBuilder<T, N>;
98
99
  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
100
  };
107
101
  /** Extends the shared query API with `delete()`. Available on stores and IndexedDB transaction callbacks. */
108
102
  export interface QueryBuilder<T extends object, N extends T = T> extends ChainedQuery<T, N, QueryBuilder<T, N>> {
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAEA,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC;AACrE;;;GAGG;AACH,KAAK,OAAO,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AACvE,KAAK,mBAAmB,CAAC,CAAC,SAAS,MAAM,IAAI;KAC1C,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,SAAS,KAAK,GAAG,KAAK,GAAG,CAAC;CACxF,CAAC,MAAM,CAAC,CAAC,CAAC;AAEX;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACnD;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEvC,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,IAAI;IAC3C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACpE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAChD,oGAAoG;IACpG,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;CAC5B,CAAC;AAIF;;;GAGG;AACH,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,IAAI,SAAS,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI;IACxF;;;OAGG;IACH,OAAO,CAAC,CAAC,SAAS,mBAAmB,CAAC,CAAC,CAAC,EACtC,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,EAClD,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,GACjD,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtB;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB;;;;;;;;;OASG;IACH,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvG;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3B,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC/B,KAAK,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAChC,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IACvE;;;;;;;OAOG;IACH,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChH,OAAO,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACxB;;;;;OAKG;IACH,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/B,CAAC;AAEF,6GAA6G;AAC7G,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAE,SAAQ,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7G,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3B;AA+BD,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAClE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,EACpB,GAAG,GAAE,SAAS,OAAO,CAAC,CAAC,CAAC,EAAO,GAC9B,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CA2JpB"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
package/dist/query.js CHANGED
@@ -38,7 +38,7 @@ function i(a, o = []) {
38
38
  }) });
39
39
  },
40
40
  count() {
41
- return t(a, o).then((e) => e.length);
41
+ return t(a, o.filter((e) => !e.isNonFilter)).then((e) => e.length);
42
42
  },
43
43
  async delete() {
44
44
  if (!a.deleteMany) throw new e("query.delete is not available for this adapter context");
@@ -114,9 +114,6 @@ function i(a, o = []) {
114
114
  },
115
115
  toArray() {
116
116
  return t(a, o);
117
- },
118
- totalCount() {
119
- return t(a, o.filter((e) => !e.isNonFilter)).then((e) => e.length);
120
117
  }
121
118
  };
122
119
  }
package/dist/query.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","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 object> = {\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 object> = {\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 object, 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 stores and IndexedDB transaction callbacks. */\nexport interface QueryBuilder<T extends object, N extends T = T> extends ChainedQuery<T, N, QueryBuilder<T, N>> {\n delete(): Promise<number>;\n}\n\n/* -------------------- Helpers -------------------- */\n\nasync function applyOps<T extends object>(ctx: QueryContext<T>, ops: readonly QueryOp<T>[]): 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 object>(ctx: QueryContext<T>, newSource: () => Promise<T[]>): QueryContext<T> {\n return { deleteMany: ctx.deleteMany, source: newSource };\n}\n\n/* -------------------- Factory -------------------- */\n\nexport function createQueryBuilder<T extends object, 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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"}
1
+ {"version":3,"file":"query.js","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 `count()` — 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 object> = {\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 object> = {\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 object, 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 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 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\n/** Extends the shared query API with `delete()`. Available on stores and IndexedDB transaction callbacks. */\nexport interface QueryBuilder<T extends object, N extends T = T> extends ChainedQuery<T, N, QueryBuilder<T, N>> {\n delete(): Promise<number>;\n}\n\n/* -------------------- Helpers -------------------- */\n\nasync function applyOps<T extends object>(ctx: QueryContext<T>, ops: readonly QueryOp<T>[]): 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 object>(ctx: QueryContext<T>, newSource: () => Promise<T[]>): QueryContext<T> {\n return { deleteMany: ctx.deleteMany, source: newSource };\n}\n\n/* -------------------- Factory -------------------- */\n\nexport function createQueryBuilder<T extends object, 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 const filterOps = ops.filter((op) => !op.isNonFilter);\n\n return applyOps(ctx, filterOps).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 };\n}\n"],"mappings":"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"}
package/dist/sqlite.d.ts CHANGED
@@ -1,3 +1,3 @@
1
+ export type { SQLiteDatabase, SQLiteParameter, SQLiteStatement, SQLiteVaultOptions, SQLiteVaultStore, TransactionContext, } from './adapters/sqlite';
1
2
  export { createSQLite } from './adapters/sqlite';
2
- export type { SQLiteDatabase, SQLiteParameter, SQLiteStatement, SQLiteVaultOptions, SQLiteVaultStore, } from './adapters/sqlite';
3
3
  //# sourceMappingURL=sqlite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EACV,cAAc,EACd,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/ttl.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs");var t={days:e=>r(r(e,`ttl.days`)*864e5,`ttl.days (result)`),hours:e=>r(r(e,`ttl.hours`)*36e5,`ttl.hours (result)`),minutes:e=>r(r(e,`ttl.minutes`)*6e4,`ttl.minutes (result)`),ms:e=>r(e,`ttl.ms`),seconds:e=>r(r(e,`ttl.seconds`)*1e3,`ttl.seconds (result)`)};function n(e){return e!==void 0&&Date.now()>=e}function r(t,n){if(!Number.isFinite(t)||t<=0)throw new e.VaultError(`${n} expected a finite positive number, received ${String(t)}`);return t}function i(e){if(typeof e!=`object`||!e||!(`value`in e))return;let t=e;if(!(t.expiresAt!==void 0&&(typeof t.expiresAt!=`number`||!Number.isFinite(t.expiresAt))))return t}exports.assertTtlMs=r,exports.isExpired=n,exports.parseStored=i,exports.ttl=t;
1
+ const e=require("./errors.cjs");var t={days:e=>r(e*864e5,`ttl.days`),hours:e=>r(e*36e5,`ttl.hours`),minutes:e=>r(e*6e4,`ttl.minutes`),ms:e=>r(e,`ttl.ms`),seconds:e=>r(e*1e3,`ttl.seconds`)};function n(e){return e!==void 0&&Date.now()>=e}function r(t,n){if(!Number.isFinite(t)||t<=0)throw new e.VaultError(`${n} expected a finite positive number, received ${String(t)}`);return t}function i(e){if(typeof e!=`object`||!e||!(`value`in e))return;let t=e;if(!(t.expiresAt!==void 0&&(typeof t.expiresAt!=`number`||!Number.isFinite(t.expiresAt))))return t}exports.assertPositiveFinite=r,exports.isExpired=n,exports.parseStored=i,exports.ttl=t;
2
2
  //# sourceMappingURL=ttl.cjs.map
package/dist/ttl.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ttl.cjs","names":[],"sources":["../src/ttl.ts"],"sourcesContent":["import { VaultError } from './errors';\n\ndeclare const ttlMsBrand: unique symbol;\n/** A duration in milliseconds, produced by the `ttl.*` helpers. */\nexport type TtlMs = number & { readonly [ttlMsBrand]: never };\n\nexport const ttl = {\n days: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.days') * 86_400_000, 'ttl.days (result)'),\n hours: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.hours') * 3_600_000, 'ttl.hours (result)'),\n minutes: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.minutes') * 60_000, 'ttl.minutes (result)'),\n ms: (n: number) => assertTtlMs(n, 'ttl.ms'),\n seconds: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.seconds') * 1000, 'ttl.seconds (result)'),\n} as const;\n\n/** Fixed storage envelope shared by every adapter, so data and indexes stay portable. */\nexport type StoredRecord<T> = { expiresAt?: number; value: T };\n\nexport function isExpired(expiresAt: number | undefined): boolean {\n return expiresAt !== undefined && Date.now() >= expiresAt;\n}\n\nexport function assertTtlMs(ttlMs: number, source: string): TtlMs {\n if (!Number.isFinite(ttlMs) || ttlMs <= 0) {\n throw new VaultError(`${source} expected a finite positive number, received ${String(ttlMs)}`);\n }\n\n return ttlMs as TtlMs;\n}\n\nexport function wrapStored<T>(value: T, ttlMs?: TtlMs): StoredRecord<T> {\n return ttlMs === undefined ? { value } : { expiresAt: Date.now() + ttlMs, value };\n}\n\nexport function unwrapStored<T>(raw: StoredRecord<T>): T | undefined {\n return isExpired(raw.expiresAt) ? undefined : raw.value;\n}\n\nexport function parseStored<T>(raw: unknown): StoredRecord<T> | undefined {\n if (typeof raw !== 'object' || raw === null || !('value' in raw)) return undefined;\n\n const record = raw as { expiresAt?: unknown; value: unknown };\n\n if (record.expiresAt !== undefined && (typeof record.expiresAt !== 'number' || !Number.isFinite(record.expiresAt))) {\n return undefined;\n }\n\n return record as StoredRecord<T>;\n}\n\nexport function readWithTtl<T>(raw: unknown): { expired: boolean; found: boolean; value: T | undefined } {\n const parsed = parseStored<T>(raw);\n\n if (!parsed) return { expired: false, found: false, value: undefined };\n\n const value = unwrapStored(parsed);\n\n return { expired: value === undefined, found: true, value };\n}\n"],"mappings":"gCAMA,IAAa,EAAM,CACjB,KAAO,GAAc,EAAY,EAAY,EAAG,UAAU,EAAI,MAAY,mBAAmB,EAC7F,MAAQ,GAAc,EAAY,EAAY,EAAG,WAAW,EAAI,KAAW,oBAAoB,EAC/F,QAAU,GAAc,EAAY,EAAY,EAAG,aAAa,EAAI,IAAQ,sBAAsB,EAClG,GAAK,GAAc,EAAY,EAAG,QAAQ,EAC1C,QAAU,GAAc,EAAY,EAAY,EAAG,aAAa,EAAI,IAAM,sBAAsB,CAClG,EAKA,SAAgB,EAAU,EAAwC,CAChE,OAAO,IAAc,IAAA,IAAa,KAAK,IAAI,GAAK,CAClD,CAEA,SAAgB,EAAY,EAAe,EAAuB,CAChE,GAAI,CAAC,OAAO,SAAS,CAAK,GAAK,GAAS,EACtC,MAAM,IAAI,EAAA,WAAW,GAAG,EAAO,+CAA+C,OAAO,CAAK,GAAG,EAG/F,OAAO,CACT,CAUA,SAAgB,EAAe,EAA2C,CACxE,GAAI,OAAO,GAAQ,WAAY,GAAgB,EAAE,UAAW,GAAM,OAElE,IAAM,EAAS,EAEX,OAAO,YAAc,IAAA,KAAc,OAAO,EAAO,WAAc,UAAY,CAAC,OAAO,SAAS,EAAO,SAAS,IAIhH,OAAO,CACT"}
1
+ {"version":3,"file":"ttl.cjs","names":[],"sources":["../src/ttl.ts"],"sourcesContent":["import { VaultError } from './errors';\n\n/** Duration helpers that produce finite, positive millisecond values. */\nexport const ttl = {\n days: (n: number) => assertPositiveFinite(n * 86_400_000, 'ttl.days'),\n hours: (n: number) => assertPositiveFinite(n * 3_600_000, 'ttl.hours'),\n minutes: (n: number) => assertPositiveFinite(n * 60_000, 'ttl.minutes'),\n ms: (n: number) => assertPositiveFinite(n, 'ttl.ms'),\n seconds: (n: number) => assertPositiveFinite(n * 1000, 'ttl.seconds'),\n} as const;\n\n/** Fixed storage envelope shared by every adapter, so data and indexes stay portable. */\nexport type StoredRecord<T> = { expiresAt?: number; value: T };\n\nexport function isExpired(expiresAt: number | undefined): boolean {\n return expiresAt !== undefined && Date.now() >= expiresAt;\n}\n\n/** Throws when `ttlMs` is not a finite, positive number. Returns it unchanged otherwise. */\nexport function assertPositiveFinite(ttlMs: number, source: string): number {\n if (!Number.isFinite(ttlMs) || ttlMs <= 0) {\n throw new VaultError(`${source} expected a finite positive number, received ${String(ttlMs)}`);\n }\n\n return ttlMs;\n}\n\nexport function parseStored<T>(raw: unknown): StoredRecord<T> | undefined {\n if (typeof raw !== 'object' || raw === null || !('value' in raw)) return undefined;\n\n const record = raw as { expiresAt?: unknown; value: unknown };\n\n if (record.expiresAt !== undefined && (typeof record.expiresAt !== 'number' || !Number.isFinite(record.expiresAt))) {\n return undefined;\n }\n\n return record as StoredRecord<T>;\n}\n"],"mappings":"gCAGA,IAAa,EAAM,CACjB,KAAO,GAAc,EAAqB,EAAI,MAAY,UAAU,EACpE,MAAQ,GAAc,EAAqB,EAAI,KAAW,WAAW,EACrE,QAAU,GAAc,EAAqB,EAAI,IAAQ,aAAa,EACtE,GAAK,GAAc,EAAqB,EAAG,QAAQ,EACnD,QAAU,GAAc,EAAqB,EAAI,IAAM,aAAa,CACtE,EAKA,SAAgB,EAAU,EAAwC,CAChE,OAAO,IAAc,IAAA,IAAa,KAAK,IAAI,GAAK,CAClD,CAGA,SAAgB,EAAqB,EAAe,EAAwB,CAC1E,GAAI,CAAC,OAAO,SAAS,CAAK,GAAK,GAAS,EACtC,MAAM,IAAI,EAAA,WAAW,GAAG,EAAO,+CAA+C,OAAO,CAAK,GAAG,EAG/F,OAAO,CACT,CAEA,SAAgB,EAAe,EAA2C,CACxE,GAAI,OAAO,GAAQ,WAAY,GAAgB,EAAE,UAAW,GAAM,OAElE,IAAM,EAAS,EAEX,OAAO,YAAc,IAAA,KAAc,OAAO,EAAO,WAAc,UAAY,CAAC,OAAO,SAAS,EAAO,SAAS,IAIhH,OAAO,CACT"}
package/dist/ttl.d.ts CHANGED
@@ -1,14 +1,10 @@
1
- declare const ttlMsBrand: unique symbol;
2
- /** A duration in milliseconds, produced by the `ttl.*` helpers. */
3
- export type TtlMs = number & {
4
- readonly [ttlMsBrand]: never;
5
- };
1
+ /** Duration helpers that produce finite, positive millisecond values. */
6
2
  export declare const ttl: {
7
- readonly days: (n: number) => TtlMs;
8
- readonly hours: (n: number) => TtlMs;
9
- readonly minutes: (n: number) => TtlMs;
10
- readonly ms: (n: number) => TtlMs;
11
- readonly seconds: (n: number) => TtlMs;
3
+ readonly days: (n: number) => number;
4
+ readonly hours: (n: number) => number;
5
+ readonly minutes: (n: number) => number;
6
+ readonly ms: (n: number) => number;
7
+ readonly seconds: (n: number) => number;
12
8
  };
13
9
  /** Fixed storage envelope shared by every adapter, so data and indexes stay portable. */
14
10
  export type StoredRecord<T> = {
@@ -16,14 +12,7 @@ export type StoredRecord<T> = {
16
12
  value: T;
17
13
  };
18
14
  export declare function isExpired(expiresAt: number | undefined): boolean;
19
- export declare function assertTtlMs(ttlMs: number, source: string): TtlMs;
20
- export declare function wrapStored<T>(value: T, ttlMs?: TtlMs): StoredRecord<T>;
21
- export declare function unwrapStored<T>(raw: StoredRecord<T>): T | undefined;
15
+ /** Throws when `ttlMs` is not a finite, positive number. Returns it unchanged otherwise. */
16
+ export declare function assertPositiveFinite(ttlMs: number, source: string): number;
22
17
  export declare function parseStored<T>(raw: unknown): StoredRecord<T> | undefined;
23
- export declare function readWithTtl<T>(raw: unknown): {
24
- expired: boolean;
25
- found: boolean;
26
- value: T | undefined;
27
- };
28
- export {};
29
18
  //# sourceMappingURL=ttl.d.ts.map
package/dist/ttl.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ttl.d.ts","sourceRoot":"","sources":["../src/ttl.ts"],"names":[],"mappings":"AAEA,OAAO,CAAC,MAAM,UAAU,EAAE,OAAO,MAAM,CAAC;AACxC,mEAAmE;AACnE,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAE9D,eAAO,MAAM,GAAG;uBACJ,MAAM;wBACL,MAAM;0BACJ,MAAM;qBACX,MAAM;0BACD,MAAM;CACX,CAAC;AAEX,yFAAyF;AACzF,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAE/D,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEhE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAMhE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAEtE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAEnE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAUxE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,SAAS,CAAA;CAAE,CAQvG"}
1
+ {"version":3,"file":"ttl.d.ts","sourceRoot":"","sources":["../src/ttl.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,eAAO,MAAM,GAAG;uBACJ,MAAM;wBACL,MAAM;0BACJ,MAAM;qBACX,MAAM;0BACD,MAAM;CACX,CAAC;AAEX,yFAAyF;AACzF,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAE/D,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEhE;AAED,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAM1E;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAUxE"}
package/dist/ttl.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { VaultError as e } from "./errors.js";
2
2
  //#region src/ttl.ts
3
3
  var t = {
4
- days: (e) => r(r(e, "ttl.days") * 864e5, "ttl.days (result)"),
5
- hours: (e) => r(r(e, "ttl.hours") * 36e5, "ttl.hours (result)"),
6
- minutes: (e) => r(r(e, "ttl.minutes") * 6e4, "ttl.minutes (result)"),
4
+ days: (e) => r(e * 864e5, "ttl.days"),
5
+ hours: (e) => r(e * 36e5, "ttl.hours"),
6
+ minutes: (e) => r(e * 6e4, "ttl.minutes"),
7
7
  ms: (e) => r(e, "ttl.ms"),
8
- seconds: (e) => r(r(e, "ttl.seconds") * 1e3, "ttl.seconds (result)")
8
+ seconds: (e) => r(e * 1e3, "ttl.seconds")
9
9
  };
10
10
  function n(e) {
11
11
  return e !== void 0 && Date.now() >= e;
@@ -20,6 +20,6 @@ function i(e) {
20
20
  if (!(t.expiresAt !== void 0 && (typeof t.expiresAt != "number" || !Number.isFinite(t.expiresAt)))) return t;
21
21
  }
22
22
  //#endregion
23
- export { r as assertTtlMs, n as isExpired, i as parseStored, t as ttl };
23
+ export { r as assertPositiveFinite, n as isExpired, i as parseStored, t as ttl };
24
24
 
25
25
  //# sourceMappingURL=ttl.js.map
package/dist/ttl.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ttl.js","names":[],"sources":["../src/ttl.ts"],"sourcesContent":["import { VaultError } from './errors';\n\ndeclare const ttlMsBrand: unique symbol;\n/** A duration in milliseconds, produced by the `ttl.*` helpers. */\nexport type TtlMs = number & { readonly [ttlMsBrand]: never };\n\nexport const ttl = {\n days: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.days') * 86_400_000, 'ttl.days (result)'),\n hours: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.hours') * 3_600_000, 'ttl.hours (result)'),\n minutes: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.minutes') * 60_000, 'ttl.minutes (result)'),\n ms: (n: number) => assertTtlMs(n, 'ttl.ms'),\n seconds: (n: number) => assertTtlMs(assertTtlMs(n, 'ttl.seconds') * 1000, 'ttl.seconds (result)'),\n} as const;\n\n/** Fixed storage envelope shared by every adapter, so data and indexes stay portable. */\nexport type StoredRecord<T> = { expiresAt?: number; value: T };\n\nexport function isExpired(expiresAt: number | undefined): boolean {\n return expiresAt !== undefined && Date.now() >= expiresAt;\n}\n\nexport function assertTtlMs(ttlMs: number, source: string): TtlMs {\n if (!Number.isFinite(ttlMs) || ttlMs <= 0) {\n throw new VaultError(`${source} expected a finite positive number, received ${String(ttlMs)}`);\n }\n\n return ttlMs as TtlMs;\n}\n\nexport function wrapStored<T>(value: T, ttlMs?: TtlMs): StoredRecord<T> {\n return ttlMs === undefined ? { value } : { expiresAt: Date.now() + ttlMs, value };\n}\n\nexport function unwrapStored<T>(raw: StoredRecord<T>): T | undefined {\n return isExpired(raw.expiresAt) ? undefined : raw.value;\n}\n\nexport function parseStored<T>(raw: unknown): StoredRecord<T> | undefined {\n if (typeof raw !== 'object' || raw === null || !('value' in raw)) return undefined;\n\n const record = raw as { expiresAt?: unknown; value: unknown };\n\n if (record.expiresAt !== undefined && (typeof record.expiresAt !== 'number' || !Number.isFinite(record.expiresAt))) {\n return undefined;\n }\n\n return record as StoredRecord<T>;\n}\n\nexport function readWithTtl<T>(raw: unknown): { expired: boolean; found: boolean; value: T | undefined } {\n const parsed = parseStored<T>(raw);\n\n if (!parsed) return { expired: false, found: false, value: undefined };\n\n const value = unwrapStored(parsed);\n\n return { expired: value === undefined, found: true, value };\n}\n"],"mappings":";;AAMA,IAAa,IAAM;CACjB,OAAO,MAAc,EAAY,EAAY,GAAG,UAAU,IAAI,OAAY,mBAAmB;CAC7F,QAAQ,MAAc,EAAY,EAAY,GAAG,WAAW,IAAI,MAAW,oBAAoB;CAC/F,UAAU,MAAc,EAAY,EAAY,GAAG,aAAa,IAAI,KAAQ,sBAAsB;CAClG,KAAK,MAAc,EAAY,GAAG,QAAQ;CAC1C,UAAU,MAAc,EAAY,EAAY,GAAG,aAAa,IAAI,KAAM,sBAAsB;AAClG;AAKA,SAAgB,EAAU,GAAwC;CAChE,OAAO,MAAc,KAAA,KAAa,KAAK,IAAI,KAAK;AAClD;AAEA,SAAgB,EAAY,GAAe,GAAuB;CAChE,IAAI,CAAC,OAAO,SAAS,CAAK,KAAK,KAAS,GACtC,MAAM,IAAI,EAAW,GAAG,EAAO,+CAA+C,OAAO,CAAK,GAAG;CAG/F,OAAO;AACT;AAUA,SAAgB,EAAe,GAA2C;CACxE,IAAI,OAAO,KAAQ,aAAY,KAAgB,EAAE,WAAW,IAAM;CAElE,IAAM,IAAS;CAEX,QAAO,cAAc,KAAA,MAAc,OAAO,EAAO,aAAc,YAAY,CAAC,OAAO,SAAS,EAAO,SAAS,KAIhH,OAAO;AACT"}
1
+ {"version":3,"file":"ttl.js","names":[],"sources":["../src/ttl.ts"],"sourcesContent":["import { VaultError } from './errors';\n\n/** Duration helpers that produce finite, positive millisecond values. */\nexport const ttl = {\n days: (n: number) => assertPositiveFinite(n * 86_400_000, 'ttl.days'),\n hours: (n: number) => assertPositiveFinite(n * 3_600_000, 'ttl.hours'),\n minutes: (n: number) => assertPositiveFinite(n * 60_000, 'ttl.minutes'),\n ms: (n: number) => assertPositiveFinite(n, 'ttl.ms'),\n seconds: (n: number) => assertPositiveFinite(n * 1000, 'ttl.seconds'),\n} as const;\n\n/** Fixed storage envelope shared by every adapter, so data and indexes stay portable. */\nexport type StoredRecord<T> = { expiresAt?: number; value: T };\n\nexport function isExpired(expiresAt: number | undefined): boolean {\n return expiresAt !== undefined && Date.now() >= expiresAt;\n}\n\n/** Throws when `ttlMs` is not a finite, positive number. Returns it unchanged otherwise. */\nexport function assertPositiveFinite(ttlMs: number, source: string): number {\n if (!Number.isFinite(ttlMs) || ttlMs <= 0) {\n throw new VaultError(`${source} expected a finite positive number, received ${String(ttlMs)}`);\n }\n\n return ttlMs;\n}\n\nexport function parseStored<T>(raw: unknown): StoredRecord<T> | undefined {\n if (typeof raw !== 'object' || raw === null || !('value' in raw)) return undefined;\n\n const record = raw as { expiresAt?: unknown; value: unknown };\n\n if (record.expiresAt !== undefined && (typeof record.expiresAt !== 'number' || !Number.isFinite(record.expiresAt))) {\n return undefined;\n }\n\n return record as StoredRecord<T>;\n}\n"],"mappings":";;AAGA,IAAa,IAAM;CACjB,OAAO,MAAc,EAAqB,IAAI,OAAY,UAAU;CACpE,QAAQ,MAAc,EAAqB,IAAI,MAAW,WAAW;CACrE,UAAU,MAAc,EAAqB,IAAI,KAAQ,aAAa;CACtE,KAAK,MAAc,EAAqB,GAAG,QAAQ;CACnD,UAAU,MAAc,EAAqB,IAAI,KAAM,aAAa;AACtE;AAKA,SAAgB,EAAU,GAAwC;CAChE,OAAO,MAAc,KAAA,KAAa,KAAK,IAAI,KAAK;AAClD;AAGA,SAAgB,EAAqB,GAAe,GAAwB;CAC1E,IAAI,CAAC,OAAO,SAAS,CAAK,KAAK,KAAS,GACtC,MAAM,IAAI,EAAW,GAAG,EAAO,+CAA+C,OAAO,CAAK,GAAG;CAG/F,OAAO;AACT;AAEA,SAAgB,EAAe,GAA2C;CACxE,IAAI,OAAO,KAAQ,aAAY,KAAgB,EAAE,WAAW,IAAM;CAElE,IAAM,IAAS;CAEX,QAAO,cAAc,KAAA,MAAc,OAAO,EAAO,aAAc,YAAY,CAAC,OAAO,SAAS,EAAO,SAAS,KAIhH,OAAO;AACT"}
package/dist/types.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs"),t=require("./ttl.cjs");function n(n){function r(n){return{...n,index:t=>{let i=n.indexes??[];if(i.includes(t))throw new e.VaultError(`table index "${t}" is already registered`);return r({...n,indexes:[...i,t]})},ttl:e=>(t.assertTtlMs(e,`table.ttl`),r({...n,defaultTtl:e}))}}return r({key:n})}exports.table=n;
1
+ const e=require("./errors.cjs"),t=require("./ttl.cjs");function n(n,r={}){let{defaultTtl:i,indexes:a}=r;if(i!==void 0&&t.assertPositiveFinite(i,`table: defaultTtl`),a){let t=new Set;for(let n of a){if(t.has(n))throw new e.VaultError(`table: index "${n}" is already registered`);t.add(n)}}return{defaultTtl:i,indexes:a,key:n}}exports.table=n;
2
2
  //# sourceMappingURL=types.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.cjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\n\nimport type { QueryBuilder } from './query';\n\nimport { VaultError } from './errors';\nimport { assertTtlMs, type TtlMs } from './ttl';\n\nexport type { TtlMs };\n\n/** Portable primary-key values. Vault preserves their type and encodes them distinctly at rest. */\nexport type VaultKey = number | string;\n\ndeclare const schemaEntryBrand: unique symbol;\n\n/** A typed table definition whose primary-key field must hold a portable Vault key. */\nexport type SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> = T[Key] extends VaultKey\n ? {\n defaultTtl?: TtlMs;\n /** IndexedDB creates these as `value.<field>` indexes; other stores filter in memory. */\n indexes?: readonly (keyof T & string)[];\n key: Key;\n readonly [schemaEntryBrand]?: T;\n }\n : never;\n\nexport type AnySchema = Record<string, { defaultTtl?: TtlMs; indexes?: readonly string[]; key: string }>;\n\nexport type TableBuilder<T extends object, Key extends keyof T & string = keyof T & string> = SchemaEntry<T, Key> & {\n index: <F extends keyof T & string>(field: F) => TableBuilder<T, Key>;\n ttl: (ms: TtlMs) => TableBuilder<T, Key>;\n};\n\n/** Define a table once so all stores share the same record and portable-key contract. */\nexport function table<T extends object, Key extends keyof T & string = keyof T & string>(\n key: Key & (T[Key] extends VaultKey ? unknown : never),\n): TableBuilder<T, Key> {\n function makeBuilder(entry: SchemaEntry<T, Key>): TableBuilder<T, Key> {\n return {\n ...entry,\n index: <F extends keyof T & string>(field: F): TableBuilder<T, Key> => {\n const current = (entry.indexes ?? []) as readonly (keyof T & string)[];\n\n if (current.includes(field)) {\n throw new VaultError(`table index \"${field}\" is already registered`);\n }\n\n return makeBuilder({ ...entry, indexes: [...current, field] });\n },\n ttl: (ms: TtlMs): TableBuilder<T, Key> => {\n assertTtlMs(ms, 'table.ttl');\n\n return makeBuilder({ ...entry, defaultTtl: ms });\n },\n };\n }\n\n return makeBuilder({ key } as unknown as SchemaEntry<T, Key>);\n}\n\nexport type RecordOf<S extends AnySchema, K extends keyof S> =\n S[K] extends SchemaEntry<infer R, infer _Key> ? R : never;\nexport type KeyOf<S extends AnySchema, K extends keyof S> = Extract<\n S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never,\n VaultKey\n>;\n\nexport type MigrationContext = {\n db: IDBDatabase;\n newVersion: number | null;\n oldVersion: number;\n tx: IDBTransaction;\n};\n\nexport type MigrationFn = (ctx: MigrationContext) => void;\n\nexport interface VaultLogger {\n error(messageOrContext?: Record<string, unknown> | Error | string, message?: string): void;\n}\n\nexport interface RecordValidator<T> {\n parse(value: unknown): T;\n}\n\nexport type TableValidators<S extends AnySchema> = {\n [K in keyof S]?: RecordValidator<RecordOf<S, K>>;\n};\n\nexport type Observer<T> = (records: T[]) => void;\nexport type Unsubscribe = () => void;\n\n/** Shared factory options. Values always use Vault's fixed `{ value, expiresAt? }` envelope. */\nexport type BaseAdapterOptions<S extends AnySchema> = {\n logger?: VaultLogger;\n onMetrics?: (event: MetricsEvent) => void;\n schema: S;\n validators?: TableValidators<S>;\n};\n\nexport type MetricsEvent = {\n duration: number;\n operation:\n | 'batch'\n | 'clear'\n | 'count'\n | 'delete'\n | 'deleteMany'\n | 'entries'\n | 'get'\n | 'getAll'\n | 'getMany'\n | 'getOrDefault'\n | 'has'\n | 'isEmpty'\n | 'keys'\n | 'put'\n | 'putAll'\n | 'query'\n | 'queryDelete'\n | 'update'\n | 'upsert';\n table: string;\n};\n\nexport type DebugStats = { expiredCount: number; recordCount: number };\nexport type DebugInfo<S extends AnySchema> = { tables: Array<{ name: keyof S & string } & DebugStats> };\n\n/** Methods available within an IndexedDB transaction. */\nexport type TransactionContext<S extends AnySchema, K extends keyof S & string = keyof S & string> = {\n clear<T extends K>(table: T): Promise<void>;\n count<T extends K>(table: T): Promise<number>;\n delete<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n deleteMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<number>;\n entries<T extends K>(table: T): Promise<Array<[KeyOf<S, T>, RecordOf<S, T>]>>;\n get<T extends K>(table: T, key: KeyOf<S, T>): Promise<RecordOf<S, T> | undefined>;\n getAll<T extends K>(table: T): Promise<RecordOf<S, T>[]>;\n getMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<Array<RecordOf<S, T> | undefined>>;\n getOrDefault<T extends K>(\n table: T,\n key: KeyOf<S, T>,\n defaultFn: () => RecordOf<S, T>,\n ttl?: TtlMs,\n ): Promise<RecordOf<S, T>>;\n has<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n isEmpty<T extends K>(table: T): Promise<boolean>;\n keys<T extends K>(table: T, filter?: (record: RecordOf<S, T>) => boolean): Promise<KeyOf<S, T>[]>;\n put<T extends K>(table: T, value: RecordOf<S, T>, ttl?: TtlMs): Promise<void>;\n putAll<T extends K>(table: T, values: RecordOf<S, T>[], ttl?: TtlMs): Promise<void>;\n query<T extends K>(table: T): QueryBuilder<RecordOf<S, T>>;\n update<T extends K>(\n table: T,\n key: KeyOf<S, T>,\n changes: Partial<RecordOf<S, T>>,\n ttl?: TtlMs,\n ): Promise<RecordOf<S, T> | undefined>;\n upsert<T extends K>(\n table: T,\n key: KeyOf<S, T>,\n fn: (existing: RecordOf<S, T> | undefined) => RecordOf<S, T>,\n ttl?: TtlMs,\n ): Promise<RecordOf<S, T>>;\n};\n\n/** Portable API returned by memory and Web Storage factories. */\nexport interface VaultStore<S extends AnySchema> {\n clear<K extends keyof S & string>(table: K): Promise<void>;\n count<K extends keyof S & string>(table: K): Promise<number>;\n delete<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;\n entries<K extends keyof S & string>(table: K): Promise<Array<[KeyOf<S, K>, RecordOf<S, K>]>>;\n get<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<RecordOf<S, K> | undefined>;\n getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;\n getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;\n getOrDefault<K extends keyof S & string>(\n table: K,\n key: KeyOf<S, K>,\n defaultFn: () => RecordOf<S, K>,\n ttl?: TtlMs,\n ): Promise<RecordOf<S, K>>;\n has<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;\n keys<K extends keyof S & string>(table: K, filter?: (record: RecordOf<S, K>) => boolean): Promise<KeyOf<S, K>[]>;\n put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: TtlMs): Promise<void>;\n putAll<K extends keyof S & string>(table: K, values: RecordOf<S, K>[], ttl?: TtlMs): Promise<void>;\n query<K extends keyof S & string>(table: K): QueryBuilder<RecordOf<S, K>>;\n update<K extends keyof S & string>(\n table: K,\n key: KeyOf<S, K>,\n changes: Partial<RecordOf<S, K>>,\n ttl?: TtlMs,\n ): Promise<RecordOf<S, K> | undefined>;\n upsert<K extends keyof S & string>(\n table: K,\n key: KeyOf<S, K>,\n fn: (existing: RecordOf<S, K> | undefined) => RecordOf<S, K>,\n ttl?: TtlMs,\n ): Promise<RecordOf<S, K>>;\n pruneExpired(): Promise<Record<keyof S & string, number>>;\n debug(): Promise<DebugInfo<S>>;\n observe<K extends keyof S & string>(\n table: K,\n listener: Observer<RecordOf<S, K>>,\n options?: { immediate?: boolean; signal?: AbortSignal },\n ): Unsubscribe;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.asyncDispose](): Promise<void>;\n}\n\n/** Atomic, scoped transactions supplied by storage engines that support them. */\nexport interface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n}\n\n/** Lazy record iteration supplied by storage engines that support it. */\nexport interface IterableVaultStore<S extends AnySchema> extends VaultStore<S> {\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n\n/** IndexedDB-only guarantees: cursor iteration and atomic, scoped transactions. */\nexport interface IndexedDbVaultStore<S extends AnySchema> extends TransactionalVaultStore<S>, IterableVaultStore<S> {}\n"],"mappings":"uDAiCA,SAAgB,EACd,EACsB,CACtB,SAAS,EAAY,EAAkD,CACrE,MAAO,CACL,GAAG,EACH,MAAoC,GAAmC,CACrE,IAAM,EAAW,EAAM,SAAW,CAAC,EAEnC,GAAI,EAAQ,SAAS,CAAK,EACxB,MAAM,IAAI,EAAA,WAAW,gBAAgB,EAAM,wBAAwB,EAGrE,OAAO,EAAY,CAAE,GAAG,EAAO,QAAS,CAAC,GAAG,EAAS,CAAK,CAAE,CAAC,CAC/D,EACA,IAAM,IACJ,EAAA,YAAY,EAAI,WAAW,EAEpB,EAAY,CAAE,GAAG,EAAO,WAAY,CAAG,CAAC,EAEnD,CACF,CAEA,OAAO,EAAY,CAAE,KAAI,CAAmC,CAC9D"}
1
+ {"version":3,"file":"types.cjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { VaultError } from './errors';\nimport type { QueryBuilder } from './query';\nimport { assertPositiveFinite } from './ttl';\n\n/** Portable primary-key values. Vault preserves their type and encodes them distinctly at rest. */\nexport type VaultKey = number | string;\n\n/** A typed table definition whose primary-key field must hold a portable Vault key. */\nexport type SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> = T[Key] extends VaultKey\n ? {\n defaultTtl?: number;\n /** IndexedDB creates these as `value.<field>` indexes; other stores filter in memory. */\n indexes?: readonly (keyof T & string)[];\n key: Key;\n }\n : never;\n\nexport type AnySchema = Record<string, { defaultTtl?: number; indexes?: readonly string[]; key: string }>;\n\nexport type RecordOf<S extends AnySchema, K extends keyof S> =\n S[K] extends SchemaEntry<infer R, infer _Key> ? R : never;\nexport type KeyOf<S extends AnySchema, K extends keyof S> = Extract<\n S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never,\n VaultKey\n>;\n\nexport type VaultLogger = {\n error(message: string, context?: Error | Record<string, unknown>): void;\n};\n\nexport type RecordValidator<T> = {\n parse(value: unknown): T;\n};\n\nexport type TableValidators<S extends AnySchema> = {\n [K in keyof S]?: RecordValidator<RecordOf<S, K>>;\n};\n\nexport type Observer<T> = (records: T[]) => void;\nexport type Unsubscribe = () => void;\n\n/** Shared factory options. Values always use Vault's fixed `{ value, expiresAt? }` envelope. */\nexport type BaseAdapterOptions<S extends AnySchema> = {\n logger?: VaultLogger;\n onMetrics?: (event: MetricsEvent) => void;\n schema: S;\n validators?: TableValidators<S>;\n};\n\nexport type MetricsEvent = {\n duration: number;\n operation:\n | 'batch'\n | 'clear'\n | 'count'\n | 'delete'\n | 'deleteMany'\n | 'entries'\n | 'get'\n | 'getAll'\n | 'getMany'\n | 'getOrDefault'\n | 'has'\n | 'isEmpty'\n | 'keys'\n | 'put'\n | 'putAll'\n | 'query'\n | 'queryDelete'\n | 'update'\n | 'upsert';\n table: string;\n};\n\nexport type DebugStats = { expiredCount: number; recordCount: number };\nexport type DebugInfo<S extends AnySchema> = { tables: Array<{ name: keyof S & string } & DebugStats> };\n\n/** Methods available within an IndexedDB or SQLite transaction. */\nexport type TransactionContext<S extends AnySchema, K extends keyof S & string = keyof S & string> = {\n clear<T extends K>(table: T): Promise<void>;\n count<T extends K>(table: T): Promise<number>;\n delete<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n deleteMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<number>;\n entries<T extends K>(table: T): Promise<Array<[KeyOf<S, T>, RecordOf<S, T>]>>;\n get<T extends K>(table: T, key: KeyOf<S, T>): Promise<RecordOf<S, T> | undefined>;\n getAll<T extends K>(table: T): Promise<RecordOf<S, T>[]>;\n getMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<Array<RecordOf<S, T> | undefined>>;\n getOrDefault<T extends K>(\n table: T,\n key: KeyOf<S, T>,\n defaultFn: () => RecordOf<S, T>,\n ttl?: number,\n ): Promise<RecordOf<S, T>>;\n has<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n isEmpty<T extends K>(table: T): Promise<boolean>;\n keys<T extends K>(table: T, filter?: (record: RecordOf<S, T>) => boolean): Promise<KeyOf<S, T>[]>;\n put<T extends K>(table: T, value: RecordOf<S, T>, ttl?: number): Promise<void>;\n putAll<T extends K>(table: T, values: RecordOf<S, T>[], ttl?: number): Promise<void>;\n query<T extends K>(table: T): QueryBuilder<RecordOf<S, T>>;\n update<T extends K>(\n table: T,\n key: KeyOf<S, T>,\n changes: Partial<RecordOf<S, T>>,\n ttl?: number,\n ): Promise<RecordOf<S, T> | undefined>;\n upsert<T extends K>(\n table: T,\n key: KeyOf<S, T>,\n fn: (existing: RecordOf<S, T> | undefined) => RecordOf<S, T>,\n ttl?: number,\n ): Promise<RecordOf<S, T>>;\n};\n\n/** Portable API returned by memory and Web Storage factories. */\nexport interface VaultStore<S extends AnySchema> {\n clear<K extends keyof S & string>(table: K): Promise<void>;\n count<K extends keyof S & string>(table: K): Promise<number>;\n debug(): Promise<DebugInfo<S>>;\n delete<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;\n readonly disposalSignal: AbortSignal;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n entries<K extends keyof S & string>(table: K): Promise<Array<[KeyOf<S, K>, RecordOf<S, K>]>>;\n get<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<RecordOf<S, K> | undefined>;\n getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;\n getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;\n getOrDefault<K extends keyof S & string>(\n table: K,\n key: KeyOf<S, K>,\n defaultFn: () => RecordOf<S, K>,\n ttl?: number,\n ): Promise<RecordOf<S, K>>;\n has<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;\n keys<K extends keyof S & string>(table: K, filter?: (record: RecordOf<S, K>) => boolean): Promise<KeyOf<S, K>[]>;\n observe<K extends keyof S & string>(\n table: K,\n listener: Observer<RecordOf<S, K>>,\n options?: { immediate?: boolean; signal?: AbortSignal },\n ): Unsubscribe;\n pruneExpired(): Promise<Record<keyof S & string, number>>;\n put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: number): Promise<void>;\n putAll<K extends keyof S & string>(table: K, values: RecordOf<S, K>[], ttl?: number): Promise<void>;\n query<K extends keyof S & string>(table: K): QueryBuilder<RecordOf<S, K>>;\n update<K extends keyof S & string>(\n table: K,\n key: KeyOf<S, K>,\n changes: Partial<RecordOf<S, K>>,\n ttl?: number,\n ): Promise<RecordOf<S, K> | undefined>;\n upsert<K extends keyof S & string>(\n table: K,\n key: KeyOf<S, K>,\n fn: (existing: RecordOf<S, K> | undefined) => RecordOf<S, K>,\n ttl?: number,\n ): Promise<RecordOf<S, K>>;\n [Symbol.asyncDispose](): Promise<void>;\n}\n\n/** Atomic, scoped transactions supplied by storage engines that support them. */\nexport interface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n}\n\n/** Lazy record iteration supplied by storage engines that support it. */\nexport interface IterableVaultStore<S extends AnySchema> extends VaultStore<S> {\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n\n/** IndexedDB-only guarantees: cursor iteration and atomic, scoped transactions. */\nexport interface IndexedDbVaultStore<S extends AnySchema> extends TransactionalVaultStore<S>, IterableVaultStore<S> {}\n\n/** Define a typed table whose primary-key field holds a portable Vault key. */\nexport function table<T extends object, Key extends keyof T & string = keyof T & string>(\n key: Key & (T[Key] extends VaultKey ? unknown : never),\n options: { defaultTtl?: number; indexes?: readonly (keyof T & string)[] } = {},\n): SchemaEntry<T, Key> {\n const { defaultTtl, indexes } = options;\n\n if (defaultTtl !== undefined) assertPositiveFinite(defaultTtl, 'table: defaultTtl');\n\n if (indexes) {\n const seen = new Set<string>();\n\n for (const field of indexes) {\n if (seen.has(field)) {\n throw new VaultError(`table: index \"${field}\" is already registered`);\n }\n\n seen.add(field);\n }\n }\n\n return { defaultTtl, indexes, key } as unknown as SchemaEntry<T, Key>;\n}\n"],"mappings":"uDAmLA,SAAgB,EACd,EACA,EAA4E,CAAC,EACxD,CACrB,GAAM,CAAE,aAAY,WAAY,EAIhC,GAFI,IAAe,IAAA,IAAW,EAAA,qBAAqB,EAAY,mBAAmB,EAE9E,EAAS,CACX,IAAM,EAAO,IAAI,IAEjB,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,EAAK,IAAI,CAAK,EAChB,MAAM,IAAI,EAAA,WAAW,iBAAiB,EAAM,wBAAwB,EAGtE,EAAK,IAAI,CAAK,CAChB,CACF,CAEA,MAAO,CAAE,aAAY,UAAS,KAAI,CACpC"}