@monlite/kv 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -10,11 +10,17 @@ function kv(db, options = {}) {
10
10
  `CREATE TABLE IF NOT EXISTS _kv (
11
11
  ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,
12
12
  expires_at INTEGER, PRIMARY KEY (ns, k)
13
- )`
13
+ );
14
+ CREATE TABLE IF NOT EXISTS _monlite_kv_pubsub (
15
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL
17
+ );
18
+ CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq)`
14
19
  );
15
20
  ensured.add(db);
16
21
  }
17
22
  const now = () => Date.now();
23
+ const pubsubPollMs = Math.max(20, options.pubsubPollMs ?? 200);
18
24
  const fresh = (row) => !!row && !(row.expires_at != null && row.expires_at <= now());
19
25
  const getRow = (key) => driver.prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`).get(ns, key);
20
26
  const del = (key) => driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key).changes > 0;
@@ -23,6 +29,30 @@ function kv(db, options = {}) {
23
29
  ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`
24
30
  ).run(ns, key, v, expires);
25
31
  let timer;
32
+ const subs = /* @__PURE__ */ new Set();
33
+ let psTimer;
34
+ const psMaxSeq = () => driver.prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`).get(ns).s ?? 0;
35
+ const drainPubsub = () => {
36
+ if (subs.size === 0) return;
37
+ let min = Infinity;
38
+ for (const s of subs) min = Math.min(min, s.cursor);
39
+ const rows = driver.prepare(
40
+ `SELECT seq, channel, payload FROM _monlite_kv_pubsub WHERE ns = ? AND seq > ? ORDER BY seq`
41
+ ).all(ns, min);
42
+ if (rows.length === 0) return;
43
+ for (const s of [...subs]) {
44
+ for (const r of rows) {
45
+ if (r.seq <= s.cursor) continue;
46
+ s.cursor = r.seq;
47
+ if (r.channel === s.channel) {
48
+ try {
49
+ s.cb(JSON.parse(r.payload ?? "null"));
50
+ } catch {
51
+ }
52
+ }
53
+ }
54
+ }
55
+ };
26
56
  const api = {
27
57
  get(key) {
28
58
  const row = getRow(key);
@@ -102,9 +132,37 @@ function kv(db, options = {}) {
102
132
  `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`
103
133
  ).get(ns, now()).n;
104
134
  },
135
+ publish(channel, message) {
136
+ const ts = now();
137
+ driver.prepare(
138
+ `INSERT INTO _monlite_kv_pubsub (ns, channel, payload, ts) VALUES (?, ?, ?, ?)`
139
+ ).run(ns, channel, JSON.stringify(message ?? null), ts);
140
+ driver.prepare(`DELETE FROM _monlite_kv_pubsub WHERE ts < ?`).run(ts - 3e4);
141
+ drainPubsub();
142
+ let n = 0;
143
+ for (const s of subs) if (s.channel === channel) n++;
144
+ return n;
145
+ },
146
+ subscribe(channel, cb) {
147
+ const sub = { channel, cb, cursor: psMaxSeq() };
148
+ subs.add(sub);
149
+ if (!psTimer) {
150
+ psTimer = setInterval(drainPubsub, pubsubPollMs);
151
+ psTimer.unref?.();
152
+ }
153
+ return () => {
154
+ subs.delete(sub);
155
+ if (subs.size === 0 && psTimer) {
156
+ clearInterval(psTimer);
157
+ psTimer = void 0;
158
+ }
159
+ };
160
+ },
105
161
  stop() {
106
162
  if (timer) clearInterval(timer);
107
163
  timer = void 0;
164
+ if (psTimer) clearInterval(psTimer);
165
+ psTimer = void 0;
108
166
  }
109
167
  };
