From 61403020ae5aad6ef048e0710c2589e8ccec2553 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:14:52 +0330 Subject: [PATCH] Import source: src/kv Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- src/kv.ts | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/kv.ts diff --git a/src/kv.ts b/src/kv.ts new file mode 100644 index 0000000..bd9faae --- /dev/null +++ b/src/kv.ts @@ -0,0 +1,86 @@ +import Database from 'better-sqlite3'; + +export interface KVNamespace { + get(key: string): Promise; + get(key: string, type: 'text'): Promise; + get(key: string, type: 'json'): Promise; + put(key: string, value: string, options?: unknown): Promise; + delete(key: string): Promise; + list(options?: { prefix?: string; cursor?: string; limit?: number }): Promise<{ + keys: Array<{ name: string }>; + list_complete: boolean; + cursor?: string; + }>; +} + +export class SqliteKV implements KVNamespace { + private db: InstanceType; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getStmt: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private putStmt: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private deleteStmt: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private listStmt: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private cleanupStmt: any; + + constructor(path: string) { + this.db = new Database(path); + this.db.exec(` + CREATE TABLE IF NOT EXISTS kv_store ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + expires_at INTEGER + ); + CREATE INDEX IF NOT EXISTS idx_kv_prefix ON kv_store(key); + `); + this.getStmt = this.db.prepare('SELECT value, expires_at FROM kv_store WHERE key = ?'); + this.putStmt = this.db.prepare('INSERT OR REPLACE INTO kv_store (key, value, expires_at) VALUES (?, ?, ?)'); + this.deleteStmt = this.db.prepare('DELETE FROM kv_store WHERE key = ?'); + this.listStmt = this.db.prepare('SELECT key FROM kv_store WHERE key LIKE ? ESCAPE \'\\\' AND (expires_at IS NULL OR expires_at >= ?) ORDER BY key LIMIT ? OFFSET ?'); + this.cleanupStmt = this.db.prepare('DELETE FROM kv_store WHERE expires_at IS NOT NULL AND expires_at < ?'); + setInterval(() => this.cleanupStmt.run(Date.now()), 60_000); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async get(key: string, type?: 'text' | 'json'): Promise { + const row = this.getStmt.get(key) as { value: string; expires_at: number | null } | undefined; + if (!row) return null; + if (row.expires_at !== null && row.expires_at < Date.now()) { + this.deleteStmt.run(key); + return null; + } + if (type === 'json') { + try { return JSON.parse(row.value); } catch { return null; } + } + return row.value; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async put(key: string, value: string, options?: any): Promise { + let expires_at: number | null = null; + if (options?.expirationTtl) expires_at = Date.now() + options.expirationTtl * 1000; + else if (options?.expiration) expires_at = options.expiration * 1000; + this.putStmt.run(key, value, expires_at); + } + + async delete(key: string): Promise { + this.deleteStmt.run(key); + } + + async list(options?: { prefix?: string; cursor?: string; limit?: number }): Promise<{ keys: Array<{ name: string }>; list_complete: boolean; cursor?: string }> { + const prefix = (options?.prefix ?? '').replace(/[%_\\]/g, '\\$&'); + const limit = Math.min(options?.limit ?? 1000, 1000); + const offset = options?.cursor ? parseInt(Buffer.from(options.cursor, 'base64url').toString(), 10) : 0; + const rows = this.listStmt.all(`${prefix}%`, Date.now(), limit + 1, offset) as { key: string }[]; + const hasMore = rows.length > limit; + const keys = rows.slice(0, limit).map(r => ({ name: r.key })); + return { + keys, + list_complete: !hasMore, + cursor: hasMore ? Buffer.from(String(offset + keys.length)).toString('base64url') : undefined, + }; + } +}