@monlite/kv 0.3.0 → 0.3.1
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 +9 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,7 @@ function kv(db, options = {}) {
|
|
|
30
30
|
).run(ns, key, v, expires);
|
|
31
31
|
let timer;
|
|
32
32
|
const subs = /* @__PURE__ */ new Set();
|
|
33
|
-
let
|
|
33
|
+
let psTask;
|
|
34
34
|
const psMaxSeq = () => driver.prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`).get(ns).s ?? 0;
|
|
35
35
|
const drainPubsub = () => {
|
|
36
36
|
if (subs.size === 0) return;
|
|
@@ -146,23 +146,22 @@ function kv(db, options = {}) {
|
|
|
146
146
|
subscribe(channel, cb) {
|
|
147
147
|
const sub = { channel, cb, cursor: psMaxSeq() };
|
|
148
148
|
subs.add(sub);
|
|
149
|
-
if (!
|
|
150
|
-
psTimer = setInterval(drainPubsub, pubsubPollMs);
|
|
151
|
-
psTimer.unref?.();
|
|
152
|
-
}
|
|
149
|
+
if (!psTask) psTask = db.heartbeat.every(pubsubPollMs, drainPubsub);
|
|
153
150
|
return () => {
|
|
154
151
|
subs.delete(sub);
|
|
155
|
-
if (subs.size === 0 &&
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
if (subs.size === 0 && psTask) {
|
|
153
|
+
psTask.cancel();
|
|
154
|
+
psTask = void 0;
|
|
158
155
|
}
|
|
159
156
|
};
|
|
160
157
|
},
|
|
161
158
|
stop() {
|
|
162
159
|
if (timer) clearInterval(timer);
|
|
163
160
|
timer = void 0;
|
|
164
|
-
if (
|
|
165
|
-
|
|
161
|
+
if (psTask) {
|
|
162
|
+
psTask.cancel();
|
|
163
|
+
psTask = void 0;
|
|
164
|
+
}
|
|
166
165
|
}
|
|
167
166
|
};
|
|
168
167
|
if (options.sweepIntervalMs && options.sweepIntervalMs > 0) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAwDA,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;AAAA;AAAA;AAAA;AAAA;AAAA,+EAAA;AAAA,KASF;AACA,IAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAAA,EAChB;AAEA,EAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,GAAA,EAAI;AAC3B,EAAA,MAAM,eAAe,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,OAAA,CAAQ,gBAAgB,GAAG,CAAA;AAC7D,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;AAIJ,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAS;AAC1B,EAAA,IAAI,OAAA;AAEJ,EAAA,MAAM,QAAA,GAAW,MAEb,MAAA,CACG,OAAA,CAAQ,2DAA2D,CAAA,CACnE,GAAA,CAAI,EAAE,CAAA,CACT,CAAA,IAAK,CAAA;AAGT,EAAA,MAAM,cAAc,MAAY;AAC9B,IAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACrB,IAAA,IAAI,GAAA,GAAM,QAAA;AACV,IAAA,KAAA,MAAW,KAAK,IAAA,EAAM,GAAA,GAAM,KAAK,GAAA,CAAI,GAAA,EAAK,EAAE,MAAM,CAAA;AAClD,IAAA,MAAM,OAAO,MAAA,CACV,OAAA;AAAA,MACC,CAAA,0FAAA;AAAA,KACF,CACC,GAAA,CAAI,EAAA,EAAI,GAAG,CAAA;AACd,IAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACvB,IAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAG,IAAI,CAAA,EAAG;AACzB,MAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,QAAA,IAAI,CAAA,CAAE,GAAA,IAAO,CAAA,CAAE,MAAA,EAAQ;AACvB,QAAA,CAAA,CAAE,SAAS,CAAA,CAAE,GAAA;AACb,QAAA,IAAI,CAAA,CAAE,OAAA,KAAY,CAAA,CAAE,OAAA,EAAS;AAC3B,UAAA,IAAI;AACF,YAAA,CAAA,CAAE,GAAG,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,OAAA,IAAW,MAAM,CAAC,CAAA;AAAA,UACtC,CAAA,CAAA,MAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA;AAEA,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,OAAA,CAAQ,SAAS,OAAA,EAAS;AACxB,MAAA,MAAM,KAAK,GAAA,EAAI;AACf,MAAA,MAAA,CACG,OAAA;AAAA,QACC,CAAA,6EAAA;AAAA,OACF,CACC,IAAI,EAAA,EAAI,OAAA,EAAS,KAAK,SAAA,CAAU,OAAA,IAAW,IAAI,CAAA,EAAG,EAAE,CAAA;AAGvD,MAAA,MAAA,CACG,OAAA,CAAQ,CAAA,2CAAA,CAA6C,CAAA,CACrD,GAAA,CAAI,KAAK,GAAM,CAAA;AAGlB,MAAA,WAAA,EAAY;AACZ,MAAA,IAAI,CAAA,GAAI,CAAA;AACR,MAAA,KAAA,MAAW,CAAA,IAAK,IAAA,EAAM,IAAI,CAAA,CAAE,YAAY,OAAA,EAAS,CAAA,EAAA;AACjD,MAAA,OAAO,CAAA;AAAA,IACT,CAAA;AAAA,IACA,SAAA,CAAU,SAAS,EAAA,EAAI;AACrB,MAAA,MAAM,MAAW,EAAE,OAAA,EAAS,EAAA,EAAI,MAAA,EAAQ,UAAS,EAAE;AACnD,MAAA,IAAA,CAAK,IAAI,GAAG,CAAA;AACZ,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,OAAA,GAAU,WAAA,CAAY,aAAa,YAAY,CAAA;AAC/C,QAAA,OAAA,CAAQ,KAAA,IAAQ;AAAA,MAClB;AACA,MAAA,OAAO,MAAM;AACX,QAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AACf,QAAA,IAAI,IAAA,CAAK,IAAA,KAAS,CAAA,IAAK,OAAA,EAAS;AAC9B,UAAA,aAAA,CAAc,OAAO,CAAA;AACrB,UAAA,OAAA,GAAU,MAAA;AAAA,QACZ;AAAA,MACF,CAAA;AAAA,IACF,CAAA;AAAA,IACA,IAAA,GAAO;AACL,MAAA,IAAI,KAAA,gBAAqB,KAAK,CAAA;AAC9B,MAAA,KAAA,GAAQ,MAAA;AACR,MAAA,IAAI,OAAA,gBAAuB,OAAO,CAAA;AAClC,MAAA,OAAA,GAAU,MAAA;AAAA,IACZ;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 /** 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 * 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 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 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.js
CHANGED
|
@@ -28,7 +28,7 @@ function kv(db, options = {}) {
|
|
|
28
28
|
).run(ns, key, v, expires);
|
|
29
29
|
let timer;
|
|
30
30
|
const subs = /* @__PURE__ */ new Set();
|
|
31
|
-
let
|
|
31
|
+
let psTask;
|
|
32
32
|
const psMaxSeq = () => driver.prepare(`SELECT MAX(seq) AS s FROM _monlite_kv_pubsub WHERE ns = ?`).get(ns).s ?? 0;
|
|
33
33
|
const drainPubsub = () => {
|
|
34
34
|
if (subs.size === 0) return;
|
|
@@ -144,23 +144,22 @@ function kv(db, options = {}) {
|
|
|
144
144
|
subscribe(channel, cb) {
|
|
145
145
|
const sub = { channel, cb, cursor: psMaxSeq() };
|
|
146
146
|
subs.add(sub);
|
|
147
|
-
if (!
|
|
148
|
-
psTimer = setInterval(drainPubsub, pubsubPollMs);
|
|
149
|
-
psTimer.unref?.();
|
|
150
|
-
}
|
|
147
|
+
if (!psTask) psTask = db.heartbeat.every(pubsubPollMs, drainPubsub);
|
|
151
148
|
return () => {
|
|
152
149
|
subs.delete(sub);
|
|
153
|
-
if (subs.size === 0 &&
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
if (subs.size === 0 && psTask) {
|
|
151
|
+
psTask.cancel();
|
|
152
|
+
psTask = void 0;
|
|
156
153
|
}
|
|
157
154
|
};
|
|
158
155
|
},
|
|
159
156
|
stop() {
|
|
160
157
|
if (timer) clearInterval(timer);
|
|
161
158
|
timer = void 0;
|
|
162
|
-
if (
|
|
163
|
-
|
|
159
|
+
if (psTask) {
|
|
160
|
+
psTask.cancel();
|
|
161
|
+
psTask = void 0;
|
|
162
|
+
}
|
|
164
163
|
}
|
|
165
164
|
};
|
|
166
165
|
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 * 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 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 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.
|
|
3
|
+
"version": "0.3.1",
|
|
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.
|
|
52
|
+
"@monlite/core": "^2.8.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^22.10.0",
|