110
168
  if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AA0CA,IAAM,OAAA,uBAAc,OAAA,EAAgB;AAW7B,SAAS,EAAA,CAAG,EAAA,EAAa,OAAA,GAAqB,EAAC,EAAO;AAC3D,EAAA,MAAM,EAAA,GAAK,QAAQ,SAAA,IAAa,SAAA;AAChC,EAAA,MAAM,SAAS,EAAA,CAAG,MAAA;AAElB,EAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,EAAE,CAAA,EAAG;AACpB,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,CAAA;AAAA;AAAA;AAAA,OAAA;AAAA,KAIF;AACA,IAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAAA,EAChB;AAEA,EAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,GAAA,EAAI;AAC3B,EAAA,MAAM,KAAA,GAAQ,CAAC,GAAA,KACb,CAAC,CAAC,GAAA,IAAO,EAAE,GAAA,CAAI,UAAA,IAAc,IAAA,IAAQ,GAAA,CAAI,UAAA,IAAc,GAAA,EAAI,CAAA;AAE7D,EAAA,MAAM,MAAA,GAAS,CAAC,GAAA,KACd,MAAA,CACG,QAAQ,CAAA,oDAAA,CAAsD,CAAA,CAC9D,GAAA,CAAI,EAAA,EAAI,GAAG,CAAA;AAEhB,EAAA,MAAM,GAAA,GAAM,CAAC,GAAA,KACX,MAAA,CAAO,OAAA,CAAQ,CAAA,sCAAA,CAAwC,CAAA,CAAE,GAAA,CAAI,EAAA,EAAI,GAAG,CAAA,CACjE,OAAA,GAAU,CAAA;AAEf,EAAA,MAAM,MAAA,GAAS,CAAC,GAAA,EAAa,CAAA,EAAW,YACtC,MAAA,CACG,OAAA;AAAA,IACC,CAAA;AAAA,0FAAA;AAAA,GAEF,CACC,GAAA,CAAI,EAAA,EAAI,GAAA,EAAK,GAAG,OAAO,CAAA;AAE5B,EAAA,IAAI,KAAA;AAEJ,EAAA,MAAM,GAAA,GAAU;AAAA,IACd,IAAI,GAAA,EAAK;AACP,MAAA,MAAM,GAAA,GAAM,OAAO,GAAG,CAAA;AACtB,MAAA,IAAI,CAAC,KAAA,CAAM,GAAG,CAAA,EAAG;AACf,QAAA,IAAI,GAAA,MAAS,GAAG,CAAA;AAChB,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAK,CAAC,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,GAAA,CAAI,GAAA,EAAK,KAAA,EAAO,IAAA,EAAM;AACpB,MAAA,MAAM,UAAU,IAAA,EAAM,GAAA,IAAO,OAAO,GAAA,EAAI,GAAI,KAAK,GAAA,GAAM,IAAA;AAGvD,MAAA,MAAA,CAAO,KAAK,IAAA,CAAK,SAAA,CAAU,KAAA,IAAS,IAAI,GAAG,OAAO,CAAA;AAAA,IACpD,CAAA;AAAA,IACA,KAAA,CAAM,GAAA,EAAK,KAAA,EAAO,IAAA,EAAM;AAGtB,MAAA,OAAO,MAAA,CAAO,YAAY,MAAM;AAC9B,QAAA,MAAM,GAAA,GAAM,OAAO,GAAG,CAAA;AACtB,QAAA,IAAI,KAAA,CAAM,GAAG,CAAA,EAAG,OAAO,KAAA;AACvB,QAAA,MAAM,UAAU,IAAA,EAAM,GAAA,IAAO,OAAO,GAAA,EAAI,GAAI,KAAK,GAAA,GAAM,IAAA;AAGvD,QAAA,MAAA,CAAO,KAAK,IAAA,CAAK,SAAA,CAAU,KAAA,IAAS,IAAI,GAAG,OAAO,CAAA;AAClD,QAAA,OAAO,IAAA;AAAA,MACT,GAAG,IAAI,CAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,GAAA,EAAK;AACP,MAAA,OAAO,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,KAAM,MAAA;AAAA,IAC1B,CAAA;AAAA,IACA,OAAO,GAAA,EAAK;AACV,MAAA,OAAO,IAAI,GAAG,CAAA;AAAA,IAChB,CAAA;AAAA,IACA,IAAA,CAAK,GAAA,EAAK,EAAA,GAAK,CAAA,EAAG;AAChB,MAAA,OAAO,MAAA,CAAO,YAAY,MAAM;AAE9B,QAAA,MAAM,GAAA,GAAM,OAAO,GAAG,CAAA;AACtB,QAAA,IAAI,CAAA,GAAI,CAAA;AACR,QAAA,IAAI,OAAA,GAAyB,IAAA;AAC7B,QAAA,IAAI,KAAA,CAAM,GAAG,CAAA,EAAG;AACd,UAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAK,CAAC,CAAA;AAC7B,UAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC3B,YAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,GAAG,CAAA,iBAAA,CAAmB,CAAA;AAAA,UAC9D;AACA,UAAA,CAAA,GAAI,GAAA;AACJ,UAAA,OAAA,GAAU,GAAA,CAAK,UAAA;AAAA,QACjB;AACA,QAAA,MAAM,OAAO,CAAA,GAAI,EAAA;AACjB,QAAA,MAAA,CAAO,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,IAAI,GAAG,OAAO,CAAA;AACzC,QAAA,OAAO,IAAA;AAAA,MACT,GAAG,IAAI,CAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAA,CAAK,GAAA,EAAK,EAAA,GAAK,CAAA,EAAG;AAChB,MAAA,OAAO,GAAA,CAAI,IAAA,CAAK,GAAA,EAAK,CAAC,EAAE,CAAA;AAAA,IAC1B,CAAA;AAAA,IACA,KAAK,IAAA,EAAM;AACT,MAAA,OAAO,KAAK,GAAA,CAAI,CAAC,MAAM,GAAA,CAAI,GAAA,CAAI,CAAC,CAAC,CAAA;AAAA,IACnC,CAAA;AAAA,IACA,KAAK,MAAA,EAAQ;AACX,MAAA,MAAM,IAAI,GAAA,EAAI;AACd,MAAA,MAAM,IAAA,GACJ,MAAA,KAAW,MAAA,GACP,MAAA,CACG,OAAA;AAAA,QACC,CAAA,mEAAA;AAAA,OACF,CACC,GAAA,CAAI,EAAA,EAAI,MAAA,CAAO,QAAQ,SAAA,EAAW,MAAM,CAAA,GAAI,GAAG,IAClD,MAAA,CAAO,OAAA,CAAQ,CAAA,0CAAA,CAA4C,CAAA,CAAE,IAAI,EAAE,CAAA;AAEzE,MAAA,OAAO,IAAA,CACJ,MAAA,CAAO,CAAC,CAAA,KAAM,EAAE,UAAA,IAAc,IAAA,IAAQ,CAAA,CAAE,UAAA,GAAa,CAAC,CAAA,CACtD,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,CAAC,CAAA;AAAA,IACnB,CAAA;AAAA,IACA,MAAA,CAAO,KAAK,GAAA,EAAK;AACf,MAAA,MAAM,GAAA,GAAM,OAAO,GAAG,CAAA;AACtB,MAAA,IAAI,CAAC,KAAA,CAAM,GAAG,CAAA,EAAG,OAAO,KAAA;AACxB,MAAA,MAAA,CACG,OAAA,CAAQ,sDAAsD,CAAA,CAC9D,GAAA,CAAI,KAAI,GAAI,GAAA,EAAK,IAAI,GAAG,CAAA;AAC3B,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAAA,IACA,IAAI,GAAA,EAAK;AACP,MAAA,MAAM,GAAA,GAAM,OAAO,GAAG,CAAA;AACtB,MAAA,IAAI,CAAC,KAAA,CAAM,GAAG,CAAA,EAAG,OAAO,EAAA;AACxB,MAAA,IAAI,GAAA,CAAK,UAAA,IAAc,IAAA,EAAM,OAAO,EAAA;AACpC,MAAA,OAAO,GAAA,CAAK,aAAa,GAAA,EAAI;AAAA,IAC/B,CAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,MAAA,CAAO,OAAA,CAAQ,CAAA,4BAAA,CAA8B,CAAA,CAAE,GAAA,CAAI,EAAE,CAAA;AAAA,IACvD,CAAA;AAAA,IACA,IAAA,GAAO;AACL,MAAA,OACE,MAAA,CACG,OAAA;AAAA,QACC,CAAA,qFAAA;AAAA,OACF,CACC,GAAA,CAAI,EAAA,EAAI,GAAA,EAAK,CAAA,CAChB,CAAA;AAAA,IACJ,CAAA;AAAA,IACA,IAAA,GAAO;AACL,MAAA,IAAI,KAAA,gBAAqB,KAAK,CAAA;AAC9B,MAAA,KAAA,GAAQ,MAAA;AAAA,IACV;AAAA,GACF;AAEA,EAAA,IAAI,OAAA,CAAQ,eAAA,IAAmB,OAAA,CAAQ,eAAA,GAAkB,CAAA,EAAG;AAC1D,IAAA,KAAA,GAAQ,YAAY,MAAM;AACxB,MAAA,MAAA,CACG,OAAA;AAAA,QACC,CAAA,gEAAA;AAAA,OACF,CACC,GAAA,CAAI,GAAA,EAAK,CAAA;AAAA,IACd,CAAA,EAAG,QAAQ,eAAe,CAAA;AAC1B,IAAA,KAAA,CAAM,KAAA,IAAQ;AAAA,EAChB;AAEA,EAAA,OAAO,GAAA;AACT","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}\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 /** Stop the sweep timer (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 );\n ensured.add(db);\n }\n\n const now = () => Date.now();\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 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 stop() {\n if (timer) clearInterval(timer);\n timer = 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 } 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"]}
package/dist/index.d.cts CHANGED
@@ -5,6 +5,8 @@ interface KVOptions {
5
5
  namespace?: string;
6
6
  /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */
