@monlite/kv 0.3.0 → 0.4.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.
package/dist/index.cjs CHANGED
@@ -15,7 +15,12 @@ function kv(db, options = {}) {
15
15
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
16
16
  ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL
17
17
  );
18
- CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq)`
18
+ CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq);
19
+ CREATE TABLE IF NOT EXISTS _monlite_kv_zset (
20
+ ns TEXT NOT NULL, k TEXT NOT NULL, member TEXT NOT NULL, score REAL NOT NULL,
21
+ PRIMARY KEY (ns, k, member)
22
+ );
23
+ CREATE INDEX IF NOT EXISTS _idx_kv_zset ON _monlite_kv_zset (ns, k, score, member)`
19
24
  );
20
25
  ensured.add(db);
21
26
  }
@@ -30,7 +35,7 @@ function kv(db, options = {}) {
30
35
  ).run(ns, key, v, expires);
31
36
  let timer;
32
37
  const subs = /* @__PURE__ */ new Set();
33
- let psTimer;
38
+ let psTask;
34
39
  const psMaxSeq = () => driver.prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`).get(ns).s ?? 0;
35
40
  const drainPubsub = () => {
36
41
  if (subs.size === 0) return;
@@ -132,6 +137,73 @@ function kv(db, options = {}) {
132
137
  `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`
133
138
  ).get(ns, now()).n;
134
139
  },
140
+ // ── sorted sets ──────────────────────────────────────────────────────────
141
+ zadd(key, score, member) {
142
+ driver.prepare(
143
+ `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)
144
+ ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`
145
+ ).run(ns, key, member, score);
146
+ },
147
+ zincrby(key, delta, member) {
148
+ return driver.transaction(() => {
149
+ const row = driver.prepare(
150
+ `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
151
+ ).get(ns, key, member);
152
+ const next = (row?.score ?? 0) + delta;
153
+ driver.prepare(
154
+ `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)
155
+ ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`
156
+ ).run(ns, key, member, next);
157
+ return next;
158
+ }, true);
159
+ },
160
+ zscore(key, member) {
161
+ const row = driver.prepare(
162
+ `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
163
+ ).get(ns, key, member);
164
+ return row?.score;
165
+ },
166
+ zrem(key, member) {
167
+ return driver.prepare(
168
+ `DELETE FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
169
+ ).run(ns, key, member).changes > 0;
170
+ },
171
+ zcard(key) {
172
+ return driver.prepare(
173
+ `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ?`
174
+ ).get(ns, key).n;
175
+ },
176
+ zrank(key, member, opts) {
177
+ const row = driver.prepare(
178
+ `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
179
+ ).get(ns, key, member);
180
+ if (!row) return void 0;
181
+ const cmp = opts?.rev ? `score > ? OR (score = ? AND member > ?)` : `score < ? OR (score = ? AND member < ?)`;
182
+ return driver.prepare(
183
+ `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND (${cmp})`
184
+ ).get(ns, key, row.score, row.score, member).n;
185
+ },
186
+ zrange(key, start, stop, opts) {
187
+ const card = api.zcard(key);
188
+ let lo = start < 0 ? card + start : start;
189
+ let hi = stop < 0 ? card + stop : stop;
190
+ if (lo < 0) lo = 0;
191
+ if (hi >= card) hi = card - 1;
192
+ if (card === 0 || lo > hi) return [];
193
+ const dir = opts?.rev ? "DESC" : "ASC";
194
+ const rows = driver.prepare(
195
+ `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?
196
+ ORDER BY score ${dir}, member ${dir} LIMIT ? OFFSET ?`
197
+ ).all(ns, key, hi - lo + 1, lo);
198
+ return opts?.withScores ? rows : rows.map((r) => r.member);
199
+ },
200
+ zrangeByScore(key, min, max, opts) {
201
+ const rows = driver.prepare(
202
+ `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?
203
+ AND score >= ? AND score <= ? ORDER BY score ASC, member ASC`
204
+ ).all(ns, key, min, max);
205
+ return opts?.withScores ? rows : rows.map((r) => r.member);
206
+ },
135
207
  publish(channel, message) {
136
208
  const ts = now();
137
209
  driver.prepare(
@@ -146,23 +218,22 @@ function kv(db, options = {}) {
146
218
  subscribe(channel, cb) {
147
219
  const sub = { channel, cb, cursor: psMaxSeq() };
148
220
  subs.add(sub);
149
- if (!psTimer) {
150
- psTimer = setInterval(drainPubsub, pubsubPollMs);
151
- psTimer.unref?.();
152
- }
221
+ if (!psTask) psTask = db.heartbeat.every(pubsubPollMs, drainPubsub);
153
222
  return () => {
154
223
  subs.delete(sub);
155
- if (subs.size === 0 && psTimer) {
156
- clearInterval(psTimer);
157
- psTimer = void 0;
224
+ if (subs.size === 0 && psTask) {
225
+ psTask.cancel();
226
+ psTask = void 0;
158
227
  }
159
228
  };
160
229
  },
161
230
  stop() {
162
231
  if (timer) clearInterval(timer);
163
232
  timer = void 0;
164
- if (psTimer) clearInterval(psTimer);
165
- psTimer = void 0;
233
+ if (psTask) {
234
+ psTask.cancel();
235
+ psTask = void 0;
236
+ }
166
237
  }
167
238
  };
168
239
  if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Monlite } from \"@monlite/core\";\n\nexport interface KVOptions {\n /** Logical namespace so multiple caches can share one database. Default \"default\". */\n namespace?: string;\n /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */\n sweepIntervalMs?: number;\n /** How often (ms) a `subscribe()` listener polls for cross-process messages. Default `200`. */\n pubsubPollMs?: number;\n}\n\n/**\n * A synchronous, Redis-like key-value cache backed by SQLite. Values are any\n * JSON-serializable data; TTLs are in milliseconds.\n */\nexport interface KV {\n get<T = any>(key: string): T | undefined;\n set(key: string, value: any, opts?: { ttl?: number }): void;\n /**\n * Atomically set the key only if it isn't already present (Redis `SET NX`).\n * Returns `true` if set, `false` if a live key already existed. The lock\n * primitive for single-instance schedulers, nonces, and once-only work.\n */\n setNX(key: string, value: any, opts?: { ttl?: number }): boolean;\n has(key: string): boolean;\n delete(key: string): boolean;\n /** Atomically add `by` (default 1) to a numeric key; returns the new value. */\n incr(key: string, by?: number): number;\n decr(key: string, by?: number): number;\n mget<T = any>(keys: string[]): (T | undefined)[];\n /** Keys in this namespace (optionally by prefix), excluding expired ones. */\n keys(prefix?: string): string[];\n /** Set/refresh a key's TTL (ms). Returns false if the key is absent. */\n expire(key: string, ttl: number): boolean;\n /** Remaining TTL in ms; `-1` if no expiry, `-2` if absent (Redis convention). */\n ttl(key: string): number;\n /** Delete all keys in this namespace. */\n flush(): void;\n /** Number of live keys in this namespace. */\n size(): number;\n /**\n * Publish a message to a channel (Redis `PUBLISH`). Delivered to every\n * `subscribe()` listener on that channel — including in OTHER processes\n * sharing this database. Ephemeral: not replayed to late subscribers. Returns\n * the number of listeners on this instance that received it.\n */\n publish(channel: string, message: any): number;\n /**\n * Subscribe to a channel (Redis `SUBSCRIBE`). The callback fires for each\n * message published AFTER this call, cross-process. Returns an unsubscribe.\n */\n subscribe(channel: string, cb: (message: any) => void): () => void;\n /** Stop the sweep + pub/sub timers (if any). */\n stop(): void;\n}\n\nconst ensured = new WeakSet<object>();\n\n/**\n * Create a cache over a monlite database.\n *\n * ```ts\n * const cache = kv(db);\n * cache.set(\"session:42\", { user: \"ali\" }, { ttl: 60_000 });\n * cache.get(\"session:42\"); // { user: \"ali\" } (synchronous)\n * ```\n */\nexport function kv(db: Monlite, options: KVOptions = {}): KV {\n const ns = options.namespace ?? \"default\";\n const driver = db.driver;\n\n if (!ensured.has(db)) {\n driver.exec(\n `CREATE TABLE IF NOT EXISTS _kv (\n ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,\n expires_at INTEGER, PRIMARY KEY (ns, k)\n );\n CREATE TABLE IF NOT EXISTS _monlite_kv_pubsub (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq)`,\n );\n ensured.add(db);\n }\n\n const now = () => Date.now();\n const pubsubPollMs = Math.max(20, options.pubsubPollMs ?? 200);\n const fresh = (row: any): boolean =>\n !!row && !(row.expires_at != null && row.expires_at <= now());\n\n const getRow = (key: string) =>\n driver\n .prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`)\n .get(ns, key) as { v: string; expires_at: number | null } | undefined;\n\n const del = (key: string): boolean =>\n driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key)\n .changes > 0;\n\n const setRaw = (key: string, v: string, expires: number | null) =>\n driver\n .prepare(\n `INSERT INTO _kv (ns, k, v, expires_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`,\n )\n .run(ns, key, v, expires);\n\n let timer: ReturnType<typeof setInterval> | undefined;\n\n // ── pub/sub ──────────────────────────────────────────────────────────────\n type Sub = { channel: string; cb: (m: any) => void; cursor: number };\n const subs = new Set<Sub>();\n let psTimer: ReturnType<typeof setInterval> | undefined;\n\n const psMaxSeq = (): number =>\n (\n driver\n .prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`)\n .get(ns) as { s: number | null }\n ).s ?? 0;\n\n /** Deliver new messages to every local subscriber (cross-process via the table). */\n const drainPubsub = (): void => {\n if (subs.size === 0) return;\n let min = Infinity;\n for (const s of subs) min = Math.min(min, s.cursor);\n const rows = driver\n .prepare(\n `SELECT seq, channel, payload FROM _monlite_kv_pubsub WHERE ns = ? AND seq > ? ORDER BY seq`,\n )\n .all(ns, min) as Array<{ seq: number; channel: string; payload: string }>;\n if (rows.length === 0) return;\n for (const s of [...subs]) {\n for (const r of rows) {\n if (r.seq <= s.cursor) continue;\n s.cursor = r.seq;\n if (r.channel === s.channel) {\n try {\n s.cb(JSON.parse(r.payload ?? \"null\"));\n } catch {\n /* a subscriber callback must not break delivery to others */\n }\n }\n }\n }\n };\n\n const api: KV = {\n get(key) {\n const row = getRow(key);\n if (!fresh(row)) {\n if (row) del(key);\n return undefined;\n }\n return JSON.parse(row!.v);\n },\n set(key, value, opts) {\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n },\n setNX(key, value, opts) {\n // IMMEDIATE: take the write lock up front so two processes racing the same\n // key can't deadlock on lock upgrade — the loser cleanly gets `false`.\n return driver.transaction(() => {\n const row = getRow(key);\n if (fresh(row)) return false; // a live key already exists\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n return true;\n }, true);\n },\n has(key) {\n return api.get(key) !== undefined;\n },\n delete(key) {\n return del(key);\n },\n incr(key, by = 1) {\n return driver.transaction(() => {\n // IMMEDIATE: read-modify-write needs the write lock up front\n const row = getRow(key);\n let n = 0;\n let expires: number | null = null;\n if (fresh(row)) {\n const cur = JSON.parse(row!.v);\n if (typeof cur !== \"number\") {\n throw new Error(`kv.incr: value at \"${key}\" is not a number`);\n }\n n = cur;\n expires = row!.expires_at;\n }\n const next = n + by;\n setRaw(key, JSON.stringify(next), expires);\n return next;\n }, true);\n },\n decr(key, by = 1) {\n return api.incr(key, -by);\n },\n mget(keys) {\n return keys.map((k) => api.get(k));\n },\n keys(prefix) {\n const t = now();\n const rows = (\n prefix !== undefined\n ? driver\n .prepare(\n `SELECT k, expires_at FROM _kv WHERE ns = ? AND k LIKE ? ESCAPE '\\\\'`,\n )\n .all(ns, prefix.replace(/[%_\\\\]/g, \"\\\\$&\") + \"%\")\n : driver.prepare(`SELECT k, expires_at FROM _kv WHERE ns = ?`).all(ns)\n ) as Array<{ k: string; expires_at: number | null }>;\n return rows\n .filter((r) => r.expires_at == null || r.expires_at > t)\n .map((r) => r.k);\n },\n expire(key, ttl) {\n const row = getRow(key);\n if (!fresh(row)) return false;\n driver\n .prepare(`UPDATE _kv SET expires_at = ? WHERE ns = ? AND k = ?`)\n .run(now() + ttl, ns, key);\n return true;\n },\n ttl(key) {\n const row = getRow(key);\n if (!fresh(row)) return -2;\n if (row!.expires_at == null) return -1;\n return row!.expires_at - now();\n },\n flush() {\n driver.prepare(`DELETE FROM _kv WHERE ns = ?`).run(ns);\n },\n size() {\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(ns, now()) as { n: number }\n ).n;\n },\n publish(channel, message) {\n const ts = now();\n driver\n .prepare(\n `INSERT INTO _monlite_kv_pubsub (ns, channel, payload, ts) VALUES (?, ?, ?, ?)`,\n )\n .run(ns, channel, JSON.stringify(message ?? null), ts);\n // Ephemeral: prune old messages (late subscribers don't replay) so the\n // table can't grow unbounded.\n driver\n .prepare(`DELETE FROM _monlite_kv_pubsub WHERE ts < ?`)\n .run(ts - 30_000);\n // Deliver to same-instance subscribers immediately (cross-process listeners\n // pick it up on their next poll).\n drainPubsub();\n let n = 0;\n for (const s of subs) if (s.channel === channel) n++;\n return n;\n },\n subscribe(channel, cb) {\n const sub: Sub = { channel, cb, cursor: psMaxSeq() }; // start at \"now\" — no replay\n subs.add(sub);\n if (!psTimer) {\n psTimer = setInterval(drainPubsub, pubsubPollMs);\n psTimer.unref?.();\n }\n return () => {\n subs.delete(sub);\n if (subs.size === 0 && psTimer) {\n clearInterval(psTimer);\n psTimer = undefined;\n }\n };\n },\n stop() {\n if (timer) clearInterval(timer);\n timer = undefined;\n if (psTimer) clearInterval(psTimer);\n psTimer = undefined;\n },\n };\n\n if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {\n timer = setInterval(() => {\n driver\n .prepare(\n `DELETE FROM _kv WHERE expires_at IS NOT NULL AND expires_at <= ?`,\n )\n .run(now());\n }, options.sweepIntervalMs);\n timer.unref?.();\n }\n\n return api;\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Monlite, HeartbeatTask } from \"@monlite/core\";\n\nexport interface KVOptions {\n /** Logical namespace so multiple caches can share one database. Default \"default\". */\n namespace?: string;\n /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */\n sweepIntervalMs?: number;\n /** How often (ms) a `subscribe()` listener polls for cross-process messages. Default `200`. */\n pubsubPollMs?: number;\n}\n\n/**\n * A synchronous, Redis-like key-value cache backed by SQLite. Values are any\n * JSON-serializable data; TTLs are in milliseconds.\n */\nexport interface KV {\n get<T = any>(key: string): T | undefined;\n set(key: string, value: any, opts?: { ttl?: number }): void;\n /**\n * Atomically set the key only if it isn't already present (Redis `SET NX`).\n * Returns `true` if set, `false` if a live key already existed. The lock\n * primitive for single-instance schedulers, nonces, and once-only work.\n */\n setNX(key: string, value: any, opts?: { ttl?: number }): boolean;\n has(key: string): boolean;\n delete(key: string): boolean;\n /** Atomically add `by` (default 1) to a numeric key; returns the new value. */\n incr(key: string, by?: number): number;\n decr(key: string, by?: number): number;\n mget<T = any>(keys: string[]): (T | undefined)[];\n /** Keys in this namespace (optionally by prefix), excluding expired ones. */\n keys(prefix?: string): string[];\n /** Set/refresh a key's TTL (ms). Returns false if the key is absent. */\n expire(key: string, ttl: number): boolean;\n /** Remaining TTL in ms; `-1` if no expiry, `-2` if absent (Redis convention). */\n ttl(key: string): number;\n /** Delete all keys in this namespace. */\n flush(): void;\n /** Number of live keys in this namespace. */\n size(): number;\n\n // ── sorted sets (Redis ZSET) — leaderboards, rate-limiters, priority indexes ──\n /** Add or update `member` with `score` (Redis `ZADD`). */\n zadd(key: string, score: number, member: string): void;\n /** Increment `member`'s score by `delta` (`ZINCRBY`); returns the new score. */\n zincrby(key: string, delta: number, member: string): number;\n /** A member's score, or `undefined` if absent (`ZSCORE`). */\n zscore(key: string, member: string): number | undefined;\n /** Remove `member` (`ZREM`); returns `true` if it existed. */\n zrem(key: string, member: string): boolean;\n /** Number of members (`ZCARD`). */\n zcard(key: string): number;\n /** 0-based rank by ascending score (ties lexicographic); `rev` for descending. `undefined` if absent. */\n zrank(\n key: string,\n member: string,\n opts?: { rev?: boolean },\n ): number | undefined;\n /**\n * Members by rank range `[start, stop]` inclusive (negative counts from the end,\n * Redis-style), ascending by score (`rev` = descending). With `withScores`,\n * returns `{ member, score }[]` (`ZRANGE` / `ZREVRANGE`).\n */\n zrange(\n key: string,\n start: number,\n stop: number,\n opts?: { rev?: boolean; withScores?: boolean },\n ): string[] | Array<{ member: string; score: number }>;\n /** Members with `min <= score <= max`, ascending (`ZRANGEBYSCORE`). */\n zrangeByScore(\n key: string,\n min: number,\n max: number,\n opts?: { withScores?: boolean },\n ): string[] | Array<{ member: string; score: number }>;\n\n /**\n * Publish a message to a channel (Redis `PUBLISH`). Delivered to every\n * `subscribe()` listener on that channel — including in OTHER processes\n * sharing this database. Ephemeral: not replayed to late subscribers. Returns\n * the number of listeners on this instance that received it.\n */\n publish(channel: string, message: any): number;\n /**\n * Subscribe to a channel (Redis `SUBSCRIBE`). The callback fires for each\n * message published AFTER this call, cross-process. Returns an unsubscribe.\n */\n subscribe(channel: string, cb: (message: any) => void): () => void;\n /** Stop the sweep + pub/sub timers (if any). */\n stop(): void;\n}\n\nconst ensured = new WeakSet<object>();\n\n/**\n * Create a cache over a monlite database.\n *\n * ```ts\n * const cache = kv(db);\n * cache.set(\"session:42\", { user: \"ali\" }, { ttl: 60_000 });\n * cache.get(\"session:42\"); // { user: \"ali\" } (synchronous)\n * ```\n */\nexport function kv(db: Monlite, options: KVOptions = {}): KV {\n const ns = options.namespace ?? \"default\";\n const driver = db.driver;\n\n if (!ensured.has(db)) {\n driver.exec(\n `CREATE TABLE IF NOT EXISTS _kv (\n ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,\n expires_at INTEGER, PRIMARY KEY (ns, k)\n );\n CREATE TABLE IF NOT EXISTS _monlite_kv_pubsub (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq);\n CREATE TABLE IF NOT EXISTS _monlite_kv_zset (\n ns TEXT NOT NULL, k TEXT NOT NULL, member TEXT NOT NULL, score REAL NOT NULL,\n PRIMARY KEY (ns, k, member)\n );\n CREATE INDEX IF NOT EXISTS _idx_kv_zset ON _monlite_kv_zset (ns, k, score, member)`,\n );\n ensured.add(db);\n }\n\n const now = () => Date.now();\n const pubsubPollMs = Math.max(20, options.pubsubPollMs ?? 200);\n const fresh = (row: any): boolean =>\n !!row && !(row.expires_at != null && row.expires_at <= now());\n\n const getRow = (key: string) =>\n driver\n .prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`)\n .get(ns, key) as { v: string; expires_at: number | null } | undefined;\n\n const del = (key: string): boolean =>\n driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key)\n .changes > 0;\n\n const setRaw = (key: string, v: string, expires: number | null) =>\n driver\n .prepare(\n `INSERT INTO _kv (ns, k, v, expires_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`,\n )\n .run(ns, key, v, expires);\n\n let timer: ReturnType<typeof setInterval> | undefined;\n\n // ── pub/sub ──────────────────────────────────────────────────────────────\n type Sub = { channel: string; cb: (m: any) => void; cursor: number };\n const subs = new Set<Sub>();\n let psTask: HeartbeatTask | undefined;\n\n const psMaxSeq = (): number =>\n (\n driver\n .prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`)\n .get(ns) as { s: number | null }\n ).s ?? 0;\n\n /** Deliver new messages to every local subscriber (cross-process via the table). */\n const drainPubsub = (): void => {\n if (subs.size === 0) return;\n let min = Infinity;\n for (const s of subs) min = Math.min(min, s.cursor);\n const rows = driver\n .prepare(\n `SELECT seq, channel, payload FROM _monlite_kv_pubsub WHERE ns = ? AND seq > ? ORDER BY seq`,\n )\n .all(ns, min) as Array<{ seq: number; channel: string; payload: string }>;\n if (rows.length === 0) return;\n for (const s of [...subs]) {\n for (const r of rows) {\n if (r.seq <= s.cursor) continue;\n s.cursor = r.seq;\n if (r.channel === s.channel) {\n try {\n s.cb(JSON.parse(r.payload ?? \"null\"));\n } catch {\n /* a subscriber callback must not break delivery to others */\n }\n }\n }\n }\n };\n\n const api: KV = {\n get(key) {\n const row = getRow(key);\n if (!fresh(row)) {\n if (row) del(key);\n return undefined;\n }\n return JSON.parse(row!.v);\n },\n set(key, value, opts) {\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n },\n setNX(key, value, opts) {\n // IMMEDIATE: take the write lock up front so two processes racing the same\n // key can't deadlock on lock upgrade — the loser cleanly gets `false`.\n return driver.transaction(() => {\n const row = getRow(key);\n if (fresh(row)) return false; // a live key already exists\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n return true;\n }, true);\n },\n has(key) {\n return api.get(key) !== undefined;\n },\n delete(key) {\n return del(key);\n },\n incr(key, by = 1) {\n return driver.transaction(() => {\n // IMMEDIATE: read-modify-write needs the write lock up front\n const row = getRow(key);\n let n = 0;\n let expires: number | null = null;\n if (fresh(row)) {\n const cur = JSON.parse(row!.v);\n if (typeof cur !== \"number\") {\n throw new Error(`kv.incr: value at \"${key}\" is not a number`);\n }\n n = cur;\n expires = row!.expires_at;\n }\n const next = n + by;\n setRaw(key, JSON.stringify(next), expires);\n return next;\n }, true);\n },\n decr(key, by = 1) {\n return api.incr(key, -by);\n },\n mget(keys) {\n return keys.map((k) => api.get(k));\n },\n keys(prefix) {\n const t = now();\n const rows = (\n prefix !== undefined\n ? driver\n .prepare(\n `SELECT k, expires_at FROM _kv WHERE ns = ? AND k LIKE ? ESCAPE '\\\\'`,\n )\n .all(ns, prefix.replace(/[%_\\\\]/g, \"\\\\$&\") + \"%\")\n : driver.prepare(`SELECT k, expires_at FROM _kv WHERE ns = ?`).all(ns)\n ) as Array<{ k: string; expires_at: number | null }>;\n return rows\n .filter((r) => r.expires_at == null || r.expires_at > t)\n .map((r) => r.k);\n },\n expire(key, ttl) {\n const row = getRow(key);\n if (!fresh(row)) return false;\n driver\n .prepare(`UPDATE _kv SET expires_at = ? WHERE ns = ? AND k = ?`)\n .run(now() + ttl, ns, key);\n return true;\n },\n ttl(key) {\n const row = getRow(key);\n if (!fresh(row)) return -2;\n if (row!.expires_at == null) return -1;\n return row!.expires_at - now();\n },\n flush() {\n driver.prepare(`DELETE FROM _kv WHERE ns = ?`).run(ns);\n },\n size() {\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(ns, now()) as { n: number }\n ).n;\n },\n\n // ── sorted sets ──────────────────────────────────────────────────────────\n zadd(key, score, member) {\n driver\n .prepare(\n `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`,\n )\n .run(ns, key, member, score);\n },\n zincrby(key, delta, member) {\n // IMMEDIATE: atomic read-modify-write across processes.\n return driver.transaction(() => {\n const row = driver\n .prepare(\n `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .get(ns, key, member) as { score: number } | undefined;\n const next = (row?.score ?? 0) + delta;\n driver\n .prepare(\n `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`,\n )\n .run(ns, key, member, next);\n return next;\n }, true);\n },\n zscore(key, member) {\n const row = driver\n .prepare(\n `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .get(ns, key, member) as { score: number } | undefined;\n return row?.score;\n },\n zrem(key, member) {\n return (\n driver\n .prepare(\n `DELETE FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .run(ns, key, member).changes > 0\n );\n },\n zcard(key) {\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ?`,\n )\n .get(ns, key) as { n: number }\n ).n;\n },\n zrank(key, member, opts) {\n const row = driver\n .prepare(\n `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .get(ns, key, member) as { score: number } | undefined;\n if (!row) return undefined;\n // Members ordered before this one (ties broken lexicographically by member).\n const cmp = opts?.rev\n ? `score > ? OR (score = ? AND member > ?)`\n : `score < ? OR (score = ? AND member < ?)`;\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND (${cmp})`,\n )\n .get(ns, key, row.score, row.score, member) as { n: number }\n ).n;\n },\n zrange(key, start, stop, opts) {\n const card = api.zcard(key);\n let lo = start < 0 ? card + start : start;\n let hi = stop < 0 ? card + stop : stop;\n if (lo < 0) lo = 0;\n if (hi >= card) hi = card - 1;\n if (card === 0 || lo > hi) return [];\n const dir = opts?.rev ? \"DESC\" : \"ASC\";\n const rows = driver\n .prepare(\n `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?\n ORDER BY score ${dir}, member ${dir} LIMIT ? OFFSET ?`,\n )\n .all(ns, key, hi - lo + 1, lo) as Array<{\n member: string;\n score: number;\n }>;\n return opts?.withScores ? rows : rows.map((r) => r.member);\n },\n zrangeByScore(key, min, max, opts) {\n const rows = driver\n .prepare(\n `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?\n AND score >= ? AND score <= ? ORDER BY score ASC, member ASC`,\n )\n .all(ns, key, min, max) as Array<{ member: string; score: number }>;\n return opts?.withScores ? rows : rows.map((r) => r.member);\n },\n\n publish(channel, message) {\n const ts = now();\n driver\n .prepare(\n `INSERT INTO _monlite_kv_pubsub (ns, channel, payload, ts) VALUES (?, ?, ?, ?)`,\n )\n .run(ns, channel, JSON.stringify(message ?? null), ts);\n // Ephemeral: prune old messages (late subscribers don't replay) so the\n // table can't grow unbounded.\n driver\n .prepare(`DELETE FROM _monlite_kv_pubsub WHERE ts < ?`)\n .run(ts - 30_000);\n // Deliver to same-instance subscribers immediately (cross-process listeners\n // pick it up on their next poll).\n drainPubsub();\n let n = 0;\n for (const s of subs) if (s.channel === channel) n++;\n return n;\n },\n subscribe(channel, cb) {\n const sub: Sub = { channel, cb, cursor: psMaxSeq() }; // start at \"now\" — no replay\n subs.add(sub);\n // Register the cross-process poll on the shared heartbeat (one timer for the\n // whole db), started on first subscribe and dropped when the last unsubscribes.\n if (!psTask) psTask = db.heartbeat.every(pubsubPollMs, drainPubsub);\n return () => {\n subs.delete(sub);\n if (subs.size === 0 && psTask) {\n psTask.cancel();\n psTask = undefined;\n }\n };\n },\n stop() {\n if (timer) clearInterval(timer);\n timer = undefined;\n if (psTask) {\n psTask.cancel();\n psTask = undefined;\n }\n },\n };\n\n if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {\n timer = setInterval(() => {\n driver\n .prepare(\n `DELETE FROM _kv WHERE expires_at IS NOT NULL AND expires_at <= ?`,\n )\n .run(now());\n }, options.sweepIntervalMs);\n timer.unref?.();\n }\n\n return api;\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -41,6 +41,39 @@ interface KV {
41
41
  flush(): void;
42
42
  /** Number of live keys in this namespace. */
43
43
  size(): number;
44
+ /** Add or update `member` with `score` (Redis `ZADD`). */
45
+ zadd(key: string, score: number, member: string): void;
46
+ /** Increment `member`'s score by `delta` (`ZINCRBY`); returns the new score. */
47
+ zincrby(key: string, delta: number, member: string): number;
48
+ /** A member's score, or `undefined` if absent (`ZSCORE`). */
49
+ zscore(key: string, member: string): number | undefined;
50
+ /** Remove `member` (`ZREM`); returns `true` if it existed. */
51
+ zrem(key: string, member: string): boolean;
52
+ /** Number of members (`ZCARD`). */
53
+ zcard(key: string): number;
54
+ /** 0-based rank by ascending score (ties lexicographic); `rev` for descending. `undefined` if absent. */
55
+ zrank(key: string, member: string, opts?: {
56
+ rev?: boolean;
57
+ }): number | undefined;
58
+ /**
59
+ * Members by rank range `[start, stop]` inclusive (negative counts from the end,
60
+ * Redis-style), ascending by score (`rev` = descending). With `withScores`,
61
+ * returns `{ member, score }[]` (`ZRANGE` / `ZREVRANGE`).
62
+ */
63
+ zrange(key: string, start: number, stop: number, opts?: {
64
+ rev?: boolean;
65
+ withScores?: boolean;
66
+ }): string[] | Array<{
67
+ member: string;
68
+ score: number;
69
+ }>;
70
+ /** Members with `min <= score <= max`, ascending (`ZRANGEBYSCORE`). */
71
+ zrangeByScore(key: string, min: number, max: number, opts?: {
72
+ withScores?: boolean;
73
+ }): string[] | Array<{
74
+ member: string;
75
+ score: number;
76
+ }>;
44
77
  /**
45
78
  * Publish a message to a channel (Redis `PUBLISH`). Delivered to every
46
79
  * `subscribe()` listener on that channel — including in OTHER processes
package/dist/index.d.ts CHANGED
@@ -41,6 +41,39 @@ interface KV {
41
41
  flush(): void;
42
42
  /** Number of live keys in this namespace. */
43
43
  size(): number;
44
+ /** Add or update `member` with `score` (Redis `ZADD`). */
45
+ zadd(key: string, score: number, member: string): void;
46
+ /** Increment `member`'s score by `delta` (`ZINCRBY`); returns the new score. */
47
+ zincrby(key: string, delta: number, member: string): number;
48
+ /** A member's score, or `undefined` if absent (`ZSCORE`). */
49
+ zscore(key: string, member: string): number | undefined;
50
+ /** Remove `member` (`ZREM`); returns `true` if it existed. */
51
+ zrem(key: string, member: string): boolean;
52
+ /** Number of members (`ZCARD`). */
53
+ zcard(key: string): number;
54
+ /** 0-based rank by ascending score (ties lexicographic); `rev` for descending. `undefined` if absent. */
55
+ zrank(key: string, member: string, opts?: {
56
+ rev?: boolean;
57
+ }): number | undefined;
58
+ /**
59
+ * Members by rank range `[start, stop]` inclusive (negative counts from the end,
60
+ * Redis-style), ascending by score (`rev` = descending). With `withScores`,
61
+ * returns `{ member, score }[]` (`ZRANGE` / `ZREVRANGE`).
62
+ */
63
+ zrange(key: string, start: number, stop: number, opts?: {
64
+ rev?: boolean;
65
+ withScores?: boolean;
66
+ }): string[] | Array<{
67
+ member: string;
68
+ score: number;
69
+ }>;
70
+ /** Members with `min <= score <= max`, ascending (`ZRANGEBYSCORE`). */
71
+ zrangeByScore(key: string, min: number, max: number, opts?: {
72
+ withScores?: boolean;
73
+ }): string[] | Array<{
74
+ member: string;
75
+ score: number;
76
+ }>;
44
77
  /**
45
78
  * Publish a message to a channel (Redis `PUBLISH`). Delivered to every
46
79
  * `subscribe()` listener on that channel — including in OTHER processes
package/dist/index.js CHANGED
@@ -13,7 +13,12 @@ function kv(db, options = {}) {
13
13
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
14
14
  ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL
15
15
  );
16
- CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq)`
16
+ CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq);
17
+ CREATE TABLE IF NOT EXISTS _monlite_kv_zset (
18
+ ns TEXT NOT NULL, k TEXT NOT NULL, member TEXT NOT NULL, score REAL NOT NULL,
19
+ PRIMARY KEY (ns, k, member)
20
+ );
21
+ CREATE INDEX IF NOT EXISTS _idx_kv_zset ON _monlite_kv_zset (ns, k, score, member)`
17
22
  );
18
23
  ensured.add(db);
19
24
  }
@@ -28,7 +33,7 @@ function kv(db, options = {}) {
28
33
  ).run(ns, key, v, expires);
29
34
  let timer;
30
35
  const subs = /* @__PURE__ */ new Set();
31
- let psTimer;
36
+ let psTask;
32
37
  const psMaxSeq = () => driver.prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`).get(ns).s ?? 0;
33
38
  const drainPubsub = () => {
34
39
  if (subs.size === 0) return;
@@ -130,6 +135,73 @@ function kv(db, options = {}) {
130
135
  `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`
131
136
  ).get(ns, now()).n;
132
137
  },
138
+ // ── sorted sets ──────────────────────────────────────────────────────────
139
+ zadd(key, score, member) {
140
+ driver.prepare(
141
+ `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)
142
+ ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`
143
+ ).run(ns, key, member, score);
144
+ },
145
+ zincrby(key, delta, member) {
146
+ return driver.transaction(() => {
147
+ const row = driver.prepare(
148
+ `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
149
+ ).get(ns, key, member);
150
+ const next = (row?.score ?? 0) + delta;
151
+ driver.prepare(
152
+ `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)
153
+ ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`
154
+ ).run(ns, key, member, next);
155
+ return next;
156
+ }, true);
157
+ },
158
+ zscore(key, member) {
159
+ const row = driver.prepare(
160
+ `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
161
+ ).get(ns, key, member);
162
+ return row?.score;
163
+ },
164
+ zrem(key, member) {
165
+ return driver.prepare(
166
+ `DELETE FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
167
+ ).run(ns, key, member).changes > 0;
168
+ },
169
+ zcard(key) {
170
+ return driver.prepare(
171
+ `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ?`
172
+ ).get(ns, key).n;
173
+ },
174
+ zrank(key, member, opts) {
175
+ const row = driver.prepare(
176
+ `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`
177
+ ).get(ns, key, member);
178
+ if (!row) return void 0;
179
+ const cmp = opts?.rev ? `score > ? OR (score = ? AND member > ?)` : `score < ? OR (score = ? AND member < ?)`;
180
+ return driver.prepare(
181
+ `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND (${cmp})`
182
+ ).get(ns, key, row.score, row.score, member).n;
183
+ },
184
+ zrange(key, start, stop, opts) {
185
+ const card = api.zcard(key);
186
+ let lo = start < 0 ? card + start : start;
187
+ let hi = stop < 0 ? card + stop : stop;
188
+ if (lo < 0) lo = 0;
189
+ if (hi >= card) hi = card - 1;
190
+ if (card === 0 || lo > hi) return [];
191
+ const dir = opts?.rev ? "DESC" : "ASC";
192
+ const rows = driver.prepare(
193
+ `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?
194
+ ORDER BY score ${dir}, member ${dir} LIMIT ? OFFSET ?`
195
+ ).all(ns, key, hi - lo + 1, lo);
196
+ return opts?.withScores ? rows : rows.map((r) => r.member);
197
+ },
198
+ zrangeByScore(key, min, max, opts) {
199
+ const rows = driver.prepare(
200
+ `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?
201
+ AND score >= ? AND score <= ? ORDER BY score ASC, member ASC`
202
+ ).all(ns, key, min, max);
203
+ return opts?.withScores ? rows : rows.map((r) => r.member);
204
+ },
133
205
  publish(channel, message) {
134
206
  const ts = now();
135
207
  driver.prepare(
@@ -144,23 +216,22 @@ function kv(db, options = {}) {
144
216
  subscribe(channel, cb) {
145
217
  const sub = { channel, cb, cursor: psMaxSeq() };
146
218
  subs.add(sub);
147
- if (!psTimer) {
148
- psTimer = setInterval(drainPubsub, pubsubPollMs);
149
- psTimer.unref?.();
150
- }
219
+ if (!psTask) psTask = db.heartbeat.every(pubsubPollMs, drainPubsub);
151
220
  return () => {
152
221
  subs.delete(sub);
153
- if (subs.size === 0 && psTimer) {
154
- clearInterval(psTimer);
155
- psTimer = void 0;
222
+ if (subs.size === 0 && psTask) {
223
+ psTask.cancel();
224
+ psTask = void 0;
156
225
  }
157
226
  };
158
227
  },
159
228
  stop() {
160
229
  if (timer) clearInterval(timer);
161
230
  timer = void 0;
162
- if (psTimer) clearInterval(psTimer);
163
- psTimer = void 0;
231
+ if (psTask) {
232
+ psTask.cancel();
233
+ psTask = void 0;
234
+ }
164
235
  }
165
236
  };
166
237
  if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Monlite } from \"@monlite/core\";\n\nexport interface KVOptions {\n /** Logical namespace so multiple caches can share one database. Default \"default\". */\n namespace?: string;\n /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */\n sweepIntervalMs?: number;\n /** How often (ms) a `subscribe()` listener polls for cross-process messages. Default `200`. */\n pubsubPollMs?: number;\n}\n\n/**\n * A synchronous, Redis-like key-value cache backed by SQLite. Values are any\n * JSON-serializable data; TTLs are in milliseconds.\n */\nexport interface KV {\n get<T = any>(key: string): T | undefined;\n set(key: string, value: any, opts?: { ttl?: number }): void;\n /**\n * Atomically set the key only if it isn't already present (Redis `SET NX`).\n * Returns `true` if set, `false` if a live key already existed. The lock\n * primitive for single-instance schedulers, nonces, and once-only work.\n */\n setNX(key: string, value: any, opts?: { ttl?: number }): boolean;\n has(key: string): boolean;\n delete(key: string): boolean;\n /** Atomically add `by` (default 1) to a numeric key; returns the new value. */\n incr(key: string, by?: number): number;\n decr(key: string, by?: number): number;\n mget<T = any>(keys: string[]): (T | undefined)[];\n /** Keys in this namespace (optionally by prefix), excluding expired ones. */\n keys(prefix?: string): string[];\n /** Set/refresh a key's TTL (ms). Returns false if the key is absent. */\n expire(key: string, ttl: number): boolean;\n /** Remaining TTL in ms; `-1` if no expiry, `-2` if absent (Redis convention). */\n ttl(key: string): number;\n /** Delete all keys in this namespace. */\n flush(): void;\n /** Number of live keys in this namespace. */\n size(): number;\n /**\n * Publish a message to a channel (Redis `PUBLISH`). Delivered to every\n * `subscribe()` listener on that channel — including in OTHER processes\n * sharing this database. Ephemeral: not replayed to late subscribers. Returns\n * the number of listeners on this instance that received it.\n */\n publish(channel: string, message: any): number;\n /**\n * Subscribe to a channel (Redis `SUBSCRIBE`). The callback fires for each\n * message published AFTER this call, cross-process. Returns an unsubscribe.\n */\n subscribe(channel: string, cb: (message: any) => void): () => void;\n /** Stop the sweep + pub/sub timers (if any). */\n stop(): void;\n}\n\nconst ensured = new WeakSet<object>();\n\n/**\n * Create a cache over a monlite database.\n *\n * ```ts\n * const cache = kv(db);\n * cache.set(\"session:42\", { user: \"ali\" }, { ttl: 60_000 });\n * cache.get(\"session:42\"); // { user: \"ali\" } (synchronous)\n * ```\n */\nexport function kv(db: Monlite, options: KVOptions = {}): KV {\n const ns = options.namespace ?? \"default\";\n const driver = db.driver;\n\n if (!ensured.has(db)) {\n driver.exec(\n `CREATE TABLE IF NOT EXISTS _kv (\n ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,\n expires_at INTEGER, PRIMARY KEY (ns, k)\n );\n CREATE TABLE IF NOT EXISTS _monlite_kv_pubsub (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq)`,\n );\n ensured.add(db);\n }\n\n const now = () => Date.now();\n const pubsubPollMs = Math.max(20, options.pubsubPollMs ?? 200);\n const fresh = (row: any): boolean =>\n !!row && !(row.expires_at != null && row.expires_at <= now());\n\n const getRow = (key: string) =>\n driver\n .prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`)\n .get(ns, key) as { v: string; expires_at: number | null } | undefined;\n\n const del = (key: string): boolean =>\n driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key)\n .changes > 0;\n\n const setRaw = (key: string, v: string, expires: number | null) =>\n driver\n .prepare(\n `INSERT INTO _kv (ns, k, v, expires_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`,\n )\n .run(ns, key, v, expires);\n\n let timer: ReturnType<typeof setInterval> | undefined;\n\n // ── pub/sub ──────────────────────────────────────────────────────────────\n type Sub = { channel: string; cb: (m: any) => void; cursor: number };\n const subs = new Set<Sub>();\n let psTimer: ReturnType<typeof setInterval> | undefined;\n\n const psMaxSeq = (): number =>\n (\n driver\n .prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`)\n .get(ns) as { s: number | null }\n ).s ?? 0;\n\n /** Deliver new messages to every local subscriber (cross-process via the table). */\n const drainPubsub = (): void => {\n if (subs.size === 0) return;\n let min = Infinity;\n for (const s of subs) min = Math.min(min, s.cursor);\n const rows = driver\n .prepare(\n `SELECT seq, channel, payload FROM _monlite_kv_pubsub WHERE ns = ? AND seq > ? ORDER BY seq`,\n )\n .all(ns, min) as Array<{ seq: number; channel: string; payload: string }>;\n if (rows.length === 0) return;\n for (const s of [...subs]) {\n for (const r of rows) {\n if (r.seq <= s.cursor) continue;\n s.cursor = r.seq;\n if (r.channel === s.channel) {\n try {\n s.cb(JSON.parse(r.payload ?? \"null\"));\n } catch {\n /* a subscriber callback must not break delivery to others */\n }\n }\n }\n }\n };\n\n const api: KV = {\n get(key) {\n const row = getRow(key);\n if (!fresh(row)) {\n if (row) del(key);\n return undefined;\n }\n return JSON.parse(row!.v);\n },\n set(key, value, opts) {\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n },\n setNX(key, value, opts) {\n // IMMEDIATE: take the write lock up front so two processes racing the same\n // key can't deadlock on lock upgrade — the loser cleanly gets `false`.\n return driver.transaction(() => {\n const row = getRow(key);\n if (fresh(row)) return false; // a live key already exists\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n return true;\n }, true);\n },\n has(key) {\n return api.get(key) !== undefined;\n },\n delete(key) {\n return del(key);\n },\n incr(key, by = 1) {\n return driver.transaction(() => {\n // IMMEDIATE: read-modify-write needs the write lock up front\n const row = getRow(key);\n let n = 0;\n let expires: number | null = null;\n if (fresh(row)) {\n const cur = JSON.parse(row!.v);\n if (typeof cur !== \"number\") {\n throw new Error(`kv.incr: value at \"${key}\" is not a number`);\n }\n n = cur;\n expires = row!.expires_at;\n }\n const next = n + by;\n setRaw(key, JSON.stringify(next), expires);\n return next;\n }, true);\n },\n decr(key, by = 1) {\n return api.incr(key, -by);\n },\n mget(keys) {\n return keys.map((k) => api.get(k));\n },\n keys(prefix) {\n const t = now();\n const rows = (\n prefix !== undefined\n ? driver\n .prepare(\n `SELECT k, expires_at FROM _kv WHERE ns = ? AND k LIKE ? ESCAPE '\\\\'`,\n )\n .all(ns, prefix.replace(/[%_\\\\]/g, \"\\\\$&\") + \"%\")\n : driver.prepare(`SELECT k, expires_at FROM _kv WHERE ns = ?`).all(ns)\n ) as Array<{ k: string; expires_at: number | null }>;\n return rows\n .filter((r) => r.expires_at == null || r.expires_at > t)\n .map((r) => r.k);\n },\n expire(key, ttl) {\n const row = getRow(key);\n if (!fresh(row)) return false;\n driver\n .prepare(`UPDATE _kv SET expires_at = ? WHERE ns = ? AND k = ?`)\n .run(now() + ttl, ns, key);\n return true;\n },\n ttl(key) {\n const row = getRow(key);\n if (!fresh(row)) return -2;\n if (row!.expires_at == null) return -1;\n return row!.expires_at - now();\n },\n flush() {\n driver.prepare(`DELETE FROM _kv WHERE ns = ?`).run(ns);\n },\n size() {\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(ns, now()) as { n: number }\n ).n;\n },\n publish(channel, message) {\n const ts = now();\n driver\n .prepare(\n `INSERT INTO _monlite_kv_pubsub (ns, channel, payload, ts) VALUES (?, ?, ?, ?)`,\n )\n .run(ns, channel, JSON.stringify(message ?? null), ts);\n // Ephemeral: prune old messages (late subscribers don't replay) so the\n // table can't grow unbounded.\n driver\n .prepare(`DELETE FROM _monlite_kv_pubsub WHERE ts < ?`)\n .run(ts - 30_000);\n // Deliver to same-instance subscribers immediately (cross-process listeners\n // pick it up on their next poll).\n drainPubsub();\n let n = 0;\n for (const s of subs) if (s.channel === channel) n++;\n return n;\n },\n subscribe(channel, cb) {\n const sub: Sub = { channel, cb, cursor: psMaxSeq() }; // start at \"now\" — no replay\n subs.add(sub);\n if (!psTimer) {\n psTimer = setInterval(drainPubsub, pubsubPollMs);\n psTimer.unref?.();\n }\n return () => {\n subs.delete(sub);\n if (subs.size === 0 && psTimer) {\n clearInterval(psTimer);\n psTimer = undefined;\n }\n };\n },\n stop() {\n if (timer) clearInterval(timer);\n timer = undefined;\n if (psTimer) clearInterval(psTimer);\n psTimer = undefined;\n },\n };\n\n if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {\n timer = setInterval(() => {\n driver\n .prepare(\n `DELETE FROM _kv WHERE expires_at IS NOT NULL AND expires_at <= ?`,\n )\n .run(now());\n }, options.sweepIntervalMs);\n timer.unref?.();\n }\n\n return api;\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Monlite, HeartbeatTask } from \"@monlite/core\";\n\nexport interface KVOptions {\n /** Logical namespace so multiple caches can share one database. Default \"default\". */\n namespace?: string;\n /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */\n sweepIntervalMs?: number;\n /** How often (ms) a `subscribe()` listener polls for cross-process messages. Default `200`. */\n pubsubPollMs?: number;\n}\n\n/**\n * A synchronous, Redis-like key-value cache backed by SQLite. Values are any\n * JSON-serializable data; TTLs are in milliseconds.\n */\nexport interface KV {\n get<T = any>(key: string): T | undefined;\n set(key: string, value: any, opts?: { ttl?: number }): void;\n /**\n * Atomically set the key only if it isn't already present (Redis `SET NX`).\n * Returns `true` if set, `false` if a live key already existed. The lock\n * primitive for single-instance schedulers, nonces, and once-only work.\n */\n setNX(key: string, value: any, opts?: { ttl?: number }): boolean;\n has(key: string): boolean;\n delete(key: string): boolean;\n /** Atomically add `by` (default 1) to a numeric key; returns the new value. */\n incr(key: string, by?: number): number;\n decr(key: string, by?: number): number;\n mget<T = any>(keys: string[]): (T | undefined)[];\n /** Keys in this namespace (optionally by prefix), excluding expired ones. */\n keys(prefix?: string): string[];\n /** Set/refresh a key's TTL (ms). Returns false if the key is absent. */\n expire(key: string, ttl: number): boolean;\n /** Remaining TTL in ms; `-1` if no expiry, `-2` if absent (Redis convention). */\n ttl(key: string): number;\n /** Delete all keys in this namespace. */\n flush(): void;\n /** Number of live keys in this namespace. */\n size(): number;\n\n // ── sorted sets (Redis ZSET) — leaderboards, rate-limiters, priority indexes ──\n /** Add or update `member` with `score` (Redis `ZADD`). */\n zadd(key: string, score: number, member: string): void;\n /** Increment `member`'s score by `delta` (`ZINCRBY`); returns the new score. */\n zincrby(key: string, delta: number, member: string): number;\n /** A member's score, or `undefined` if absent (`ZSCORE`). */\n zscore(key: string, member: string): number | undefined;\n /** Remove `member` (`ZREM`); returns `true` if it existed. */\n zrem(key: string, member: string): boolean;\n /** Number of members (`ZCARD`). */\n zcard(key: string): number;\n /** 0-based rank by ascending score (ties lexicographic); `rev` for descending. `undefined` if absent. */\n zrank(\n key: string,\n member: string,\n opts?: { rev?: boolean },\n ): number | undefined;\n /**\n * Members by rank range `[start, stop]` inclusive (negative counts from the end,\n * Redis-style), ascending by score (`rev` = descending). With `withScores`,\n * returns `{ member, score }[]` (`ZRANGE` / `ZREVRANGE`).\n */\n zrange(\n key: string,\n start: number,\n stop: number,\n opts?: { rev?: boolean; withScores?: boolean },\n ): string[] | Array<{ member: string; score: number }>;\n /** Members with `min <= score <= max`, ascending (`ZRANGEBYSCORE`). */\n zrangeByScore(\n key: string,\n min: number,\n max: number,\n opts?: { withScores?: boolean },\n ): string[] | Array<{ member: string; score: number }>;\n\n /**\n * Publish a message to a channel (Redis `PUBLISH`). Delivered to every\n * `subscribe()` listener on that channel — including in OTHER processes\n * sharing this database. Ephemeral: not replayed to late subscribers. Returns\n * the number of listeners on this instance that received it.\n */\n publish(channel: string, message: any): number;\n /**\n * Subscribe to a channel (Redis `SUBSCRIBE`). The callback fires for each\n * message published AFTER this call, cross-process. Returns an unsubscribe.\n */\n subscribe(channel: string, cb: (message: any) => void): () => void;\n /** Stop the sweep + pub/sub timers (if any). */\n stop(): void;\n}\n\nconst ensured = new WeakSet<object>();\n\n/**\n * Create a cache over a monlite database.\n *\n * ```ts\n * const cache = kv(db);\n * cache.set(\"session:42\", { user: \"ali\" }, { ttl: 60_000 });\n * cache.get(\"session:42\"); // { user: \"ali\" } (synchronous)\n * ```\n */\nexport function kv(db: Monlite, options: KVOptions = {}): KV {\n const ns = options.namespace ?? \"default\";\n const driver = db.driver;\n\n if (!ensured.has(db)) {\n driver.exec(\n `CREATE TABLE IF NOT EXISTS _kv (\n ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,\n expires_at INTEGER, PRIMARY KEY (ns, k)\n );\n CREATE TABLE IF NOT EXISTS _monlite_kv_pubsub (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq);\n CREATE TABLE IF NOT EXISTS _monlite_kv_zset (\n ns TEXT NOT NULL, k TEXT NOT NULL, member TEXT NOT NULL, score REAL NOT NULL,\n PRIMARY KEY (ns, k, member)\n );\n CREATE INDEX IF NOT EXISTS _idx_kv_zset ON _monlite_kv_zset (ns, k, score, member)`,\n );\n ensured.add(db);\n }\n\n const now = () => Date.now();\n const pubsubPollMs = Math.max(20, options.pubsubPollMs ?? 200);\n const fresh = (row: any): boolean =>\n !!row && !(row.expires_at != null && row.expires_at <= now());\n\n const getRow = (key: string) =>\n driver\n .prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`)\n .get(ns, key) as { v: string; expires_at: number | null } | undefined;\n\n const del = (key: string): boolean =>\n driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key)\n .changes > 0;\n\n const setRaw = (key: string, v: string, expires: number | null) =>\n driver\n .prepare(\n `INSERT INTO _kv (ns, k, v, expires_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`,\n )\n .run(ns, key, v, expires);\n\n let timer: ReturnType<typeof setInterval> | undefined;\n\n // ── pub/sub ──────────────────────────────────────────────────────────────\n type Sub = { channel: string; cb: (m: any) => void; cursor: number };\n const subs = new Set<Sub>();\n let psTask: HeartbeatTask | undefined;\n\n const psMaxSeq = (): number =>\n (\n driver\n .prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`)\n .get(ns) as { s: number | null }\n ).s ?? 0;\n\n /** Deliver new messages to every local subscriber (cross-process via the table). */\n const drainPubsub = (): void => {\n if (subs.size === 0) return;\n let min = Infinity;\n for (const s of subs) min = Math.min(min, s.cursor);\n const rows = driver\n .prepare(\n `SELECT seq, channel, payload FROM _monlite_kv_pubsub WHERE ns = ? AND seq > ? ORDER BY seq`,\n )\n .all(ns, min) as Array<{ seq: number; channel: string; payload: string }>;\n if (rows.length === 0) return;\n for (const s of [...subs]) {\n for (const r of rows) {\n if (r.seq <= s.cursor) continue;\n s.cursor = r.seq;\n if (r.channel === s.channel) {\n try {\n s.cb(JSON.parse(r.payload ?? \"null\"));\n } catch {\n /* a subscriber callback must not break delivery to others */\n }\n }\n }\n }\n };\n\n const api: KV = {\n get(key) {\n const row = getRow(key);\n if (!fresh(row)) {\n if (row) del(key);\n return undefined;\n }\n return JSON.parse(row!.v);\n },\n set(key, value, opts) {\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n },\n setNX(key, value, opts) {\n // IMMEDIATE: take the write lock up front so two processes racing the same\n // key can't deadlock on lock upgrade — the loser cleanly gets `false`.\n return driver.transaction(() => {\n const row = getRow(key);\n if (fresh(row)) return false; // a live key already exists\n const expires = opts?.ttl != null ? now() + opts.ttl : null;\n // `?? null` so `set(key, undefined)` stores JSON null (round-trips to null)\n // instead of binding `undefined` and tripping the NOT NULL constraint.\n setRaw(key, JSON.stringify(value ?? null), expires);\n return true;\n }, true);\n },\n has(key) {\n return api.get(key) !== undefined;\n },\n delete(key) {\n return del(key);\n },\n incr(key, by = 1) {\n return driver.transaction(() => {\n // IMMEDIATE: read-modify-write needs the write lock up front\n const row = getRow(key);\n let n = 0;\n let expires: number | null = null;\n if (fresh(row)) {\n const cur = JSON.parse(row!.v);\n if (typeof cur !== \"number\") {\n throw new Error(`kv.incr: value at \"${key}\" is not a number`);\n }\n n = cur;\n expires = row!.expires_at;\n }\n const next = n + by;\n setRaw(key, JSON.stringify(next), expires);\n return next;\n }, true);\n },\n decr(key, by = 1) {\n return api.incr(key, -by);\n },\n mget(keys) {\n return keys.map((k) => api.get(k));\n },\n keys(prefix) {\n const t = now();\n const rows = (\n prefix !== undefined\n ? driver\n .prepare(\n `SELECT k, expires_at FROM _kv WHERE ns = ? AND k LIKE ? ESCAPE '\\\\'`,\n )\n .all(ns, prefix.replace(/[%_\\\\]/g, \"\\\\$&\") + \"%\")\n : driver.prepare(`SELECT k, expires_at FROM _kv WHERE ns = ?`).all(ns)\n ) as Array<{ k: string; expires_at: number | null }>;\n return rows\n .filter((r) => r.expires_at == null || r.expires_at > t)\n .map((r) => r.k);\n },\n expire(key, ttl) {\n const row = getRow(key);\n if (!fresh(row)) return false;\n driver\n .prepare(`UPDATE _kv SET expires_at = ? WHERE ns = ? AND k = ?`)\n .run(now() + ttl, ns, key);\n return true;\n },\n ttl(key) {\n const row = getRow(key);\n if (!fresh(row)) return -2;\n if (row!.expires_at == null) return -1;\n return row!.expires_at - now();\n },\n flush() {\n driver.prepare(`DELETE FROM _kv WHERE ns = ?`).run(ns);\n },\n size() {\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(ns, now()) as { n: number }\n ).n;\n },\n\n // ── sorted sets ──────────────────────────────────────────────────────────\n zadd(key, score, member) {\n driver\n .prepare(\n `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`,\n )\n .run(ns, key, member, score);\n },\n zincrby(key, delta, member) {\n // IMMEDIATE: atomic read-modify-write across processes.\n return driver.transaction(() => {\n const row = driver\n .prepare(\n `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .get(ns, key, member) as { score: number } | undefined;\n const next = (row?.score ?? 0) + delta;\n driver\n .prepare(\n `INSERT INTO _monlite_kv_zset (ns, k, member, score) VALUES (?, ?, ?, ?)\n ON CONFLICT(ns, k, member) DO UPDATE SET score = excluded.score`,\n )\n .run(ns, key, member, next);\n return next;\n }, true);\n },\n zscore(key, member) {\n const row = driver\n .prepare(\n `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .get(ns, key, member) as { score: number } | undefined;\n return row?.score;\n },\n zrem(key, member) {\n return (\n driver\n .prepare(\n `DELETE FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .run(ns, key, member).changes > 0\n );\n },\n zcard(key) {\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ?`,\n )\n .get(ns, key) as { n: number }\n ).n;\n },\n zrank(key, member, opts) {\n const row = driver\n .prepare(\n `SELECT score FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND member = ?`,\n )\n .get(ns, key, member) as { score: number } | undefined;\n if (!row) return undefined;\n // Members ordered before this one (ties broken lexicographically by member).\n const cmp = opts?.rev\n ? `score > ? OR (score = ? AND member > ?)`\n : `score < ? OR (score = ? AND member < ?)`;\n return (\n driver\n .prepare(\n `SELECT COUNT(*) AS n FROM _monlite_kv_zset WHERE ns = ? AND k = ? AND (${cmp})`,\n )\n .get(ns, key, row.score, row.score, member) as { n: number }\n ).n;\n },\n zrange(key, start, stop, opts) {\n const card = api.zcard(key);\n let lo = start < 0 ? card + start : start;\n let hi = stop < 0 ? card + stop : stop;\n if (lo < 0) lo = 0;\n if (hi >= card) hi = card - 1;\n if (card === 0 || lo > hi) return [];\n const dir = opts?.rev ? \"DESC\" : \"ASC\";\n const rows = driver\n .prepare(\n `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?\n ORDER BY score ${dir}, member ${dir} LIMIT ? OFFSET ?`,\n )\n .all(ns, key, hi - lo + 1, lo) as Array<{\n member: string;\n score: number;\n }>;\n return opts?.withScores ? rows : rows.map((r) => r.member);\n },\n zrangeByScore(key, min, max, opts) {\n const rows = driver\n .prepare(\n `SELECT member, score FROM _monlite_kv_zset WHERE ns = ? AND k = ?\n AND score >= ? AND score <= ? ORDER BY score ASC, member ASC`,\n )\n .all(ns, key, min, max) as Array<{ member: string; score: number }>;\n return opts?.withScores ? rows : rows.map((r) => r.member);\n },\n\n publish(channel, message) {\n const ts = now();\n driver\n .prepare(\n `INSERT INTO _monlite_kv_pubsub (ns, channel, payload, ts) VALUES (?, ?, ?, ?)`,\n )\n .run(ns, channel, JSON.stringify(message ?? null), ts);\n // Ephemeral: prune old messages (late subscribers don't replay) so the\n // table can't grow unbounded.\n driver\n .prepare(`DELETE FROM _monlite_kv_pubsub WHERE ts < ?`)\n .run(ts - 30_000);\n // Deliver to same-instance subscribers immediately (cross-process listeners\n // pick it up on their next poll).\n drainPubsub();\n let n = 0;\n for (const s of subs) if (s.channel === channel) n++;\n return n;\n },\n subscribe(channel, cb) {\n const sub: Sub = { channel, cb, cursor: psMaxSeq() }; // start at \"now\" — no replay\n subs.add(sub);\n // Register the cross-process poll on the shared heartbeat (one timer for the\n // whole db), started on first subscribe and dropped when the last unsubscribes.\n if (!psTask) psTask = db.heartbeat.every(pubsubPollMs, drainPubsub);\n return () => {\n subs.delete(sub);\n if (subs.size === 0 && psTask) {\n psTask.cancel();\n psTask = undefined;\n }\n };\n },\n stop() {\n if (timer) clearInterval(timer);\n timer = undefined;\n if (psTask) {\n psTask.cancel();\n psTask = undefined;\n }\n },\n };\n\n if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {\n timer = setInterval(() => {\n driver\n .prepare(\n `DELETE FROM _kv WHERE expires_at IS NOT NULL AND expires_at <= ?`,\n )\n .run(now());\n }, options.sweepIntervalMs);\n timer.unref?.();\n }\n\n return api;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monlite/kv",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Redis-like local key-value cache for @monlite/core: get/set/incr with TTL, on SQLite.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -49,7 +49,7 @@
49
49
  "node": ">=18"
50
50
  },
51
51
  "dependencies": {
52
- "@monlite/core": "^2.7.0"
52
+ "@monlite/core": "^2.8.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.10.0",