Import source: src/kv

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:14:52 +03:30
parent 4e502c9b21
commit 61403020ae
+86
View File
@@ -0,0 +1,86 @@
import Database from 'better-sqlite3';
export interface KVNamespace {
get(key: string): Promise<string | null>;
get(key: string, type: 'text'): Promise<string | null>;
get<T = unknown>(key: string, type: 'json'): Promise<T | null>;
put(key: string, value: string, options?: unknown): Promise<void>;
delete(key: string): Promise<void>;
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<typeof Database>;
// 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<any> {
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<void> {
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<void> {
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,
};
}
}