7
7
  sweepIntervalMs?: number;
8
+ /** How often (ms) a `subscribe()` listener polls for cross-process messages. Default `200`. */
9
+ pubsubPollMs?: number;
8
10
  }
9
11
  /**
10
12
  * A synchronous, Redis-like key-value cache backed by SQLite. Values are any
@@ -39,7 +41,19 @@ interface KV {
39
41
  flush(): void;
40
42
  /** Number of live keys in this namespace. */
41
43
  size(): number;
42
- /** Stop the sweep timer (if any). */
44
+ /**
45
+ * Publish a message to a channel (Redis `PUBLISH`). Delivered to every
46
+ * `subscribe()` listener on that channel — including in OTHER processes
47
+ * sharing this database. Ephemeral: not replayed to late subscribers. Returns
48
+ * the number of listeners on this instance that received it.
49
+ */
50
+ publish(channel: string, message: any): number;
51
+ /**
52
+ * Subscribe to a channel (Redis `SUBSCRIBE`). The callback fires for each
53
+ * message published AFTER this call, cross-process. Returns an unsubscribe.
54
+ */
55
+ subscribe(channel: string, cb: (message: any) => void): () => void;
56
+ /** Stop the sweep + pub/sub timers (if any). */
43
57
  stop(): void;
44
58
  }
