yq-store
Version:
A high-performance, persistent and in-memory key-value store with TTL support, built for Node.js applications.
156 lines (153 loc) • 28.3 kB
JavaScript
function p(){process.env.NODE_NO_WARNINGS="1",process.removeAllListeners("warning"),process.on("warning",e=>{e.name==="ExperimentalWarning"&&e.message.includes("SQLite is an experimental feature")||e.message.includes("ExperimentalWarning")});let l=process.emitWarning;process.emitWarning=function(e,...t){let r=typeof e=="string"?e:e.message;if(!(r.includes("ExperimentalWarning")&&r.includes("SQLite")||r.includes("ExperimentalWarning")))return l.call(this,e,...t)}}p();import v from"node:path";import S from"node:fs/promises";import{EventEmitter as T}from"node:events";var m=class{emitter=new T;addEventListener(e,t){return this.emitter.on(e,t),this}on(e,t){return this.addEventListener(e,t)}removeEventListener(e,t){return this.emitter.removeListener(e,t),this}off(e,t){return this.removeEventListener(e,t)}emit(e,...t){return this.emitter.emit(e,...t)}};var E=class{constructor(e,t,r,s=!0){this.dbDir=e;this.dbName=t;this.emitter=r,this.softDelete=s}emitter;softDelete};var h=class extends E{db;dbFilePath;isInitialized=!1;config;isMemoryMode;maxEntries;maxMemory;constructor(e,t,r,s=!0,i){if(super(e,t,r,s),this.config=i||{},this.isMemoryMode=i?.type==="memory",this.isMemoryMode?(this.maxEntries=i?.maxEntries||5e4,this.maxMemory=i?.maxMemory||100*1024*1024):(this.maxEntries=i?.maxEntries||1e6,this.maxMemory=i?.maxMemory||0),this.isMemoryMode)this.dbFilePath=":memory:";else{let a=this.config.dbFileName||`${t}.yqs`;this.dbFilePath=v.resolve(e,a)}}async initialize(){return this.isBunRuntime()?new Promise(async(e,t)=>{try{this.isMemoryMode||(await S.mkdir(v.dirname(this.dbFilePath),{recursive:!0}),this.dbFilePath=v.resolve(this.dbFilePath));let r=(await import("bun:sqlite")).Database;this.db=new r(this.dbFilePath);let s=this.config?.sqlite||{};if(this.isMemoryMode?(this.db.exec("PRAGMA journal_mode=MEMORY;"),this.db.exec("PRAGMA synchronous=OFF;"),this.db.exec("PRAGMA cache_size=10000;"),this.db.exec("PRAGMA temp_store=memory;")):(this.db.exec(`PRAGMA journal_mode=${s.journalMode||"WAL"};`),this.db.exec(`PRAGMA synchronous=${s.synchronous||"NORMAL"};`),this.db.exec(`PRAGMA cache_size=${s.cacheSize||-1e5};`),this.db.exec(`PRAGMA temp_store=${s.tempStore||"memory"};`)),this.db.exec(`PRAGMA foreign_keys=${s.foreignKeys?"ON":"OFF"};`),s.busyTimeout&&this.db.exec(`PRAGMA busy_timeout=${s.busyTimeout};`),!this.isMemoryMode){let i=this.config?.vacuum||{};if(i.enabled!==!1){let a=i.mode||"incremental";a!=="none"&&this.db.exec(`PRAGMA auto_vacuum=${a==="full"?"FULL":"INCREMENTAL"};`)}}this.db.exec(`
CREATE TABLE IF NOT EXISTS kv_store (
key TEXT PRIMARY KEY,
value BLOB NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER,
is_deleted INTEGER DEFAULT 0,
version INTEGER NOT NULL DEFAULT 1,
last_accessed INTEGER DEFAULT (strftime('%s', 'now') * 1000),
format TEXT NOT NULL DEFAULT 'binary',
compression TEXT NOT NULL DEFAULT 'none'
);
CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv_store(expires_at);
CREATE INDEX IF NOT EXISTS idx_kv_created_at ON kv_store(created_at);
CREATE INDEX IF NOT EXISTS idx_kv_is_deleted ON kv_store(is_deleted);
CREATE INDEX IF NOT EXISTS idx_kv_last_accessed ON kv_store(last_accessed);
`),this.db.exec(`
CREATE TRIGGER IF NOT EXISTS lru_eviction
AFTER INSERT ON kv_store
WHEN (SELECT COUNT(*) FROM kv_store WHERE is_deleted = 0) > ${this.maxEntries}
BEGIN
DELETE FROM kv_store
WHERE key IN (
SELECT key FROM kv_store
WHERE is_deleted = 0
ORDER BY last_accessed ASC
LIMIT 1
);
END;
`),this.isInitialized=!0,e(this.db)}catch(r){t(r)}}):this.db}isBunRuntime(){return typeof Bun<"u"&&!!Bun?.version}async close(){return new Promise((e,t)=>{try{this.db&&(this.db.close(),this.db=void 0),this.isInitialized=!1,e()}catch(r){t(r)}})}valueToBinary(e){let t=JSON.stringify(e);return new TextEncoder().encode(t)}binaryToValue(e){let t=new TextDecoder().decode(e);return JSON.parse(t)}async set(e,t,r){return new Promise((s,i)=>{try{if(!this.isInitialized)throw new Error("Storage not initialized");let a=Date.now(),o=r?a+r*1e3:null,n=this.valueToBinary(t);this.db.prepare(`
INSERT OR REPLACE INTO kv_store
(key, value, created_at, expires_at, is_deleted, version, last_accessed)
VALUES (?, ?, ?, ?, 0, COALESCE((SELECT version + 1 FROM kv_store WHERE key = ?), 1), ?)
`).run(e,n,a,o,e,a),this.emitter.emit("set",e,t),s()}catch(a){i(a)}})}async get(e){return new Promise((t,r)=>{try{if(!this.isInitialized){t(null);return}let s=Date.now();if(this.db.prepare(`
SELECT key FROM kv_store
WHERE key = ? AND expires_at IS NOT NULL AND expires_at <= ?
`).get(e,s)){this.db.prepare("DELETE FROM kv_store WHERE key = ?").run(e),this.emitter.emit("expire",e),t(null);return}let n=this.db.prepare(`
SELECT key, value, created_at, expires_at, is_deleted, version
FROM kv_store
WHERE key = ? AND is_deleted = 0
`).get(e);if(n){this.db.prepare("UPDATE kv_store SET last_accessed = ? WHERE key = ?").run(s,e);let d=n;t({key:d.key,value:this.binaryToValue(d.value),createdAt:d.created_at,expiresAt:d.expires_at||void 0,isDeleted:!1,version:d.version})}else t(null)}catch(s){r(s)}})}async delete(e){return new Promise(async(t,r)=>{try{if(!this.isInitialized){t(!1);return}let s=await this.has(e);if(!s){t(!1);return}this.softDelete?this.db.prepare(`
UPDATE kv_store SET is_deleted = 1 WHERE key = ? AND is_deleted = 0
`).run(e):this.db.prepare("DELETE FROM kv_store WHERE key = ?").run(e);let i=await this.has(e);s&&!i?(this.emitter.emit("delete",e),t(!0)):t(!1)}catch(s){r(s)}})}async has(e){return new Promise((t,r)=>{try{if(!this.isInitialized){t(!1);return}let s=Date.now(),a=this.db.prepare(`
SELECT 1 FROM kv_store
WHERE key = ? AND is_deleted = 0
AND (expires_at IS NULL OR expires_at > ?)
`).get(e,s);t(!!a)}catch(s){r(s)}})}async keys(e={}){return new Promise((t,r)=>{try{if(!this.isInitialized){t([]);return}let s=Date.now(),i=`
SELECT key FROM kv_store
WHERE is_deleted = 0 AND (expires_at IS NULL OR expires_at > ?)
`,a=[s];e.prefix&&(i+=" AND key LIKE ?",a.push(e.prefix+"%")),e.suffix&&(i+=" AND key LIKE ?",a.push("%"+e.suffix));let n=this.db.prepare(i).all(...a);t(n.map(c=>c.key))}catch(s){r(s)}})}async getSize(e="active",t={}){return new Promise((r,s)=>{try{if(!this.isInitialized){r(0);return}let i="",a=[],o=Date.now();switch(e){case"active":i="SELECT COUNT(*) as Total FROM kv_store WHERE is_deleted = 0 AND (expires_at IS NULL OR expires_at > ?)",a.push(o);break;case"deleted":i="SELECT COUNT(*) as Total FROM kv_store WHERE is_deleted = 1";break;case"expired":i="SELECT COUNT(*) as Total FROM kv_store WHERE expires_at IS NOT NULL AND expires_at <= ?",a.push(o);break;case"all":i="SELECT COUNT(*) as Total FROM kv_store";break}t.prefix&&(i+=" AND key LIKE ?",a.push(t.prefix+"%")),t.suffix&&(i+=" AND key LIKE ?",a.push("%"+t.suffix));let n=this.db.prepare(i).get(...a);r(n.Total)}catch(i){s(i)}})}async clear(){return new Promise((e,t)=>{try{if(!this.isInitialized||!this.db){e();return}this.softDelete?this.db.exec("UPDATE kv_store SET is_deleted = 1 WHERE is_deleted = 0"):this.db.exec("DELETE FROM kv_store"),e()}catch(r){t(r)}})}async batch(e){return new Promise((t,r)=>{try{if(!this.isInitialized||!this.db){t();return}this.db.transaction(()=>{let i=Date.now();for(let a of e)if(a.type==="put"){let o=a.ttlSeconds?i+a.ttlSeconds*1e3:null,n=this.valueToBinary(a.value);this.db.prepare(`
INSERT OR REPLACE INTO kv_store
(key, value, created_at, expires_at, is_deleted, version, last_accessed)
VALUES (?, ?, ?, ?, 0, COALESCE((SELECT version + 1 FROM kv_store WHERE key = ?), 1), ?)
`).run(a.key,n,i,o,a.key,i),this.emitter.emit("set",a.key,a.value)}else a.type==="del"&&(this.softDelete?this.db.prepare(`
UPDATE kv_store SET is_deleted = 1 WHERE key = ? AND is_deleted = 0
`).run(a.key):this.db.prepare("DELETE FROM kv_store WHERE key = ?").run(a.key),this.emitter.emit("delete",a.key))})(),t()}catch(s){r(s)}})}async getIncludingDeleted(e){return new Promise((t,r)=>{try{if(!this.isInitialized){t(null);return}let i=this.db.prepare(`
SELECT key, value, created_at, expires_at, is_deleted, version
FROM kv_store WHERE key = ?
`).get(e);if(i){let a=i;t({key:a.key,value:a.is_deleted?null:this.binaryToValue(a.value),createdAt:a.created_at,expiresAt:a.expires_at||void 0,isDeleted:!!a.is_deleted,version:a.version})}else t(null)}catch(s){r(s)}})}async getAllKeysIncludingDeleted(){return new Promise((e,t)=>{try{if(!this.isInitialized){e([]);return}let s=this.db.prepare("SELECT key FROM kv_store").all();e(s.map(i=>i.key))}catch(r){t(r)}})}async clearSoftDeletedEntries(){return new Promise((e,t)=>{try{if(!this.isInitialized){e(0);return}let r=this.db.prepare("SELECT COUNT(*) as count FROM kv_store WHERE is_deleted = 1"),i=r.get()?.count||0;this.db.prepare("DELETE FROM kv_store WHERE is_deleted = 1").run();let n=r.get()?.count||0;e(i-n)}catch(r){t(r)}})}async clearExpiredEntries(){return new Promise((e,t)=>{try{if(!this.isInitialized){e(0);return}let r=Date.now(),i=this.db.prepare(`
SELECT key FROM kv_store
WHERE expires_at IS NOT NULL AND expires_at <= ?
`).all(r),o=this.db.prepare(`
DELETE FROM kv_store
WHERE expires_at IS NOT NULL AND expires_at <= ?
`).run(r);for(let n of i)this.emitter.emit("expire",n.key);e(Number(o.changes)||0)}catch(r){t(r)}})}async evictLRUEntries(e){return new Promise(async(t,r)=>{try{if(!this.isInitialized){t(0);return}let s=await this.getSize("active"),i=e||this.maxEntries;if(s<=i){t(0);return}let a=s-i,n=this.db.prepare(`
SELECT key FROM kv_store
WHERE is_deleted = 0
ORDER BY last_accessed ASC
LIMIT ?
`).all(a),c=this.db.prepare("DELETE FROM kv_store WHERE key = ?"),d=0;for(let b of n){let u=b.key;c.run(u),this.emitter.emit("evict",u),d++}t(d)}catch(s){r(s)}})}async evictExpiredEntries(){return new Promise(async(e,t)=>{try{let r=await this.clearExpiredEntries(),s=await this.evictLRUEntries();e(r+s)}catch(r){t(r)}})}getIndexFilePath(){return this.dbFilePath+".index"}getDataFilePath(){return this.dbFilePath}async getAllEntries(e={}){return new Promise((t,r)=>{try{if(!this.isInitialized){t([]);return}let s=Date.now(),i=`
SELECT key, value, created_at, expires_at, is_deleted, version
FROM kv_store
WHERE is_deleted = 0 AND (expires_at IS NULL OR expires_at > ?)
`,a=[s];e.prefix&&(i+=" AND key LIKE ?",a.push(e.prefix+"%")),e.suffix&&(i+=" AND key LIKE ?",a.push("%"+e.suffix)),e.limit&&(i+=" LIMIT ?",a.push(e.limit));let n=this.db.prepare(i).all(...a);t(n.map(c=>({key:c.key,value:this.binaryToValue(c.value),createdAt:c.created_at,expiresAt:c.expires_at||void 0,isDeleted:!!c.is_deleted,version:c.version})))}catch(s){r(s)}})}};import g from"node:path";import _ from"node:fs/promises";var y=class extends E{db;dbFilePath;isInitialized=!1;config;isMemoryMode;maxEntries;maxMemory;constructor(e,t,r,s=!0,i){if(super(e,t,r,s),this.config=i||{},this.isMemoryMode=i?.type==="memory",this.isMemoryMode?(this.maxEntries=i?.maxEntries||5e4,this.maxMemory=i?.maxMemory||100*1024*1024):(this.maxEntries=i?.maxEntries||1e6,this.maxMemory=i?.maxMemory||0),this.isMemoryMode)this.dbFilePath=":memory:";else{let a=this.config.dbFileName||`${t}.yqs`;this.dbFilePath=g.resolve(e,a)}}async initialize(){return this.isBunRuntime()?this.db:new Promise(async(e,t)=>{try{this.isMemoryMode||(await _.mkdir(g.dirname(this.dbFilePath),{recursive:!0}),this.dbFilePath=g.resolve(this.dbFilePath));let r=(await import("node:sqlite")).DatabaseSync;this.db=new r(this.dbFilePath);let s=this.config?.sqlite||{};if(this.isMemoryMode?(this.db.exec("PRAGMA journal_mode=MEMORY;"),this.db.exec("PRAGMA synchronous=OFF;"),this.db.exec("PRAGMA cache_size=10000;"),this.db.exec("PRAGMA temp_store=memory;")):(this.db.exec(`PRAGMA journal_mode=${s.journalMode||"WAL"};`),this.db.exec(`PRAGMA synchronous=${s.synchronous||"NORMAL"};`),this.db.exec(`PRAGMA cache_size=${s.cacheSize||-1e5};`),this.db.exec(`PRAGMA temp_store=${s.tempStore||"memory"};`)),this.db.exec(`PRAGMA foreign_keys=${s.foreignKeys?"ON":"OFF"};`),s.busyTimeout&&this.db.exec(`PRAGMA busy_timeout=${s.busyTimeout};`),!this.isMemoryMode){let i=this.config?.vacuum||{};if(i.enabled!==!1){let a=i.mode||"incremental";a!=="none"&&this.db.exec(`PRAGMA auto_vacuum=${a==="full"?"FULL":"INCREMENTAL"};`)}}this.db.exec(`
CREATE TABLE IF NOT EXISTS kv_store (
key TEXT PRIMARY KEY,
value BLOB NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER,
is_deleted INTEGER DEFAULT 0,
version INTEGER NOT NULL DEFAULT 1,
last_accessed INTEGER DEFAULT (strftime('%s', 'now') * 1000),
format TEXT NOT NULL DEFAULT 'binary',
compression TEXT NOT NULL DEFAULT 'none'
);
CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv_store(expires_at);
CREATE INDEX IF NOT EXISTS idx_kv_created_at ON kv_store(created_at);
CREATE INDEX IF NOT EXISTS idx_kv_is_deleted ON kv_store(is_deleted);
CREATE INDEX IF NOT EXISTS idx_kv_last_accessed ON kv_store(last_accessed);
`),this.db.exec(`
CREATE TRIGGER IF NOT EXISTS lru_eviction
AFTER INSERT ON kv_store
WHEN (SELECT COUNT(*) FROM kv_store WHERE is_deleted = 0) > ${this.maxEntries}
BEGIN
DELETE FROM kv_store
WHERE key IN (
SELECT key FROM kv_store
WHERE is_deleted = 0
ORDER BY last_accessed ASC
LIMIT 1
);
END;
`),this.isInitialized=!0,e(this.db)}catch(r){t(r)}})}isBunRuntime(){return typeof Bun<"u"&&!!Bun?.version}async close(){return new Promise((e,t)=>{try{this.db&&(this.db.close(),this.db=void 0),this.isInitialized=!1,e()}catch(r){t(r)}})}valueToBinary(e){let t=JSON.stringify(e);return new TextEncoder().encode(t)}binaryToValue(e){let t=new TextDecoder().decode(e);return JSON.parse(t)}async set(e,t,r){return new Promise((s,i)=>{try{if(!this.isInitialized)throw new Error("Storage not initialized");let a=Date.now(),o=r?a+r*1e3:null,n=this.valueToBinary(t);this.db.prepare(`
INSERT OR REPLACE INTO kv_store
(key, value, created_at, expires_at, is_deleted, version, last_accessed)
VALUES (?, ?, ?, ?, 0, COALESCE((SELECT version + 1 FROM kv_store WHERE key = ?), 1), ?)
`).run(e,n,a,o,e,a),this.emitter.emit("set",e,t),s()}catch(a){i(a)}})}async get(e){return new Promise((t,r)=>{try{if(!this.isInitialized){t(null);return}let s=Date.now();if(this.db.prepare(`
SELECT key FROM kv_store
WHERE key = ? AND expires_at IS NOT NULL AND expires_at <= ?
`).get(e,s)){this.db.prepare("DELETE FROM kv_store WHERE key = ?").run(e),this.emitter.emit("expire",e),t(null);return}let n=this.db.prepare(`
SELECT key, value, created_at, expires_at, is_deleted, version
FROM kv_store
WHERE key = ? AND is_deleted = 0
`).get(e);n?(this.db.prepare("UPDATE kv_store SET last_accessed = ? WHERE key = ?").run(s,e),t({key:n.key,value:this.binaryToValue(n.value),createdAt:n.created_at,expiresAt:n.expires_at||void 0,isDeleted:!1,version:n.version})):t(null)}catch(s){r(s)}})}async delete(e){return new Promise(async(t,r)=>{try{if(!this.isInitialized){t(!1);return}let s=await this.has(e);if(!s){t(!1);return}this.softDelete?this.db.prepare(`
UPDATE kv_store SET is_deleted = 1 WHERE key = ? AND is_deleted = 0
`).run(e):this.db.prepare("DELETE FROM kv_store WHERE key = ?").run(e);let i=await this.has(e);s&&!i?(this.emitter.emit("delete",e),t(!0)):t(!1)}catch(s){r(s)}})}async has(e){return new Promise((t,r)=>{try{if(!this.isInitialized){t(!1);return}let s=Date.now(),a=this.db.prepare(`
SELECT 1 FROM kv_store
WHERE key = ? AND is_deleted = 0
AND (expires_at IS NULL OR expires_at > ?)
`).get(e,s);t(!!a)}catch(s){r(s)}})}async keys(e={}){return new Promise((t,r)=>{try{if(!this.isInitialized){t([]);return}let s=Date.now(),i=`
SELECT key FROM kv_store
WHERE is_deleted = 0 AND (expires_at IS NULL OR expires_at > ?)
`,a=[s];e.prefix&&(i+=" AND key LIKE ?",a.push(e.prefix+"%")),e.suffix&&(i+=" AND key LIKE ?",a.push("%"+e.suffix));let n=this.db.prepare(i).all(...a);t(n.map(c=>c.key))}catch(s){r(s)}})}async getSize(e="active",t={}){return new Promise((r,s)=>{try{if(!this.isInitialized){r(0);return}let i="",a=[],o=Date.now();switch(e){case"active":i="SELECT COUNT(*) as Total FROM kv_store WHERE is_deleted = 0 AND (expires_at IS NULL OR expires_at > ?)",a.push(o);break;case"deleted":i="SELECT COUNT(*) as Total FROM kv_store WHERE is_deleted = 1";break;case"expired":i="SELECT COUNT(*) as Total FROM kv_store WHERE expires_at IS NOT NULL AND expires_at <= ?",a.push(o);break;case"all":i="SELECT COUNT(*) as Total FROM kv_store";break}t.prefix&&(i+=" AND key LIKE ?",a.push(t.prefix+"%")),t.suffix&&(i+=" AND key LIKE ?",a.push("%"+t.suffix));let n=this.db.prepare(i).get(...a);r(n.Total)}catch(i){s(i)}})}async clear(){return new Promise((e,t)=>{try{if(!this.isInitialized||!this.db){e();return}this.softDelete?this.db.exec("UPDATE kv_store SET is_deleted = 1 WHERE is_deleted = 0"):this.db.exec("DELETE FROM kv_store"),e()}catch(r){t(r)}})}async batch(e){return new Promise((t,r)=>{try{if(!this.isInitialized||!this.db){t();return}this.db.exec("BEGIN TRANSACTION");try{let s=Date.now();for(let i of e)if(i.type==="put"){let a=i.ttlSeconds?s+i.ttlSeconds*1e3:null,o=this.valueToBinary(i.value);this.db.prepare(`
INSERT OR REPLACE INTO kv_store
(key, value, created_at, expires_at, is_deleted, version, last_accessed)
VALUES (?, ?, ?, ?, 0, COALESCE((SELECT version + 1 FROM kv_store WHERE key = ?), 1), ?)
`).run(i.key,o,s,a,i.key,s),this.emitter.emit("set",i.key,i.value)}else i.type==="del"&&(this.softDelete?this.db.prepare(`
UPDATE kv_store SET is_deleted = 1 WHERE key = ? AND is_deleted = 0
`).run(i.key):this.db.prepare("DELETE FROM kv_store WHERE key = ?").run(i.key),this.emitter.emit("delete",i.key));this.db.exec("COMMIT"),t()}catch(s){this.db.exec("ROLLBACK"),r(s)}}catch(s){r(s)}})}async getIncludingDeleted(e){return new Promise((t,r)=>{try{if(!this.isInitialized){t(null);return}let i=this.db.prepare(`
SELECT key, value, created_at, expires_at, is_deleted, version
FROM kv_store WHERE key = ?
`).get(e);t(i?{key:i.key,value:i.is_deleted?null:this.binaryToValue(i.value),createdAt:i.created_at,expiresAt:i.expires_at||void 0,isDeleted:!!i.is_deleted,version:i.version}:null)}catch(s){r(s)}})}async getAllKeysIncludingDeleted(){return new Promise((e,t)=>{try{if(!this.isInitialized){e([]);return}let s=this.db.prepare("SELECT key FROM kv_store").all();e(s.map(i=>i.key))}catch(r){t(r)}})}async clearSoftDeletedEntries(){return new Promise((e,t)=>{try{if(!this.isInitialized){e(0);return}let r=this.db.prepare("SELECT COUNT(*) as count FROM kv_store WHERE is_deleted = 1"),i=r.get()?.count||0;this.db.prepare("DELETE FROM kv_store WHERE is_deleted = 1").run();let n=r.get()?.count||0;e(i-n)}catch(r){t(r)}})}async clearExpiredEntries(){return new Promise((e,t)=>{try{if(!this.isInitialized){e(0);return}let r=Date.now(),i=this.db.prepare(`
SELECT key FROM kv_store
WHERE expires_at IS NOT NULL AND expires_at <= ?
`).all(r),o=this.db.prepare(`
DELETE FROM kv_store
WHERE expires_at IS NOT NULL AND expires_at <= ?
`).run(r);for(let n of i)this.emitter.emit("expire",n.key);e(Number(o.changes)||0)}catch(r){t(r)}})}async evictLRUEntries(e){return new Promise(async(t,r)=>{try{if(!this.isInitialized){t(0);return}let s=await this.getSize("active"),i=e||this.maxEntries;if(s<=i){t(0);return}let a=s-i,n=this.db.prepare(`
SELECT key FROM kv_store
WHERE is_deleted = 0
ORDER BY last_accessed ASC
LIMIT ?
`).all(a),c=this.db.prepare("DELETE FROM kv_store WHERE key = ?"),d=0;for(let b of n){let u=b.key;c.run(u),this.emitter.emit("evict",u),d++}t(d)}catch(s){r(s)}})}async evictExpiredEntries(){return new Promise(async(e,t)=>{try{let r=await this.clearExpiredEntries(),s=await this.evictLRUEntries();e(r+s)}catch(r){t(r)}})}getIndexFilePath(){return this.dbFilePath+".index"}getDataFilePath(){return this.dbFilePath}async getAllEntries(e={}){return new Promise((t,r)=>{try{if(!this.isInitialized){t([]);return}let s=Date.now(),i=`
SELECT key, value, created_at, expires_at, is_deleted, version
FROM kv_store
WHERE is_deleted = 0 AND (expires_at IS NULL OR expires_at > ?)
`,a=[s];e.prefix&&(i+=" AND key LIKE ?",a.push(e.prefix+"%")),e.suffix&&(i+=" AND key LIKE ?",a.push("%"+e.suffix)),e.limit&&(i+=" LIMIT ?",a.push(e.limit));let n=this.db.prepare(i).all(...a);t(n.map(c=>({key:c.key,value:this.binaryToValue(c.value),createdAt:c.created_at,expiresAt:c.expires_at||void 0,isDeleted:!!c.is_deleted,version:c.version})))}catch(s){r(s)}})}};function R(){return typeof Bun<"u"&&!!Bun?.version}var x=R()?h:y;import k from"node:os";import A from"node:crypto";function N(l){return A.createHash("md5").update(l).digest("hex")}var f=class l{options;compactionTimer;evictionTimer;tombstoneCount=0;isCompacting=!1;isClosing=!1;emitter=new m;storageManager;dbName="store_storage";constructor(e){let t=k.tmpdir();this.options={compactionInterval:e?.compactionInterval??36e5,ttl:e?.ttl??0,softDelete:e?.softDelete??!0,storage:{type:e?.storage?.type??"persistence",maxEntries:e?.storage?.maxEntries,maxMemory:e?.storage?.maxMemory,persistence:e?.storage?.persistence},logging:e?.logging??{enabled:!1},debug:{enabled:e?.debug?.enabled??!1,timing:e?.debug?.timing??!1,sqlLogging:e?.debug?.sqlLogging??!1}},this.options.storage?.persistence?.dbDir?t=this.options.storage.persistence.dbDir:this.dbName=N(process.cwd())+"_"+this.dbName,this.options.storage?.persistence?.dbFileName&&(this.dbName=this.options.storage.persistence.dbFileName),this.storageManager=new x(t,this.dbName,this.emitter,this.options.softDelete,this.options.storage)}static async create(e){let t=new l(e);return await t.initialize(),t}async initialize(){try{await this.storageManager.initialize(),this.emitter.emit("db:ready"),await this.storageManager.clearExpiredEntries(),this.options.compactionInterval>0&&(this.compactionTimer=setInterval(()=>this.compact(),this.options.compactionInterval)),this.evictionTimer=setInterval(()=>this.evictExpiredEntries(),3e4),this.emitter.emit("ready")}catch(e){throw this.emitter.emit("error",e),e}}on(e,t){return this.emitter.on(e,t),this}off(e,t){return this.emitter.off(e,t),this}addEventListener(e,t){return this.on(e,t)}removeEventListener(e,t){return this.off(e,t)}async has(e){return await this.storageManager.has(e)}async get(e){let t=await this.storageManager.get(e);return t?t.value:null}async set(e,t,r){await this.storageManager.set(e,t,r),await this.checkAndTriggerCompaction(),await this.checkAndTriggerEviction(),this.emitter.emit("set",e,t)}async delete(e){let t=await this.storageManager.delete(e);return t&&this.emitter.emit("delete",e),t}async getSize(e,t){return e===void 0?this.storageManager.getSize():typeof e=="string"?this.storageManager.getSize(e,t):this.storageManager.getSize(void 0,e)}async listKeys(e={}){return await this.storageManager.keys(e)}async forEach(e,t={}){let r=0,s=t.limit??1/0,i=await this.storageManager.keys();for(let a of i){if(r>=s)break;if(t.prefix&&!a.startsWith(t.prefix)||t.suffix&&!a.endsWith(t.suffix))continue;let o=await this.get(a);o!==null&&(e(a,o),r++)}}async list(e={}){let t=[],r=e.limit??100,i=((e.page??1)-1)*r,a=0,o=0,n=await this.storageManager.keys();for(let c of n){if(a>=r)break;if(e.prefix&&!c.startsWith(e.prefix)||e.suffix&&!c.endsWith(e.suffix))continue;if(o<i){o++;continue}let d=await this.get(c);d!==null&&(t.push({key:c,value:d}),a++)}return t}async clearSoftDeletedEntries(){return await this.storageManager.clearSoftDeletedEntries()}async clearExpiredEntries(){return await this.storageManager.clearExpiredEntries()}async compact(){if(!this.isCompacting){this.isCompacting=!0,this.emitter.emit("compact:start");try{this.options.softDelete&&await this.storageManager.clearSoftDeletedEntries(),this.tombstoneCount=0,this.emitter.emit("compact:end",{newSize:0,keysProcessed:0})}catch(e){this.emitter.emit("error",e)}finally{this.isCompacting=!1}}}async checkAndTriggerCompaction(){if(this.isCompacting||this.options.compactionInterval===0)return;let t=(await this.storageManager.keys()).length;((t>0?this.tombstoneCount/t:0)>.5||this.tombstoneCount>100)&&(this.tombstoneCount=0,setImmediate(()=>{this.isClosing||this.compact().catch(()=>{})}))}async batch(e){await this.storageManager.batch(e)}createTransaction(){let e=[],t=!1,r=!1,s=this.batch.bind(this);return{put(i,a,o){if(t||r)throw new Error("Transaction has already been committed or rolled back");e.push({type:"put",key:i,value:a,ttlSeconds:o})},del(i){if(t||r)throw new Error("Transaction has already been committed or rolled back");e.push({type:"del",key:i})},async commit(){if(t||r)throw new Error("Transaction has already been committed or rolled back");await s(e),t=!0},rollback(){if(t||r)throw new Error("Transaction has already been committed or rolled back");e.length=0,r=!0}}}async close(){this.isClosing=!0,this.compactionTimer&&clearInterval(this.compactionTimer),this.evictionTimer&&clearInterval(this.evictionTimer),await this.storageManager.close(),this.emitter.emit("close")}async checkAndTriggerEviction(){let e=await this.storageManager.getSize("active"),t=this.options.storage?.maxEntries;t&&e>t&&setImmediate(()=>{this.isClosing||this.evictExpiredEntries().catch(()=>{})})}async evictExpiredEntries(){try{let e=await this.storageManager.evictExpiredEntries();e>0&&this.emitter.emit("evict",e)}catch(e){this.emitter.emit("error",e)}}};p();export{m as TypedEventEmitter,f as YqStore,f as default};
/**
* yq-store - A high-performance, zero-dependency, persistent Key-Value store for Node.js
*
* @author Olajide Mathew Ogundare <git@yuniq.solutions>
* @license MIT
*/