@vielzeug/vault 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +74 -0
  2. package/dist/_dev.cjs +2 -0
  3. package/dist/_dev.cjs.map +1 -0
  4. package/dist/_dev.d.ts +2 -0
  5. package/dist/_dev.d.ts.map +1 -0
  6. package/dist/_dev.js +12 -0
  7. package/dist/_dev.js.map +1 -0
  8. package/dist/adapter-core.cjs +2 -0
  9. package/dist/adapter-core.cjs.map +1 -0
  10. package/dist/adapter-core.d.ts +8 -0
  11. package/dist/adapter-core.d.ts.map +1 -0
  12. package/dist/adapter-core.js +269 -0
  13. package/dist/adapter-core.js.map +1 -0
  14. package/dist/adapters/indexeddb.cjs +2 -0
  15. package/dist/adapters/indexeddb.cjs.map +1 -0
  16. package/dist/adapters/indexeddb.d.ts +13 -0
  17. package/dist/adapters/indexeddb.d.ts.map +1 -0
  18. package/dist/adapters/indexeddb.js +336 -0
  19. package/dist/adapters/indexeddb.js.map +1 -0
  20. package/dist/adapters/memory.cjs +2 -0
  21. package/dist/adapters/memory.cjs.map +1 -0
  22. package/dist/adapters/memory.d.ts +14 -0
  23. package/dist/adapters/memory.d.ts.map +1 -0
  24. package/dist/adapters/memory.js +200 -0
  25. package/dist/adapters/memory.js.map +1 -0
  26. package/dist/adapters/webstorage.cjs +2 -0
  27. package/dist/adapters/webstorage.cjs.map +1 -0
  28. package/dist/adapters/webstorage.d.ts +14 -0
  29. package/dist/adapters/webstorage.d.ts.map +1 -0
  30. package/dist/adapters/webstorage.js +182 -0
  31. package/dist/adapters/webstorage.js.map +1 -0
  32. package/dist/errors.cjs +2 -0
  33. package/dist/errors.cjs.map +1 -0
  34. package/dist/errors.d.ts +22 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +17 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/index.cjs +1 -0
  39. package/dist/index.d.ts +17 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +11 -0
  42. package/dist/internal.cjs +2 -0
  43. package/dist/internal.cjs.map +1 -0
  44. package/dist/internal.d.ts +50 -0
  45. package/dist/internal.d.ts.map +1 -0
  46. package/dist/internal.js +125 -0
  47. package/dist/internal.js.map +1 -0
  48. package/dist/migration.cjs +2 -0
  49. package/dist/migration.cjs.map +1 -0
  50. package/dist/migration.d.ts +37 -0
  51. package/dist/migration.d.ts.map +1 -0
  52. package/dist/migration.js +27 -0
  53. package/dist/migration.js.map +1 -0
  54. package/dist/prune.cjs +2 -0
  55. package/dist/prune.cjs.map +1 -0
  56. package/dist/prune.d.ts +32 -0
  57. package/dist/prune.d.ts.map +1 -0
  58. package/dist/prune.js +20 -0
  59. package/dist/prune.js.map +1 -0
  60. package/dist/query.cjs +2 -0
  61. package/dist/query.cjs.map +1 -0
  62. package/dist/query.d.ts +113 -0
  63. package/dist/query.d.ts.map +1 -0
  64. package/dist/query.js +126 -0
  65. package/dist/query.js.map +1 -0
  66. package/dist/streaming.cjs +2 -0
  67. package/dist/streaming.cjs.map +1 -0
  68. package/dist/streaming.d.ts +22 -0
  69. package/dist/streaming.d.ts.map +1 -0
  70. package/dist/streaming.js +21 -0
  71. package/dist/streaming.js.map +1 -0
  72. package/dist/ttl.cjs +2 -0
  73. package/dist/ttl.cjs.map +1 -0
  74. package/dist/ttl.d.ts +74 -0
  75. package/dist/ttl.d.ts.map +1 -0
  76. package/dist/ttl.js +32 -0
  77. package/dist/ttl.js.map +1 -0
  78. package/dist/types.cjs +2 -0
  79. package/dist/types.cjs.map +1 -0
  80. package/dist/types.d.ts +409 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +27 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/vault.cjs +2 -0
  85. package/dist/vault.cjs.map +1 -0
  86. package/dist/vault.iife.js +2 -0
  87. package/dist/vault.iife.js.map +1 -0
  88. package/dist/vault.js +2 -0
  89. package/dist/vault.js.map +1 -0
  90. package/dist/versioned-codec.cjs +2 -0
  91. package/dist/versioned-codec.cjs.map +1 -0
  92. package/dist/versioned-codec.d.ts +44 -0
  93. package/dist/versioned-codec.d.ts.map +1 -0
  94. package/dist/versioned-codec.js +32 -0
  95. package/dist/versioned-codec.js.map +1 -0
  96. package/package.json +41 -0