45
59
  /**
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ interface KVOptions {
5
5
  namespace?: string;
6
6
  /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */
7
7
  sweepIntervalMs?: number;
8
+ /** How often (ms) a `subscribe()` listener polls for cross-process messages. Default `200`. */
9
+ pubsubPollMs?: number;
8
10
  }
9
11
  /**
10
12
  * A synchronous, Redis-like key-value cache backed by SQLite. Values are any
@@ -39,7 +41,19 @@ interface KV {
39
41
  flush(): void;
40
42
  /** Number of live keys in this namespace. */
41
43
  size(): number;
42
- /** Stop the sweep timer (if any). */
44
+ /**
45
+ * Publish a message to a channel (Redis `PUBLISH`). Delivered to every
46
+ * `subscribe()` listener on that channel — including in OTHER processes
47
+ * sharing this database. Ephemeral: not replayed to late subscribers. Returns
48
+ * the number of listeners on this instance that received it.
49
+ */
50
+ publish(channel: string, message: any): number;
51
+ /**
52
+ * Subscribe to a channel (Redis `SUBSCRIBE`). The callback fires for each
53
+ * message published AFTER this call, cross-process. Returns an unsubscribe.
54
+ */
55
+ subscribe(channel: string, cb: (message: any) => void): () => void;
56
+ /** Stop the sweep + pub/sub timers (if any). */
43
57
  stop(): void;
44
58
  }
