@monlite/kv 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emad Jumaah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @monlite/kv
2
+
3
+ A **Redis-like key-value cache** for [`@monlite/core`](https://www.npmjs.com/package/@monlite/core), backed by SQLite. Synchronous `get/set/incr` with TTLs — part of the [local AI-agent harness](https://github.com/qataruts/monlite#readme) (cache + queue + cron, replacing Redis locally).
4
+
5
+ ```bash
6
+ npm install @monlite/core @monlite/kv
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```ts
12
+ import { createDb } from "@monlite/core";
13
+ import { kv } from "@monlite/kv";
14
+
15
+ const db = createDb("app.db");
16
+ const cache = kv(db);
17
+
18
+ cache.set("session:42", { user: "ali" }, { ttl: 60_000 }); // expires in 60s
19
+ cache.get("session:42"); // { user: "ali" } — synchronous, no await
20
+ cache.incr("hits"); // 1
21
+ cache.incr("hits", 5); // 6
22
+ cache.ttl("session:42"); // ~60000 (ms remaining)
23
+ ```
24
+
25
+ It's **synchronous** (local SQLite — no network, no await), **durable** (survives
26
+ restarts), and persisted in your app's database. Use a separate `:memory:` db if
27
+ you want a purely ephemeral cache.
28
+
29
+ ## API
30
+
31
+ | Method | Description |
32
+ | --- | --- |
33
+ | `get(key)` | Value, or `undefined` (also if expired). |
34
+ | `set(key, value, { ttl })` | Store any JSON value; `ttl` in ms (optional). |
35
+ | `has(key)` / `delete(key)` | Existence / removal. |
36
+ | `incr(key, by?)` / `decr(key, by?)` | Atomic numeric update; returns the new value. |
37
+ | `mget(keys)` | Array of values (`undefined` per missing key). |
38
+ | `keys(prefix?)` | Live keys in the namespace, optionally by prefix. |
39
+ | `expire(key, ttl)` | Set/refresh TTL (ms); `false` if absent. |
40
+ | `ttl(key)` | Remaining ms; `-1` no expiry, `-2` absent (Redis convention). |
41
+ | `size()` / `flush()` | Count / clear the namespace. |
42
+
43
+ ## Options
44
+
45
+ ```ts
46
+ kv(db, {
47
+ namespace: "sessions", // isolate multiple caches in one db (default "default")
48
+ sweepIntervalMs: 60_000, // periodically purge expired keys (default: lazy-only)
49
+ });
50
+ ```
51
+
52
+ Expired keys are removed lazily on read; set `sweepIntervalMs` to also purge them
53
+ on a timer. Call `cache.stop()` to clear the sweep timer.
54
+
55
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var ensured = /* @__PURE__ */ new WeakSet();
5
+ function kv(db, options = {}) {
6
+ const ns = options.namespace ?? "default";
7
+ const driver = db.driver;
8
+ if (!ensured.has(db)) {
9
+ driver.exec(
10
+ `CREATE TABLE IF NOT EXISTS _kv (
11
+ ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,
12
+ expires_at INTEGER, PRIMARY KEY (ns, k)
13
+ )`
14
+ );
15
+ ensured.add(db);
16
+ }
17
+ const now = () => Date.now();
18
+ const fresh = (row) => !!row && !(row.expires_at != null && row.expires_at <= now());
19
+ const getRow = (key) => driver.prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`).get(ns, key);
20
+ const del = (key) => driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key).changes > 0;
21
+ const setRaw = (key, v, expires) => driver.prepare(
22
+ `INSERT INTO _kv (ns, k, v, expires_at) VALUES (?, ?, ?, ?)
23
+ ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`
24
+ ).run(ns, key, v, expires);
25
+ let timer;
26
+ const api = {
27
+ get(key) {
28
+ const row = getRow(key);
29
+ if (!fresh(row)) {
30
+ if (row) del(key);
31
+ return void 0;
32
+ }
33
+ return JSON.parse(row.v);
34
+ },
35
+ set(key, value, opts) {
36
+ const expires = opts?.ttl != null ? now() + opts.ttl : null;
37
+ setRaw(key, JSON.stringify(value), expires);
38
+ },
39
+ has(key) {
40
+ return api.get(key) !== void 0;
41
+ },
42
+ delete(key) {
43
+ return del(key);
44
+ },
45
+ incr(key, by = 1) {
46
+ return driver.transaction(() => {
47
+ const row = getRow(key);
48
+ let n = 0;
49
+ let expires = null;
50
+ if (fresh(row)) {
51
+ const cur = JSON.parse(row.v);
52
+ if (typeof cur !== "number") {
53
+ throw new Error(`kv.incr: value at "${key}" is not a number`);
54
+ }
55
+ n = cur;
56
+ expires = row.expires_at;
57
+ }
58
+ const next = n + by;
59
+ setRaw(key, JSON.stringify(next), expires);
60
+ return next;
61
+ });
62
+ },
63
+ decr(key, by = 1) {
64
+ return api.incr(key, -by);
65
+ },
66
+ mget(keys) {
67
+ return keys.map((k) => api.get(k));
68
+ },
69
+ keys(prefix) {
70
+ const t = now();
71
+ const rows = prefix !== void 0 ? driver.prepare(
72
+ `SELECT k, expires_at FROM _kv WHERE ns = ? AND k LIKE ? ESCAPE '\\'`
73
+ ).all(ns, prefix.replace(/[%_\\]/g, "\\$&") + "%") : driver.prepare(`SELECT k, expires_at FROM _kv WHERE ns = ?`).all(ns);
74
+ return rows.filter((r) => r.expires_at == null || r.expires_at > t).map((r) => r.k);
75
+ },
76
+ expire(key, ttl) {
77
+ const row = getRow(key);
78
+ if (!fresh(row)) return false;
79
+ driver.prepare(`UPDATE _kv SET expires_at = ? WHERE ns = ? AND k = ?`).run(now() + ttl, ns, key);
80
+ return true;
81
+ },
82
+ ttl(key) {
83
+ const row = getRow(key);
84
+ if (!fresh(row)) return -2;
85
+ if (row.expires_at == null) return -1;
86
+ return row.expires_at - now();
87
+ },
88
+ flush() {
89
+ driver.prepare(`DELETE FROM _kv WHERE ns = ?`).run(ns);
90
+ },
91
+ size() {
92
+ return driver.prepare(
93
+ `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`
94
+ ).get(ns, now()).n;
95
+ },
96
+ stop() {
97
+ if (timer) clearInterval(timer);
98
+ timer = void 0;
99
+ }
100
+ };
101
+ if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {
102
+ timer = setInterval(() => {
103
+ driver.prepare(
104
+ `DELETE FROM _kv WHERE expires_at IS NOT NULL AND expires_at <= ?`
105
+ ).run(now());
106
+ }, options.sweepIntervalMs);
107
+ timer.unref?.();
108
+ }
109
+ return api;
110
+ }
111
+
112
+ exports.kv = kv;
113
+ //# sourceMappingURL=index.cjs.map
114
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAoCA,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;AACvD,MAAA,MAAA,CAAO,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,KAAK,GAAG,OAAO,CAAA;AAAA,IAC5C,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;AAC9B,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,CAAC,CAAA;AAAA,IACH,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 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 setRaw(key, JSON.stringify(value), expires);\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 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 });\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"]}
@@ -0,0 +1,48 @@
1
+ import { Monlite } from '@monlite/core';
2
+
3
+ interface KVOptions {
4
+ /** Logical namespace so multiple caches can share one database. Default "default". */
5
+ namespace?: string;
6
+ /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */
7
+ sweepIntervalMs?: number;
8
+ }
9
+ /**
10
+ * A synchronous, Redis-like key-value cache backed by SQLite. Values are any
11
+ * JSON-serializable data; TTLs are in milliseconds.
12
+ */
13
+ interface KV {
14
+ get<T = any>(key: string): T | undefined;
15
+ set(key: string, value: any, opts?: {
16
+ ttl?: number;
17
+ }): void;
18
+ has(key: string): boolean;
19
+ delete(key: string): boolean;
20
+ /** Atomically add `by` (default 1) to a numeric key; returns the new value. */
21
+ incr(key: string, by?: number): number;
22
+ decr(key: string, by?: number): number;
23
+ mget<T = any>(keys: string[]): (T | undefined)[];
24
+ /** Keys in this namespace (optionally by prefix), excluding expired ones. */
25
+ keys(prefix?: string): string[];
26
+ /** Set/refresh a key's TTL (ms). Returns false if the key is absent. */
27
+ expire(key: string, ttl: number): boolean;
28
+ /** Remaining TTL in ms; `-1` if no expiry, `-2` if absent (Redis convention). */
29
+ ttl(key: string): number;
30
+ /** Delete all keys in this namespace. */
31
+ flush(): void;
32
+ /** Number of live keys in this namespace. */
33
+ size(): number;
34
+ /** Stop the sweep timer (if any). */
35
+ stop(): void;
36
+ }
37
+ /**
38
+ * Create a cache over a monlite database.
39
+ *
40
+ * ```ts
41
+ * const cache = kv(db);
42
+ * cache.set("session:42", { user: "ali" }, { ttl: 60_000 });
43
+ * cache.get("session:42"); // { user: "ali" } (synchronous)
44
+ * ```
45
+ */
46
+ declare function kv(db: Monlite, options?: KVOptions): KV;
47
+
48
+ export { type KV, type KVOptions, kv };
@@ -0,0 +1,48 @@
1
+ import { Monlite } from '@monlite/core';
2
+
3
+ interface KVOptions {
4
+ /** Logical namespace so multiple caches can share one database. Default "default". */
5
+ namespace?: string;
6
+ /** If set, a timer periodically purges expired keys (ms). Default: lazy-only. */
7
+ sweepIntervalMs?: number;
8
+ }
9
+ /**
10
+ * A synchronous, Redis-like key-value cache backed by SQLite. Values are any
11
+ * JSON-serializable data; TTLs are in milliseconds.
12
+ */
13
+ interface KV {
14
+ get<T = any>(key: string): T | undefined;
15
+ set(key: string, value: any, opts?: {
16
+ ttl?: number;
17
+ }): void;
18
+ has(key: string): boolean;
19
+ delete(key: string): boolean;
20
+ /** Atomically add `by` (default 1) to a numeric key; returns the new value. */
21
+ incr(key: string, by?: number): number;
22
+ decr(key: string, by?: number): number;
23
+ mget<T = any>(keys: string[]): (T | undefined)[];
24
+ /** Keys in this namespace (optionally by prefix), excluding expired ones. */
25
+ keys(prefix?: string): string[];
26
+ /** Set/refresh a key's TTL (ms). Returns false if the key is absent. */
27
+ expire(key: string, ttl: number): boolean;
28
+ /** Remaining TTL in ms; `-1` if no expiry, `-2` if absent (Redis convention). */
29
+ ttl(key: string): number;
30
+ /** Delete all keys in this namespace. */
31
+ flush(): void;
32
+ /** Number of live keys in this namespace. */
33
+ size(): number;
34
+ /** Stop the sweep timer (if any). */
35
+ stop(): void;
36
+ }
37
+ /**
38
+ * Create a cache over a monlite database.
39
+ *
40
+ * ```ts
41
+ * const cache = kv(db);
42
+ * cache.set("session:42", { user: "ali" }, { ttl: 60_000 });
43
+ * cache.get("session:42"); // { user: "ali" } (synchronous)
44
+ * ```
45
+ */
46
+ declare function kv(db: Monlite, options?: KVOptions): KV;
47
+
48
+ export { type KV, type KVOptions, kv };
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ // src/index.ts
2
+ var ensured = /* @__PURE__ */ new WeakSet();
3
+ function kv(db, options = {}) {
4
+ const ns = options.namespace ?? "default";
5
+ const driver = db.driver;
6
+ if (!ensured.has(db)) {
7
+ driver.exec(
8
+ `CREATE TABLE IF NOT EXISTS _kv (
9
+ ns TEXT NOT NULL, k TEXT NOT NULL, v TEXT NOT NULL,
10
+ expires_at INTEGER, PRIMARY KEY (ns, k)
11
+ )`
12
+ );
13
+ ensured.add(db);
14
+ }
15
+ const now = () => Date.now();
16
+ const fresh = (row) => !!row && !(row.expires_at != null && row.expires_at <= now());
17
+ const getRow = (key) => driver.prepare(`SELECT v, expires_at FROM _kv WHERE ns = ? AND k = ?`).get(ns, key);
18
+ const del = (key) => driver.prepare(`DELETE FROM _kv WHERE ns = ? AND k = ?`).run(ns, key).changes > 0;
19
+ const setRaw = (key, v, expires) => driver.prepare(
20
+ `INSERT INTO _kv (ns, k, v, expires_at) VALUES (?, ?, ?, ?)
21
+ ON CONFLICT(ns, k) DO UPDATE SET v = excluded.v, expires_at = excluded.expires_at`
22
+ ).run(ns, key, v, expires);
23
+ let timer;
24
+ const api = {
25
+ get(key) {
26
+ const row = getRow(key);
27
+ if (!fresh(row)) {
28
+ if (row) del(key);
29
+ return void 0;
30
+ }
31
+ return JSON.parse(row.v);
32
+ },
33
+ set(key, value, opts) {
34
+ const expires = opts?.ttl != null ? now() + opts.ttl : null;
35
+ setRaw(key, JSON.stringify(value), expires);
36
+ },
37
+ has(key) {
38
+ return api.get(key) !== void 0;
39
+ },
40
+ delete(key) {
41
+ return del(key);
42
+ },
43
+ incr(key, by = 1) {
44
+ return driver.transaction(() => {
45
+ const row = getRow(key);
46
+ let n = 0;
47
+ let expires = null;
48
+ if (fresh(row)) {
49
+ const cur = JSON.parse(row.v);
50
+ if (typeof cur !== "number") {
51
+ throw new Error(`kv.incr: value at "${key}" is not a number`);
52
+ }
53
+ n = cur;
54
+ expires = row.expires_at;
55
+ }
56
+ const next = n + by;
57
+ setRaw(key, JSON.stringify(next), expires);
58
+ return next;
59
+ });
60
+ },
61
+ decr(key, by = 1) {
62
+ return api.incr(key, -by);
63
+ },
64
+ mget(keys) {
65
+ return keys.map((k) => api.get(k));
66
+ },
67
+ keys(prefix) {
68
+ const t = now();
69
+ const rows = prefix !== void 0 ? driver.prepare(
70
+ `SELECT k, expires_at FROM _kv WHERE ns = ? AND k LIKE ? ESCAPE '\\'`
71
+ ).all(ns, prefix.replace(/[%_\\]/g, "\\$&") + "%") : driver.prepare(`SELECT k, expires_at FROM _kv WHERE ns = ?`).all(ns);
72
+ return rows.filter((r) => r.expires_at == null || r.expires_at > t).map((r) => r.k);
73
+ },
74
+ expire(key, ttl) {
75
+ const row = getRow(key);
76
+ if (!fresh(row)) return false;
77
+ driver.prepare(`UPDATE _kv SET expires_at = ? WHERE ns = ? AND k = ?`).run(now() + ttl, ns, key);
78
+ return true;
79
+ },
80
+ ttl(key) {
81
+ const row = getRow(key);
82
+ if (!fresh(row)) return -2;
83
+ if (row.expires_at == null) return -1;
84
+ return row.expires_at - now();
85
+ },
86
+ flush() {
87
+ driver.prepare(`DELETE FROM _kv WHERE ns = ?`).run(ns);
88
+ },
89
+ size() {
90
+ return driver.prepare(
91
+ `SELECT COUNT(*) AS n FROM _kv WHERE ns = ? AND (expires_at IS NULL OR expires_at > ?)`
92
+ ).get(ns, now()).n;
93
+ },
94
+ stop() {
95
+ if (timer) clearInterval(timer);
96
+ timer = void 0;
97
+ }
98
+ };
99
+ if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {
100
+ timer = setInterval(() => {
101
+ driver.prepare(
102
+ `DELETE FROM _kv WHERE expires_at IS NOT NULL AND expires_at <= ?`
103
+ ).run(now());
104
+ }, options.sweepIntervalMs);
105
+ timer.unref?.();
106
+ }
107
+ return api;
108
+ }
109
+
110
+ export { kv };
111
+ //# sourceMappingURL=index.js.map
112
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 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 setRaw(key, JSON.stringify(value), expires);\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 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 });\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"]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@monlite/kv",
3
+ "version": "0.1.0",
4
+ "description": "Redis-like local key-value cache for @monlite/core: get/set/incr with TTL, on SQLite.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "keywords": [
26
+ "monlite",
27
+ "kv",
28
+ "cache",
29
+ "redis",
30
+ "key-value",
31
+ "ttl",
32
+ "sqlite"
33
+ ],
34
+ "license": "MIT",
35
+ "author": "Emad Jumaah <emadjumaah@gmail.com>",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/qataruts/monlite.git",
39
+ "directory": "packages/kv"
40
+ },
41
+ "homepage": "https://github.com/qataruts/monlite/tree/main/packages/kv#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/qataruts/monlite/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "dependencies": {
52
+ "@monlite/core": "^1.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.10.0",
56
+ "better-sqlite3": "^11.8.0",
57
+ "tsup": "^8.3.5",
58
+ "typescript": "^5.7.2",
59
+ "vitest": "^2.1.8"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup",
63
+ "test": "vitest run",
64
+ "typecheck": "tsc --noEmit"
65
+ }
66
+ }