package/dist/query.js ADDED
@@ -0,0 +1,126 @@
1
+ import { VaultError as e } from "./errors.js";
2
+ //#region src/query.ts
3
+ async function t(e, t) {
4
+ let n = await e.source();
5
+ for (let e of t) n = e.apply(n);
6
+ return n;
7
+ }
8
+ function n(t, n) {
9
+ if (!Number.isInteger(t) || t < 0) throw new e(`${n} must be a non-negative integer`);
10
+ return t;
11
+ }
12
+ function r(e, t) {
13
+ return {
14
+ deleteMany: e.deleteMany,
15
+ source: t
16
+ };
17
+ }
18
+ function i(a, o = []) {
19
+ let s = (e) => i(a, [...o, e]);
20
+ return {
21
+ between(e, t, n) {
22
+ let c = String(e);
23
+ if (o.length === 0) {
24
+ if (a.getRange && a.keyField === c) return i(r(a, () => a.getRange({
25
+ lower: t,
26
+ type: "between",
27
+ upper: n
28
+ })), o);
29
+ if (a.getIndexRange && a.indexedFields?.has(c)) return i(r(a, () => a.getIndexRange(c, {
30
+ lower: t,
31
+ type: "between",
32
+ upper: n
33
+ })), o);
34
+ }
35
+ return s({ apply: (r) => r.filter((r) => {
36
+ let i = r[e];
37
+ return i >= t && i <= n;
38
+ }) });
39
+ },
40
+ count() {
41
+ return t(a, o).then((e) => e.length);
42
+ },
43
+ async delete() {
44
+ if (!a.deleteMany) throw new e("query.delete is not available for this adapter context");
45
+ let n = await t(a, o);
46
+ return a.deleteMany(n);
47
+ },
48
+ equals(e, t) {
49
+ let n = String(e);
50
+ if (o.length === 0) {
51
+ if (a.getRange && a.keyField === n) return i(r(a, () => a.getRange({
52
+ type: "eq",
53
+ value: t
54
+ })));
55
+ if (a.getIndexRange && a.indexedFields?.has(n)) return i(r(a, () => a.getIndexRange(n, {
56
+ type: "eq",
57
+ value: t
58
+ })));
59
+ }
60
+ return i(a, [...o, { apply: (n) => n.filter((n) => n[e] === t) }]);
61
+ },
62
+ async exists() {
63
+ return o.length === 0 ? a.source().then((e) => e.length > 0) : t(a, o).then((e) => e.length > 0);
64
+ },
65
+ filter(e) {
66
+ return s({ apply: (t) => t.filter(e) });
67
+ },
68
+ first() {
69
+ return o.length === 0 ? a.source().then((e) => e[0]) : t(a, o).then((e) => e[0]);
70
+ },
71
+ limit(e) {
72
+ let t = n(e, "query.limit");
73
+ return s({
74
+ apply: (e) => e.slice(0, t),
75
+ isNonFilter: !0
76
+ });
77
+ },
78
+ offset(e) {
79
+ let t = n(e, "query.offset");
80
+ return s({
81
+ apply: (e) => e.slice(t),
82
+ isNonFilter: !0
83
+ });
84
+ },
85
+ orderBy(e, t = "asc") {
86
+ return s({
87
+ apply: (n) => {
88
+ let r = t === "asc" ? 1 : -1;
89
+ return [...n].sort((t, n) => {
90
+ let i = t[e], a = n[e];
91
+ return i === a ? 0 : i > a ? r : -r;
92
+ });
93
+ },
94
+ isNonFilter: !0
95
+ });
96
+ },
97
+ startsWith(e, t, { ignoreCase: n = !1 } = {}) {
98
+ let c = String(e);
99
+ if (!n && t.length > 0 && o.length === 0) {
100
+ if (a.getRange && a.keyField === c) return i(r(a, () => a.getRange({
101
+ prefix: t,
102
+ type: "starts"
103
+ })), o);
104
+ if (a.getIndexRange && a.indexedFields?.has(c)) return i(r(a, () => a.getIndexRange(c, {
105
+ prefix: t,
106
+ type: "starts"
107
+ })), o);
108
+ }
109
+ let l = n ? t.toLowerCase() : t;
110
+ return s({ apply: (t) => t.filter((t) => {
111
+ let r = t[e];
112
+ return typeof r == "string" ? (n ? r.toLowerCase() : r).startsWith(l) : !1;
113
+ }) });
114
+ },
115
+ toArray() {
116
+ return t(a, o);
117
+ },
118
+ totalCount() {
119
+ return t(a, o.filter((e) => !e.isNonFilter)).then((e) => e.length);
120
+ }
121
+ };
122
+ }
123
+ //#endregion
124
+ export { i as createQueryBuilder };
125
+
126
+ //# sourceMappingURL=query.js.map
@@ -0,0 +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 Record<string, unknown>> = {\n [K in keyof T]-?: Extract<NonNullable<T[K]>, number | string> extends never ? never : K;\n}[keyof T];\n\n/**\n * A primary-key range hint that can be pushed down to native storage backends (e.g. IndexedDB).\n * When `QueryContext.getRange` and `keyField` are present, a matching first filter op replaces\n * the full-table `source()` scan with a targeted range fetch.\n */\nexport type NativeRange =\n | { type: 'eq'; value: unknown }\n | { lower: unknown; type: 'between'; upper: unknown }\n | { prefix: string; type: 'starts' };\n\nexport type QueryContext<T extends Record<string, unknown>> = {\n deleteMany?: (records: T[]) => Promise<number>;\n /**\n * When present alongside `indexedFields`, replaces `source()` for secondary-index filter ops.\n * Only activated when the first op is `equals`, `between`, or `startsWith` on a field that\n * has an index registered in the schema. IndexedDB uses `IDBIndex.getAll(range)` under the hood.\n */\n getIndexRange?: (field: string, range: NativeRange) => Promise<T[]>;\n /**\n * When present alongside `keyField`, replaces `source()` for primary-key filter ops.\n * Only activated when the first op is an `equals`, `between`, or case-sensitive `startsWith`\n * on `keyField`. All remaining ops still run in-memory against the range result.\n */\n getRange?: (range: NativeRange) => Promise<T[]>;\n /** Fields that have secondary indexes — used to detect when a filter op can use `getIndexRange`. */\n indexedFields?: ReadonlySet<string>;\n /** Primary key field name — used to detect when a filter op can be pushed to `getRange`. */\n keyField?: string;\n source: () => Promise<T[]>;\n};\n\n/* -------------------- Public interfaces -------------------- */\n\n/**\n * Shared query methods. `T` is the base record type; `N` is the progressively-narrowed type\n * accumulated by `equals()` calls.\n */\ntype ChainedQuery<T extends Record<string, unknown>, N extends T, Self extends ChainedQuery<T, N, Self>> = {\n /**\n * Filter records where `field` is between `lower` and `upper` (inclusive).\n * Preserves any type narrowing already accumulated by prior `equals()` calls.\n */\n between<K extends ComparableFieldKeys<T>>(\n field: K,\n lower: Extract<NonNullable<T[K]>, number | string>,\n upper: Extract<NonNullable<T[K]>, number | string>,\n ): QueryBuilder<T, N>;\n /**\n * Returns the number of records matching all applied operations, including `limit` and `offset`.\n * To get the full filtered set size regardless of pagination (e.g. for \"page X of N\" UIs),\n * use `totalCount()` instead.\n */\n count(): Promise<number>;\n /**\n * Filter records where `field` exactly equals `value`.\n * The return type is narrowed to `QueryBuilder<T & Record<K, V>>` so that subsequent\n * `.toArray()`, `.first()`, etc. reflect the equality constraint in their result type.\n *\n * ```ts\n * // result: (User & { role: 'admin' })[]\n * const admins = await db.query('users').equals('role', 'admin').toArray();\n * ```\n */\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>>;\n /**\n * Returns `true` if at least one record matches all applied filter operations.\n * Equivalent to `(await query.first()) !== undefined` but makes the intent explicit.\n * Presentation-only ops (`limit`, `offset`, `orderBy`) are respected before checking.\n */\n exists(): Promise<boolean>;\n filter(fn: Predicate<N>): Self;\n first(): Promise<N | undefined>;\n limit(n: number): Self;\n offset(n: number): Self;\n orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): Self;\n /**\n * Filter records where `field` starts with `prefix`.\n * Preserves any type narrowing already accumulated by prior `equals()` calls.\n *\n * **Index push-down** is only active when `ignoreCase` is `false` (default) and this\n * is the first operation in the chain. With `ignoreCase: true`, a full-table scan is\n * always performed regardless of whether the field has an index.\n */\n startsWith<K extends keyof T>(field: K, prefix: string, options?: { ignoreCase?: boolean }): QueryBuilder<T, N>;\n toArray(): Promise<N[]>;\n /**\n * Returns the number of records matching the applied filter predicates.\n * Presentation-only ops (`limit`, `offset`, `orderBy`) are intentionally ignored — this\n * always counts the full filtered set, making paginated total-count queries possible\n * without a second query.\n */\n totalCount(): Promise<number>;\n};\n\n/** Extends the shared query API with `delete()`. Available on both `Adapter.query()` and inside `batch()` callbacks. */\nexport interface QueryBuilder<T extends Record<string, unknown>, N extends T = T> extends ChainedQuery<\n T,\n N,\n QueryBuilder<T, N>\n> {\n delete(): Promise<number>;\n}\n\n/* -------------------- Helpers -------------------- */\n\nasync function applyOps<T extends Record<string, unknown>>(\n ctx: QueryContext<T>,\n ops: readonly QueryOp<T>[],\n): Promise<T[]> {\n let data = await ctx.source();\n\n for (const op of ops) {\n data = op.apply(data);\n }\n\n return data;\n}\n\nfunction assertNonNegativeInteger(value: number, name: string): number {\n if (!Number.isInteger(value) || value < 0) {\n throw new VaultError(`${name} must be a non-negative integer`);\n }\n\n return value;\n}\n\n/* -------------------- Push-down helpers -------------------- */\n\n/** Build a new QueryContext with source replaced by a range/index fetch. Drops range hints since push-down is done. */\nfunction pushDownContext<T extends Record<string, unknown>>(\n ctx: QueryContext<T>,\n newSource: () => Promise<T[]>,\n): QueryContext<T> {\n return { deleteMany: ctx.deleteMany, source: newSource };\n}\n\n/* -------------------- Factory -------------------- */\n\nexport function createQueryBuilder<T extends Record<string, unknown>, N extends T = T>(\n ctx: QueryContext<T>,\n ops: readonly QueryOp<T>[] = [],\n): QueryBuilder<T, N> {\n const append = (op: QueryOp<T>): QueryBuilder<T, N> => createQueryBuilder<T, N>(ctx, [...ops, op]);\n\n return {\n between(field, lower, upper) {\n const fieldStr = String(field);\n\n if (ops.length === 0) {\n if (ctx.getRange && ctx.keyField === fieldStr) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getRange!({ lower, type: 'between', upper })),\n ops,\n );\n }\n\n if (ctx.getIndexRange && ctx.indexedFields?.has(fieldStr)) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getIndexRange!(fieldStr, { lower, type: 'between', upper })),\n ops,\n );\n }\n }\n\n return append({\n apply: (data) =>\n data.filter((r) => {\n const v = r[field] as number | string;\n\n return v >= lower && v <= upper;\n }),\n }) as unknown as QueryBuilder<T, N>;\n },\n count(): Promise<number> {\n return applyOps(ctx, ops).then((r) => r.length);\n },\n async delete(): Promise<number> {\n if (!ctx.deleteMany) {\n throw new VaultError('query.delete is not available for this adapter context');\n }\n\n const records = await applyOps(ctx, ops);\n\n return ctx.deleteMany(records);\n },\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>> {\n const fieldStr = String(field);\n\n if (ops.length === 0) {\n if (ctx.getRange && ctx.keyField === fieldStr) {\n return createQueryBuilder<T & Record<K, V>>(\n pushDownContext(\n ctx as unknown as QueryContext<T & Record<K, V>>,\n () => ctx.getRange!({ type: 'eq', value }) as Promise<(T & Record<K, V>)[]>,\n ),\n ) as QueryBuilder<T & Record<K, V>>;\n }\n\n if (ctx.getIndexRange && ctx.indexedFields?.has(fieldStr)) {\n return createQueryBuilder<T & Record<K, V>>(\n pushDownContext(\n ctx as unknown as QueryContext<T & Record<K, V>>,\n () => ctx.getIndexRange!(fieldStr, { type: 'eq', value }) as Promise<(T & Record<K, V>)[]>,\n ),\n ) as QueryBuilder<T & Record<K, V>>;\n }\n }\n\n return createQueryBuilder<T & Record<K, V>>(ctx as unknown as QueryContext<T & Record<K, V>>, [\n ...(ops as unknown as QueryOp<T & Record<K, V>>[]),\n { apply: (data) => data.filter((r) => r[field] === value) as (T & Record<K, V>)[] },\n ]);\n },\n async exists(): Promise<boolean> {\n if (ops.length === 0) return ctx.source().then((r) => r.length > 0);\n\n return applyOps(ctx, ops).then((r) => r.length > 0);\n },\n filter(fn) {\n return append({ apply: (data) => data.filter(fn as Predicate<T>) });\n },\n first(): Promise<N | undefined> {\n if (ops.length === 0) return ctx.source().then((r) => r[0] as N | undefined);\n\n return applyOps(ctx, ops).then((r) => r[0] as N | undefined);\n },\n limit(n) {\n const safeN = assertNonNegativeInteger(n, 'query.limit');\n\n return append({ apply: (data) => data.slice(0, safeN), isNonFilter: true });\n },\n offset(n) {\n const safeN = assertNonNegativeInteger(n, 'query.offset');\n\n return append({ apply: (data) => data.slice(safeN), isNonFilter: true });\n },\n orderBy(field, direction = 'asc') {\n return append({\n apply: (data) => {\n const sign = direction === 'asc' ? 1 : -1;\n\n return [...data].sort((a, b) => {\n const av = a[field] as number | string;\n const bv = b[field] as number | string;\n\n if (av === bv) return 0;\n\n return av > bv ? sign : -sign;\n });\n },\n isNonFilter: true,\n });\n },\n startsWith(field, prefix, { ignoreCase = false } = {}) {\n const fieldStr = String(field);\n\n if (!ignoreCase && prefix.length > 0 && ops.length === 0) {\n if (ctx.getRange && ctx.keyField === fieldStr) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getRange!({ prefix, type: 'starts' })),\n ops,\n );\n }\n\n if (ctx.getIndexRange && ctx.indexedFields?.has(fieldStr)) {\n return createQueryBuilder(\n pushDownContext(ctx, () => ctx.getIndexRange!(fieldStr, { prefix, type: 'starts' })),\n ops,\n );\n }\n }\n\n const needle = ignoreCase ? prefix.toLowerCase() : prefix;\n\n return append({\n apply: (data) =>\n data.filter((r) => {\n const v = r[field];\n\n if (typeof v !== 'string') return false;\n\n const haystack = ignoreCase ? v.toLowerCase() : v;\n\n return haystack.startsWith(needle);\n }),\n }) as unknown as QueryBuilder<T, N>;\n },\n toArray(): Promise<N[]> {\n return applyOps(ctx, ops) as Promise<N[]>;\n },\n totalCount(): Promise<number> {\n const filterOps = ops.filter((op) => !op.isNonFilter);\n\n return applyOps(ctx, filterOps).then((r) => r.length);\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ function e(e){let t=e[Symbol.asyncIterator]();return new ReadableStream({async cancel(){await t.return?.()},async pull(e){try{let{done:n,value:r}=await t.next();n?e.close():e.enqueue(r)}catch(t){e.error(t)}}})}exports.toReadableStream=e;
2
+ //# sourceMappingURL=streaming.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.cjs","names":[],"sources":["../src/streaming.ts"],"sourcesContent":["/**\n * Converts an `AsyncIterable<T>` into a Web Standard `ReadableStream<T>`.\n *\n * Use this with `db.watch()` when you need a `ReadableStream` instead of an async iterator —\n * for example, to `pipeTo()` a `WritableStream` or `pipeThrough()` a `TransformStream`.\n *\n * ```ts\n * const stream = toReadableStream(db.watch('users'));\n * await stream.pipeTo(new WritableStream({ write: (users) => render(users) }));\n * ```\n *\n * The stream closes when the iterable is exhausted or when the stream is cancelled.\n * Pass an `AbortSignal` to `db.watch()` to cancel from outside:\n *\n * ```ts\n * const controller = new AbortController();\n * const stream = toReadableStream(db.watch('users', { signal: controller.signal }));\n * controller.abort(); // closes the stream\n * ```\n */\nexport function toReadableStream<T>(iterable: AsyncIterable<T>): ReadableStream<T> {\n const iter = iterable[Symbol.asyncIterator]();\n\n return new ReadableStream<T>({\n async cancel() {\n await iter.return?.();\n },\n async pull(controller) {\n try {\n const { done, value } = await iter.next();\n\n if (done) {\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (err) {\n controller.error(err);\n }\n },\n });\n}\n"],"mappings":"AAoBA,SAAgB,EAAoB,EAA+C,CACjF,IAAM,EAAO,EAAS,OAAO,cAAc,CAAC,EAE5C,OAAO,IAAI,eAAkB,CAC3B,MAAM,QAAS,CACb,MAAM,EAAK,SAAS,CACtB,EACA,MAAM,KAAK,EAAY,CACrB,GAAI,CACF,GAAM,CAAE,OAAM,SAAU,MAAM,EAAK,KAAK,EAEpC,EACF,EAAW,MAAM,EAEjB,EAAW,QAAQ,CAAK,CAE5B,OAAS,EAAK,CACZ,EAAW,MAAM,CAAG,CACtB,CACF,CACF,CAAC,CACH"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Converts an `AsyncIterable<T>` into a Web Standard `ReadableStream<T>`.
3
+ *
4
+ * Use this with `db.watch()` when you need a `ReadableStream` instead of an async iterator —
5
+ * for example, to `pipeTo()` a `WritableStream` or `pipeThrough()` a `TransformStream`.
6
+ *
7
+ * ```ts
8
+ * const stream = toReadableStream(db.watch('users'));
9
+ * await stream.pipeTo(new WritableStream({ write: (users) => render(users) }));
10
+ * ```
11
+ *
12
+ * The stream closes when the iterable is exhausted or when the stream is cancelled.
13
+ * Pass an `AbortSignal` to `db.watch()` to cancel from outside:
14
+ *
15
+ * ```ts
16
+ * const controller = new AbortController();
17
+ * const stream = toReadableStream(db.watch('users', { signal: controller.signal }));
18
+ * controller.abort(); // closes the stream
19
+ * ```
20
+ */
21
+ export declare function toReadableStream<T>(iterable: AsyncIterable<T>): ReadableStream<T>;
22
+ //# sourceMappingURL=streaming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAqBjF"}
@@ -0,0 +1,21 @@
1
+ //#region src/streaming.ts
2
+ function e(e) {
3
+ let t = e[Symbol.asyncIterator]();
4
+ return new ReadableStream({
5
+ async cancel() {
6
+ await t.return?.();
7
+ },
8
+ async pull(e) {
9
+ try {
10
+ let { done: n, value: r } = await t.next();
11
+ n ? e.close() : e.enqueue(r);
12
+ } catch (t) {
13
+ e.error(t);
14
+ }
15
+ }
16
+ });
17
+ }
18
+ //#endregion
19
+ export { e as toReadableStream };
20
+
21
+ //# sourceMappingURL=streaming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming.js","names":[],"sources":["../src/streaming.ts"],"sourcesContent":["/**\n * Converts an `AsyncIterable<T>` into a Web Standard `ReadableStream<T>`.\n *\n * Use this with `db.watch()` when you need a `ReadableStream` instead of an async iterator —\n * for example, to `pipeTo()` a `WritableStream` or `pipeThrough()` a `TransformStream`.\n *\n * ```ts\n * const stream = toReadableStream(db.watch('users'));\n * await stream.pipeTo(new WritableStream({ write: (users) => render(users) }));\n * ```\n *\n * The stream closes when the iterable is exhausted or when the stream is cancelled.\n * Pass an `AbortSignal` to `db.watch()` to cancel from outside:\n *\n * ```ts\n * const controller = new AbortController();\n * const stream = toReadableStream(db.watch('users', { signal: controller.signal }));\n * controller.abort(); // closes the stream\n * ```\n */\nexport function toReadableStream<T>(iterable: AsyncIterable<T>): ReadableStream<T> {\n const iter = iterable[Symbol.asyncIterator]();\n\n return new ReadableStream<T>({\n async cancel() {\n await iter.return?.();\n },\n async pull(controller) {\n try {\n const { done, value } = await iter.next();\n\n if (done) {\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (err) {\n controller.error(err);\n }\n },\n });\n}\n"],"mappings":";AAoBA,SAAgB,EAAoB,GAA+C;CACjF,IAAM,IAAO,EAAS,OAAO,cAAc,CAAC;CAE5C,OAAO,IAAI,eAAkB;EAC3B,MAAM,SAAS;GACb,MAAM,EAAK,SAAS;EACtB;EACA,MAAM,KAAK,GAAY;GACrB,IAAI;IACF,IAAM,EAAE,SAAM,aAAU,MAAM,EAAK,KAAK;IAExC,AAAI,IACF,EAAW,MAAM,IAEjB,EAAW,QAAQ,CAAK;GAE5B,SAAS,GAAK;IACZ,EAAW,MAAM,CAAG;GACtB;EACF;CACF,CAAC;AACH"}
package/dist/ttl.cjs ADDED
@@ -0,0 +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}var a={decode:i,encode:(e,t)=>t===void 0?{value:e}:{expiresAt:t,value:e}};exports.assertTtlMs=r,exports.defaultCodec=a,exports.isExpired=n,exports.parseStored=i,exports.ttl=t;
2
+ //# sourceMappingURL=ttl.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ttl.cjs","names":[],"sources":["../src/ttl.ts"],"sourcesContent":["import { VaultError } from './errors';\n\n/* -------------------- TtlMs (owns here to avoid circular deps with types.ts) -------------------- */\n\ndeclare const ttlMsBrand: unique symbol;\n/** A duration in milliseconds, produced by the `ttl.*` helpers. Branded to prevent accidental raw numbers. */\nexport type TtlMs = number & { readonly [ttlMsBrand]: never };\n\n/* -------------------- Codec -------------------- */\n\n/**\n * Pluggable serialization contract. Implement to change how vault stores values at rest\n * (e.g. compressed JSON, msgpack, encrypted envelopes).\n *\n * The codec translates between the vault-internal TTL envelope `{ value, expiresAt? }` and\n * whatever format is actually written to the underlying storage backend.\n *\n * The default codec stores `{ value: T, expiresAt?: number }` verbatim — identical to the\n * previous behaviour.\n *\n * ```ts\n * const compactCodec: VaultCodec = {\n * encode: (value, expiresAt) => expiresAt ? { v: value, e: expiresAt } : { v: value },\n * decode: (raw) => {\n * if (typeof raw !== 'object' || raw === null || !('v' in raw)) return undefined;\n * const { v, e } = raw as { v: unknown; e?: unknown };\n * return { value: v as any, expiresAt: typeof e === 'number' ? e : undefined };\n * },\n * };\n * ```\n */\nexport type VaultCodec = {\n /**\n * Parse a raw stored value into the internal TTL envelope.\n * Return `undefined` for any unrecognized or corrupt data.\n */\n decode<T>(raw: unknown): { expiresAt?: number; value: T } | undefined;\n /**\n * Encode a value (and optional absolute expiry timestamp) into the storage format.\n * `expiresAt` is an epoch-ms timestamp — use `Date.now() + ttlMs` to compute it.\n */\n encode<T>(value: T, expiresAt?: number): unknown;\n};\n\n/* -------------------- Duration helpers -------------------- */\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/* -------------------- Storage record -------------------- */\n\n/** Internal envelope used by all storage backends. `expiresAt` is an epoch timestamp in ms. */\nexport type StoredRecord<T> = {\n expiresAt?: number;\n value: T;\n};\n\n/** Returns true when an epoch-ms expiry timestamp has passed. Safe to call with `undefined`. */\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 if (ttlMs === undefined) return { value };\n\n return { expiresAt: Date.now() + ttlMs, value };\n}\n\nexport function unwrapStored<T>(raw: StoredRecord<T>): T | undefined {\n if (isExpired(raw.expiresAt)) return undefined;\n\n return 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\n/**\n * Parse a raw stored value and determine its TTL status in one call.\n * Combines `parseStored` + `unwrapStored` with an explicit `expired` flag so callers\n * can distinguish between \"key not found\" (`found: false`) and \"key is TTL-expired\"\n * (`found: true, expired: true, value: undefined`).\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\n/** Default codec — stores `{ value, expiresAt? }` verbatim. Zero overhead vs. previous behaviour. */\nexport const defaultCodec: VaultCodec = {\n decode: parseStored as VaultCodec['decode'],\n encode: <T>(value: T, expiresAt?: number): unknown => (expiresAt !== undefined ? { expiresAt, value } : { value }),\n};\n"],"mappings":"gCA8CA,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,EAWA,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,CAcA,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,CAmBA,IAAa,EAA2B,CACtC,OAAQ,EACR,QAAY,EAAU,IAAiC,IAAc,IAAA,GAAmC,CAAE,OAAM,EAA/B,CAAE,YAAW,OAAM,CACtG"}
package/dist/ttl.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ declare const ttlMsBrand: unique symbol;
2
+ /** A duration in milliseconds, produced by the `ttl.*` helpers. Branded to prevent accidental raw numbers. */
3
+ export type TtlMs = number & {
4
+ readonly [ttlMsBrand]: never;
5
+ };
6
+ /**
7
+ * Pluggable serialization contract. Implement to change how vault stores values at rest
8
+ * (e.g. compressed JSON, msgpack, encrypted envelopes).
9
+ *
10
+ * The codec translates between the vault-internal TTL envelope `{ value, expiresAt? }` and
11
+ * whatever format is actually written to the underlying storage backend.
12
+ *
13
+ * The default codec stores `{ value: T, expiresAt?: number }` verbatim — identical to the
14
+ * previous behaviour.
15
+ *
16
+ * ```ts
17
+ * const compactCodec: VaultCodec = {
18
+ * encode: (value, expiresAt) => expiresAt ? { v: value, e: expiresAt } : { v: value },
19
+ * decode: (raw) => {
20
+ * if (typeof raw !== 'object' || raw === null || !('v' in raw)) return undefined;
21
+ * const { v, e } = raw as { v: unknown; e?: unknown };
22
+ * return { value: v as any, expiresAt: typeof e === 'number' ? e : undefined };
23
+ * },
24
+ * };
25
+ * ```
26
+ */
27
+ export type VaultCodec = {
28
+ /**
29
+ * Parse a raw stored value into the internal TTL envelope.
30
+ * Return `undefined` for any unrecognized or corrupt data.
31
+ */
32
+ decode<T>(raw: unknown): {
33
+ expiresAt?: number;
34
+ value: T;
35
+ } | undefined;
36
+ /**
37
+ * Encode a value (and optional absolute expiry timestamp) into the storage format.
38
+ * `expiresAt` is an epoch-ms timestamp — use `Date.now() + ttlMs` to compute it.
39
+ */
40
+ encode<T>(value: T, expiresAt?: number): unknown;
41
+ };
42
+ export declare const ttl: {
43
+ readonly days: (n: number) => TtlMs;
44
+ readonly hours: (n: number) => TtlMs;
45
+ readonly minutes: (n: number) => TtlMs;
46
+ readonly ms: (n: number) => TtlMs;
47
+ readonly seconds: (n: number) => TtlMs;
48
+ };
49
+ /** Internal envelope used by all storage backends. `expiresAt` is an epoch timestamp in ms. */
50
+ export type StoredRecord<T> = {
51
+ expiresAt?: number;
52
+ value: T;
53
+ };
54
+ /** Returns true when an epoch-ms expiry timestamp has passed. Safe to call with `undefined`. */
55
+ export declare function isExpired(expiresAt: number | undefined): boolean;
56
+ export declare function assertTtlMs(ttlMs: number, source: string): TtlMs;
57
+ export declare function wrapStored<T>(value: T, ttlMs?: TtlMs): StoredRecord<T>;
58
+ export declare function unwrapStored<T>(raw: StoredRecord<T>): T | undefined;
59
+ export declare function parseStored<T>(raw: unknown): StoredRecord<T> | undefined;
60
+ /**
61
+ * Parse a raw stored value and determine its TTL status in one call.
62
+ * Combines `parseStored` + `unwrapStored` with an explicit `expired` flag so callers
63
+ * can distinguish between "key not found" (`found: false`) and "key is TTL-expired"
64
+ * (`found: true, expired: true, value: undefined`).
65
+ */
66
+ export declare function readWithTtl<T>(raw: unknown): {
67
+ expired: boolean;
68
+ found: boolean;
69
+ value: T | undefined;
70
+ };
71
+ /** Default codec — stores `{ value, expiresAt? }` verbatim. Zero overhead vs. previous behaviour. */
72
+ export declare const defaultCodec: VaultCodec;
73
+ export {};
74
+ //# sourceMappingURL=ttl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ttl.d.ts","sourceRoot":"","sources":["../src/ttl.ts"],"names":[],"mappings":"AAIA,OAAO,CAAC,MAAM,UAAU,EAAE,OAAO,MAAM,CAAC;AACxC,8GAA8G;AAC9G,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAI9D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB;;;OAGG;IACH,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IACtE;;;OAGG;IACH,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAClD,CAAC;AAIF,eAAO,MAAM,GAAG;uBACJ,MAAM;wBACL,MAAM;0BACJ,MAAM;qBACX,MAAM;0BACD,MAAM;CACX,CAAC;AAIX,+FAA+F;AAC/F,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,CAAC,CAAC;CACV,CAAC;AAEF,gGAAgG;AAChG,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,CAItE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAInE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAUxE;AAED;;;;;GAKG;AACH,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;AAED,qGAAqG;AACrG,eAAO,MAAM,YAAY,EAAE,UAG1B,CAAC"}
package/dist/ttl.js ADDED
@@ -0,0 +1,32 @@
1
+ import { VaultError as e } from "./errors.js";
2
+ //#region src/ttl.ts
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)"),
7
+ ms: (e) => r(e, "ttl.ms"),
8
+ seconds: (e) => r(r(e, "ttl.seconds") * 1e3, "ttl.seconds (result)")
9
+ };
10
+ function n(e) {
11
+ return e !== void 0 && Date.now() >= e;
12
+ }
13
+ function r(t, n) {
14
+ if (!Number.isFinite(t) || t <= 0) throw new e(`${n} expected a finite positive number, received ${String(t)}`);
15
+ return t;
16
+ }
17
+ function i(e) {
18
+ if (typeof e != "object" || !e || !("value" in e)) return;
19
+ let t = e;
20
+ if (!(t.expiresAt !== void 0 && (typeof t.expiresAt != "number" || !Number.isFinite(t.expiresAt)))) return t;
21
+ }
22
+ var a = {
23
+ decode: i,
24
+ encode: (e, t) => t === void 0 ? { value: e } : {
25
+ expiresAt: t,
26
+ value: e
27
+ }
28
+ };
29
+ //#endregion
30
+ export { r as assertTtlMs, a as defaultCodec, n as isExpired, i as parseStored, t as ttl };
31
+
32
+ //# sourceMappingURL=ttl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ttl.js","names":[],"sources":["../src/ttl.ts"],"sourcesContent":["import { VaultError } from './errors';\n\n/* -------------------- TtlMs (owns here to avoid circular deps with types.ts) -------------------- */\n\ndeclare const ttlMsBrand: unique symbol;\n/** A duration in milliseconds, produced by the `ttl.*` helpers. Branded to prevent accidental raw numbers. */\nexport type TtlMs = number & { readonly [ttlMsBrand]: never };\n\n/* -------------------- Codec -------------------- */\n\n/**\n * Pluggable serialization contract. Implement to change how vault stores values at rest\n * (e.g. compressed JSON, msgpack, encrypted envelopes).\n *\n * The codec translates between the vault-internal TTL envelope `{ value, expiresAt? }` and\n * whatever format is actually written to the underlying storage backend.\n *\n * The default codec stores `{ value: T, expiresAt?: number }` verbatim — identical to the\n * previous behaviour.\n *\n * ```ts\n * const compactCodec: VaultCodec = {\n * encode: (value, expiresAt) => expiresAt ? { v: value, e: expiresAt } : { v: value },\n * decode: (raw) => {\n * if (typeof raw !== 'object' || raw === null || !('v' in raw)) return undefined;\n * const { v, e } = raw as { v: unknown; e?: unknown };\n * return { value: v as any, expiresAt: typeof e === 'number' ? e : undefined };\n * },\n * };\n * ```\n */\nexport type VaultCodec = {\n /**\n * Parse a raw stored value into the internal TTL envelope.\n * Return `undefined` for any unrecognized or corrupt data.\n */\n decode<T>(raw: unknown): { expiresAt?: number; value: T } | undefined;\n /**\n * Encode a value (and optional absolute expiry timestamp) into the storage format.\n * `expiresAt` is an epoch-ms timestamp — use `Date.now() + ttlMs` to compute it.\n */\n encode<T>(value: T, expiresAt?: number): unknown;\n};\n\n/* -------------------- Duration helpers -------------------- */\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/* -------------------- Storage record -------------------- */\n\n/** Internal envelope used by all storage backends. `expiresAt` is an epoch timestamp in ms. */\nexport type StoredRecord<T> = {\n expiresAt?: number;\n value: T;\n};\n\n/** Returns true when an epoch-ms expiry timestamp has passed. Safe to call with `undefined`. */\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 if (ttlMs === undefined) return { value };\n\n return { expiresAt: Date.now() + ttlMs, value };\n}\n\nexport function unwrapStored<T>(raw: StoredRecord<T>): T | undefined {\n if (isExpired(raw.expiresAt)) return undefined;\n\n return 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\n/**\n * Parse a raw stored value and determine its TTL status in one call.\n * Combines `parseStored` + `unwrapStored` with an explicit `expired` flag so callers\n * can distinguish between \"key not found\" (`found: false`) and \"key is TTL-expired\"\n * (`found: true, expired: true, value: undefined`).\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\n/** Default codec — stores `{ value, expiresAt? }` verbatim. Zero overhead vs. previous behaviour. */\nexport const defaultCodec: VaultCodec = {\n decode: parseStored as VaultCodec['decode'],\n encode: <T>(value: T, expiresAt?: number): unknown => (expiresAt !== undefined ? { expiresAt, value } : { value }),\n};\n"],"mappings":";;AA8CA,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;AAWA,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;AAcA,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;AAmBA,IAAa,IAA2B;CACtC,QAAQ;CACR,SAAY,GAAU,MAAiC,MAAc,KAAA,IAAmC,EAAE,SAAM,IAA/B;EAAE;EAAW;CAAM;AACtG"}
package/dist/types.cjs ADDED
@@ -0,0 +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;
2
+ //# sourceMappingURL=types.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.cjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { QueryBuilder } from './query';\n\nimport { VaultError } from './errors';\nimport { assertTtlMs, type TtlMs, type VaultCodec } from './ttl';\n\n/* -------------------- Re-export TtlMs and VaultCodec for public API -------------------- */\n\nexport type { TtlMs, VaultCodec };\n\n/* -------------------- Schema types -------------------- */\n\n/** @internal Unique symbol used as a phantom type brand — never appears at runtime. */\ndeclare const schemaEntryBrand: unique symbol;\n\n/**\n * Schema entry for a single table.\n * `T` is the record type; `Key` is the primary key field name.\n *\n * The phantom brand `[schemaEntryBrand]` holds `T` in a directly inferable position so\n * TypeScript can recover `T` in `RecordOf` conditional types. It uses a unique symbol key\n * so it is invisible in IDE autocompletion and cannot be set accidentally.\n */\nexport type SchemaEntry<T extends Record<string, unknown>, Key extends keyof T & string = keyof T & string> = {\n defaultTtl?: TtlMs;\n /**\n * Secondary index field names. The IndexedDB adapter creates an IDB index for each field,\n * enabling push-down optimisation for `equals`, `between`, and `startsWith` queries on those\n * fields — avoiding a full-table scan.\n *\n * ```ts\n * const schema = {\n * users: table<User>('id').index('email').index('city'),\n * };\n * // db.query('users').equals('email', 'alice@example.com') → uses IDB index\n * ```\n */\n indexes?: readonly (keyof T & string)[];\n key: Key;\n /** @internal Phantom brand — enables TypeScript to infer T. Never has a runtime value. */\n readonly [schemaEntryBrand]?: T;\n};\n\n/** A schema is any record of `SchemaEntry`-compatible values. Checked structurally so that\n * concrete `SchemaEntry<T, Key>` values satisfy it without covariance constraints. */\nexport type AnySchema = Record<string, { defaultTtl?: TtlMs; indexes?: readonly string[]; key: string }>;\n\n/**\n * Fluent builder returned by `table()` — satisfies `SchemaEntry` and adds `.ttl()` and `.index()` chaining.\n * Export this type to annotate schema entry variables without using `ReturnType<typeof table<T, K>>`.\n */\nexport type TableBuilder<\n T extends Record<string, unknown>,\n Key extends keyof T & string = keyof T & string,\n> = SchemaEntry<T, Key> & {\n /**\n * Register a secondary index on the given field. Can be chained multiple times.\n * Only used by the IndexedDB adapter; other adapters fall back to in-memory filtering.\n *\n * **Custom codec caveat:** the IndexedDB adapter creates the index with keyPath\n * `value.<field>`, which assumes the default `{ value, expiresAt? }` storage envelope\n * (see `VaultCodec`). A custom codec that changes the top-level shape (e.g.\n * `createVersionedCodec`, or a compact/encrypted format) breaks index push-down silently —\n * queries on the indexed field return empty results instead of throwing. Avoid combining\n * `.index()` with a non-default codec, or design the custom codec to preserve `value.<field>`.\n */\n index: <F extends keyof T & string>(field: F) => TableBuilder<T, Key>;\n /** Set a default TTL (ms) applied to all `put`/`putAll` calls that don't specify one explicitly. */\n ttl: (ms: TtlMs) => TableBuilder<T, Key>;\n};\n\nexport function table<T extends Record<string, unknown>, Key extends keyof T & string = keyof T & string>(\n key: Key,\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 });\n}\n\n/** Extracts the record type from a schema table entry. */\nexport type RecordOf<S extends AnySchema, K extends keyof S> = S[K] extends SchemaEntry<infer R, string> ? R : never;\n\n/** Extracts the primary key value type from a schema table entry. */\nexport type KeyOf<S extends AnySchema, K extends keyof S> =\n S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never;\n\n/* -------------------- Migration -------------------- */\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\n/* -------------------- Plugin / integration types -------------------- */\n\n/**\n * Minimal logger interface satisfied structurally by `/rune` Logger.\n * Pass a rune Logger instance directly — no adapter needed:\n *\n * ```ts\n * import { createLogger } from '@vielzeug/rune';\n * const db = createMemory({ schema, logger: createLogger('db') });\n * ```\n *\n * Vault only emits error-level logs, so a single rune-compatible `error`\n * method is enough.\n */\nexport interface VaultLogger {\n error(messageOrContext?: Record<string, unknown> | Error | string, message?: string): void;\n}\n\n/**\n * Minimal synchronous validator interface satisfied structurally by\n * `/sieve` Schema.\n * Pass a sieve schema directly — no adapter needed:\n *\n * ```ts\n * import { s } from '@vielzeug/spell';\n * const db = createMemory({\n * schema: { users: table<User>('id') },\n * validators: { users: v.object({ id: v.number(), name: v.string() }) },\n * });\n * ```\n *\n * Vault requires synchronous validation because writes may execute inside a\n * live IndexedDB transaction. Any object with a `parse(value: unknown): T`\n * method works.\n */\nexport interface RecordValidator<T> {\n parse(value: unknown): T;\n}\n\n/**\n * Per-table record parsers. Keys match your vault schema table names.\n * Validators run before every `put`, `putAll`, and inside `update`/`upsert`.\n */\nexport type TableValidators<S extends AnySchema> = {\n [K in keyof S]?: RecordValidator<RecordOf<S, K>>;\n};\n\n/**\n * Minimal writable-signal interface satisfied structurally by\n * `/ripple` `Signal<T>` and `Store<T>`.\n * Pass a ripple signal directly — no adapter needed:\n *\n * ```ts\n * import { signal } from '@vielzeug/ripple';\n * const usersSignal = signal<User[]>([]);\n *\n * const db = createMemory({\n * schema: { users: table<User>('id') },\n * signals: { users: usersSignal },\n * });\n *\n * // usersSignal.value is now always in sync with the users table.\n * ```\n *\n * Any object with an `update(fn: (current: T) => T): void` method satisfies\n * this interface. Vault calls `signal.update(() => snapshot)` on each change.\n */\nexport interface ReactiveSignal<T> {\n update(fn: (current: T) => T): void;\n}\n\n/**\n * Per-table reactive signals. Keys match your vault schema table names.\n * Each signal is automatically kept in sync with the table via `observe()`.\n * Signals are wired at construction time and cleaned up on `dispose()`.\n */\nexport type TableSignals<S extends AnySchema> = {\n [K in keyof S]?: ReactiveSignal<RecordOf<S, K>[]>;\n};\n\n/* -------------------- Observer -------------------- */\n\nexport type Observer<T> = (records: T[]) => void;\n\n/** A function that cancels an active subscription. Returned by `observe()` and `observeMany()`. */\nexport type Unsubscribe = () => void;\n\n/* -------------------- Shared adapter options -------------------- */\n\n/**\n * Common options shared by all adapter factories.\n * Individual adapters extend this with adapter-specific fields.\n */\nexport type BaseAdapterOptions<S extends AnySchema> = {\n /**\n * Pluggable serialization codec. Provide a custom implementation to change how values\n * are encoded at rest (e.g. compact keys, encryption, msgpack).\n * Defaults to the standard `{ value, expiresAt? }` JSON envelope.\n *\n * **IndexedDB + `.index()` caveat:** secondary indexes are created with keyPath\n * `value.<field>`, which assumes the default envelope shape. A custom codec that changes\n * the top-level shape breaks index push-down silently (queries return empty results\n * instead of throwing) — see `TableBuilder.index`.\n */\n codec?: VaultCodec;\n /** Structured logger. A /rune Logger satisfies VaultLogger directly. */\n logger?: VaultLogger;\n /** Performance monitoring hook. Called after every operation with duration in ms. */\n onMetrics?: (event: MetricsEvent) => void;\n schema: S;\n /**\n * Per-table reactive signals. A /ripple Signal<T[]> satisfies ReactiveSignal directly.\n * Each signal is kept in sync with its table automatically via observe().\n */\n signals?: TableSignals<S>;\n /** Per-table validators. A /sieve Schema satisfies this directly via `parse()`. */\n validators?: TableValidators<S>;\n};\n\n/* -------------------- Metrics -------------------- */\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 /**\n * The table name. For `batch` operations this is `'*'` because a batch may\n * span multiple tables and there is no single canonical table name.\n */\n table: string;\n};\n\n/* -------------------- Debug info -------------------- */\n\nexport type DebugStats = {\n /** Number of TTL-expired records still physically in the store (not yet lazily evicted). */\n expiredCount: number;\n /** Number of live (non-expired) records. */\n recordCount: number;\n};\n\nexport type DebugInfo<S extends AnySchema> = {\n tables: Array<{ name: keyof S & string } & DebugStats>;\n};\n\n/* -------------------- Transaction context -------------------- */\n\n/**\n * Available inside `batch()` callbacks. For IndexedDB, operations run in a real atomic IDB transaction.\n * The batch scope restricts accessed tables to those declared in `batch(tables, fn)` — accessing\n * others throws `VaultScopeError`.\n */\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 /** Delete multiple records by key in a single operation. Returns the number of records removed. */\n deleteMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<number>;\n /**\n * Returns all `[key, record]` pairs in the table.\n * Useful for cache-warming, migration scripts, and debugging.\n */\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 /** Fetch all records in the table. */\n getAll<T extends K>(table: T): Promise<RecordOf<S, T>[]>;\n /** Fetch multiple records by key in a single operation. Preserves key order; missing keys yield `undefined`. */\n getMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<Array<RecordOf<S, T> | undefined>>;\n /**\n * Read-or-insert: returns the existing record if present, otherwise calls `defaultFn()`,\n * writes the result, and returns it. Equivalent to an `upsert` that never overwrites.\n *\n * **Not atomic for memory and WebStorage adapters.** Two concurrent calls may both observe\n * a missing record and both invoke `defaultFn()`, writing twice. For guaranteed atomicity,\n * wrap in `batch(['table'], tx => tx.getOrDefault(...))` with the IndexedDB adapter.\n */\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 /** Returns `true` if the table contains no live records. Equivalent to `(await count(table)) === 0`. */\n isEmpty<T extends K>(table: T): Promise<boolean>;\n /**\n * Returns the primary key of every live record in the table.\n * Without a `filter`, uses a key-only backend path (no full records fetched).\n * Useful for existence checks, diffing, and cache-invalidation.\n *\n * Pass an optional `filter` predicate to restrict results — when provided, all records are\n * fetched internally and the predicate is applied before key extraction.\n */\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 /**\n * Merge `changes` into the existing record identified by `key` and persist the result.\n * Returns `undefined` when the key does not exist — use `upsert` for insert-or-update semantics.\n */\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/* -------------------- Adapter interface -------------------- */\n\n/** Full client API for vault adapters. */\nexport interface Adapter<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 /** Delete multiple records by key in a single operation. Returns the number of records removed. */\n deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;\n /**\n * Returns all `[key, record]` pairs in the table.\n * Useful for cache-warming, migration scripts, and debugging.\n */\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 /** Fetch all records in the table. */\n getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;\n /** Fetch multiple records by key in a single operation. Preserves key order; missing keys yield `undefined`. */\n getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;\n /**\n * Read-or-insert: returns the existing record if present, otherwise calls `defaultFn()`,\n * writes the result, and returns it. Equivalent to an `upsert` that never overwrites.\n *\n * **Not atomic for memory and WebStorage adapters.** Two concurrent calls may both observe\n * a missing record and both invoke `defaultFn()`, writing twice. For guaranteed atomicity,\n * wrap in `batch(['table'], tx => tx.getOrDefault(...))` with the IndexedDB adapter.\n */\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 /** Returns `true` if the table contains no live records. Equivalent to `(await count(table)) === 0`. */\n isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;\n /**\n * Returns the primary key of every live record in the table.\n * Without a `filter`, uses a key-only backend path (no full records fetched).\n * Useful for existence checks, diffing, and cache-invalidation.\n *\n * Pass an optional `filter` predicate to restrict results — when provided, all records are\n * fetched internally and the predicate is applied before key extraction.\n */\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 /**\n * Merge `changes` into the existing record identified by `key` and persist the result.\n * Returns `undefined` when the key does not exist — use `upsert` for insert-or-update semantics.\n */\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 /**\n * Execute multiple operations against a set of tables with deferred observer notifications.\n *\n * All observer callbacks fire once per dirty table after `fn` resolves, instead of after\n * each individual write. Inside `fn`, only the tables declared in `tables` may be accessed.\n *\n * **Atomicity:** For the IndexedDB adapter this runs as a real IDB transaction — all writes\n * are atomic and rolled back on error. For the memory and WebStorage adapters the operation\n * is **not atomic** — concurrent `batch()` calls or concurrent mutations may interleave.\n */\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n /** Returns live record counts and expired-but-not-yet-evicted counts per table. */\n debug(): Promise<DebugInfo<S>>;\n /** `AbortSignal` aborted when `dispose()` is called. Use to tie external lifetimes to this adapter. */\n readonly disposalSignal: AbortSignal;\n /** Releases all resources (observers, cross-tab channel, DB connection). */\n dispose(): Promise<void>;\n /** `true` after `dispose()` has been called. */\n readonly disposed: boolean;\n /** Delegates to `dispose()`. Enables `await using` declarations. */\n [Symbol.asyncDispose](): Promise<void>;\n observe<K extends keyof S & string>(\n table: K,\n listener: Observer<RecordOf<S, K>>,\n options?: {\n /**\n * When `false`, skips the automatic initial snapshot fired on registration.\n * Useful when the caller already has the current table state (e.g. from a\n * preceding `getAll()` call) and only wants change notifications.\n * Defaults to `true`.\n */\n immediate?: boolean;\n /**\n * An `AbortSignal` that, when aborted, automatically unsubscribes this observer.\n * Aligns with the platform pattern used by `fetch`, `addEventListener`, etc.\n *\n * ```ts\n * const controller = new AbortController();\n * db.observe('users', (users) => render(users), { signal: controller.signal });\n * // later:\n * controller.abort(); // stops the observer\n * ```\n */\n signal?: AbortSignal;\n },\n ): Unsubscribe;\n /**\n * Observe multiple tables simultaneously. The listener receives a combined snapshot\n * `{ [tableName]: RecordOf<S, T>[] }` and fires once after all tables have delivered\n * their first snapshot (initial load). Subsequent firings are coalesced per microtask,\n * so a batch that writes to multiple observed tables triggers exactly one combined callback.\n *\n * @param tables - The tables to observe. Must be non-empty.\n * @param listener - Called with a snapshot map keyed by table name.\n * @param options.signal - An `AbortSignal` that automatically unsubscribes all observers.\n * @returns An unsubscribe function that stops all underlying observers.\n */\n observeMany<K extends keyof S & string>(\n tables: readonly K[],\n listener: (snapshots: { [T in K]: RecordOf<S, T>[] }) => void,\n options?: {\n /**\n * When `true`, fires the listener as soon as any table delivers its first snapshot,\n * using empty arrays for tables not yet resolved. Defaults to `false` (wait for all tables).\n *\n * Useful when some tables may be large and you want to render partial data immediately.\n */\n eager?: boolean;\n signal?: AbortSignal;\n },\n ): Unsubscribe;\n /**\n * Explicitly delete all TTL-expired records from the specified tables (or all tables when\n * no filter is provided). Returns the number of records pruned per table.\n *\n * Useful as a scheduled maintenance task for write-heavy tables that are rarely\n * read (lazy eviction would not reclaim storage otherwise).\n *\n * ```ts\n * // Prune all tables\n * await db.pruneExpired();\n *\n * // Prune only TTL-bearing tables\n * await db.pruneExpired(['sessions', 'tokens']);\n * ```\n *\n * **Does not trigger observer callbacks.** TTL-expired records are already logically\n * absent, so their physical removal does not change observable state.\n */\n pruneExpired(tables?: readonly (keyof S & string)[]): Promise<{ [K in keyof S & string]: number }>;\n /**\n * An AsyncIterable that yields a fresh snapshot of the table on every change.\n * The first value is emitted immediately.\n *\n * ```ts\n * for await (const users of db.watch('users')) {\n * render(users);\n * }\n * ```\n *\n * Pass an `AbortSignal` to stop iteration from outside the loop:\n * ```ts\n * const controller = new AbortController();\n * for await (const users of db.watch('users', { signal: controller.signal })) {\n * render(users);\n * }\n * controller.abort(); // stops the iteration\n * ```\n *\n * @param options.mode\n * - `'latest'` (default): if the consumer lags, intermediate snapshots are dropped and\n * only the most recent one is retained. Best for rendering/display use-cases.\n * - `'all'`: every snapshot is queued and delivered in order. Use when every intermediate\n * state matters (audit trails, animation frames).\n * @param options.signal - An `AbortSignal` that stops the iteration.\n */\n watch<K extends keyof S & string>(\n table: K,\n options?: { mode?: 'all' | 'latest'; signal?: AbortSignal },\n ): AsyncIterable<RecordOf<S, K>[]>;\n}\n"],"mappings":"uDAsEA,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,CAAC,CAC5B"}