45
59
  /**
package/dist/index.js CHANGED
@@ -8,11 +8,17 @@ function kv(db, options = {}) {
8
8
  `CREATE TABLE IF NOT EXISTS _kv (
9
9
  ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,
10
10
  expires_at INTEGER, PRIMARY KEY (ns, k)
11
- )`
11
+ );
12
+ CREATE TABLE IF NOT EXISTS _monlite_kv_pubsub (
13
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ ns TEXT NOT NULL, channel TEXT NOT NULL, payload TEXT, ts INTEGER NOT NULL
15
+ );
16
+ CREATE INDEX IF NOT EXISTS _idx_kv_pubsub ON _monlite_kv_pubsub (ns, seq)`
12
17
  );
13
18
  ensured.add(db);
14
19
  }
15
20
  const now = () => Date.now();
21
+ const pubsubPollMs = Math.max(20, options.pubsubPollMs ?? 200);
16
22
  const fresh = (row) => !!row && !(row.expires_at != null && row.expires_at <= now());
17
23
  const getRow = (key) => driver.prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`).get(ns, key);
18
24
  const del = (key) => driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key).changes > 0;
@@ -21,6 +27,30 @@ function kv(db, options = {}) {
21
27
  ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`
22
28
  ).run(ns, key, v, expires);
23
29
  let timer;
30
+ const subs = /* @__PURE__ */ new Set();
31
+ let psTimer;
32
+ const psMaxSeq = () => driver.prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`).get(ns).s ?? 0;
33
+ const drainPubsub = () => {
34
+ if (subs.size === 0) return;
35
+ let min = Infinity;
36
+ for (const s of subs) min = Math.min(min, s.cursor);
37
+ const rows = driver.prepare(
38
+ `SELECT seq, channel, payload FROM _monlite_kv_pubsub WHERE ns = ? AND seq > ? ORDER BY seq`
39
+ ).all(ns, min);
40
+ if (rows.length === 0) return;
41
+ for (const s of [...subs]) {
42
+ for (const r of rows) {
43
+ if (r.seq <= s.cursor) continue;
44
+ s.cursor = r.seq;
45
+ if (r.channel === s.channel) {
46
+ try {
47
+ s.cb(JSON.parse(r.payload ?? "null"));
48
+ } catch {
49
+ }
50
+ }
51
+ }
52
+ }
53
+ };
24
54
  const api = {
25
55
  get(key) {
26
56
  const row = getRow(key);
@@ -100,9 +130,37 @@ function kv(db, options = {}) {
100
130
  `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`
101
131
  ).get(ns, now()).n;
102
132
  },
133
+ publish(channel, message) {
134
+ const ts = now();
135
+ driver.prepare(
136
+ `INSERT INTO _monlite_kv_pubsub (ns, channel, payload, ts) VALUES (?, ?, ?, ?)`
137
+ ).run(ns, channel, JSON.stringify(message ?? null), ts);
138
+ driver.prepare(`DELETE FROM _monlite_kv_pubsub WHERE ts < ?`).run(ts - 3e4);
139
+ drainPubsub();
140
+ let n = 0;
141
+ for (const s of subs) if (s.channel === channel) n++;
142
+ return n;
143
+ },
144
+ subscribe(channel, cb) {
145
+ const sub = { channel, cb, cursor: psMaxSeq() };
146
+ subs.add(sub);
147
+ if (!psTimer) {
148
+ psTimer = setInterval(drainPubsub, pubsubPollMs);
149
+ psTimer.unref?.();
150
+ }
151
+ return () => {
152
+ subs.delete(sub);
153
+ if (subs.size === 0 && psTimer) {
154
+ clearInterval(psTimer);
155
+ psTimer = void 0;
156
+ }
157
+ };
158
+ },
103
159
  stop() {
104
160
  if (timer) clearInterval(timer);
105
161
  timer = void 0;
162
+ if (psTimer) clearInterval(psTimer);
163
+ psTimer = void 0;
106
164
  }
107
165
  };
108
166
  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}\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 /** Stop the sweep timer (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 );\n ensured.add(db);\n }\n\n const now = () => Date.now();\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 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 stop() {\n if (timer) clearInterval(timer);\n timer = 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 } 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monlite/kv",
3
- "version": "0.2.2",
3
+ "version": "0.3.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.6.13"
52
+ "@monlite/core": "^2.7.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^22.10.0",