@worker-tools/deno-kv-storage
Version:
An implementation of the StorageArea (1,2,3) interface for Deno with an extensible system for supporting various database backends.
113 lines (95 loc) • 2.95 kB
text/typescript
// deno-lint-ignore-file require-await
import "../_dnt.polyfills.js";
import { DB } from "../deps/deno.land/x/sqlite@v2.5.0/mod.js";
import type { Adapter, AdapterParams } from './mod.js';
const CREATE = 'CREATE TABLE IF NOT EXISTS [kv-storage] (area TEXT, key TEXT, value TEXT, PRIMARY KEY (area, key))';
const GET = 'SELECT value FROM [kv-storage] WHERE key=:key AND area=:area';
const UPSERT = 'INSERT INTO [kv-storage] (area, key, value) VALUES (:area, :key, :value) ON CONFLICT(area, key) DO UPDATE SET value=:value';
const DELETE = 'DELETE FROM [kv-storage] WHERE key=:key AND area=:area';
const CLEAR = 'DELETE FROM [kv-storage] WHERE area=:area';
const KEYS = 'SELECT key FROM [kv-storage] WHERE area=:area';
const VALUES = 'SELECT value FROM [kv-storage] WHERE area=:area';
const ENTRIES = 'SELECT key, value FROM [kv-storage] WHERE area=:area';
export class SQLiteAdapter implements Adapter {
private filename: string;
private area: string;
private db?: DB;
private memory: boolean;
private refs = 0
constructor({ area, url }: AdapterParams) {
this.area = area;
const filename = url.substring('sqlite://'.length);
const memory = this.memory = ['', 'memory'].includes(filename ?? '');
const db = this.db = new DB(this.filename = memory ? ':memory:' : filename);
[...db.query(CREATE)];
this.keepOpen();
}
private keepOpen() {
if (!this.memory) queueMicrotask(() => {
if (this.refs === 0) {
this.db?.close()
delete this.db;
}
})
}
private query(query: string, params?: { key?: string, value?: string }) {
const db = this.db ||= new DB(this.filename);
const rows = db.query(query, { ...params, area: this.area });
return rows;
}
async get(key: string): Promise<string | undefined> {
const res = this.query(GET, { key }).next().value?.[0];
this.keepOpen()
return res;
}
async set(key: string, value: string) {
[...this.query(UPSERT, { key, value })];
this.keepOpen()
}
async delete(key: string) {
[...this.query(DELETE, { key })];
this.keepOpen()
}
async clear() {
[...this.query(CLEAR)];
this.keepOpen()
}
async *keys() {
try {
this.refs++
for (const [key] of this.query(KEYS)) {
yield key;
}
} finally {
this.refs--;
this.keepOpen()
}
}
async *values() {
try {
this.refs++
for (const [value] of this.query(VALUES)) {
yield value;
}
} finally {
this.refs--
this.keepOpen()
}
}
async *entries() {
try {
this.refs++
for (const [key, value] of this.query(ENTRIES)) {
yield [key, value] as const;
}
} finally {
this.refs--;
this.keepOpen()
}
}
backingStore() {
return this.db ?? new DB(this.filename);
}
}
// @ts-ignore: ...
(globalThis.deno_storage_area__adapters ||= new Map()).set('sqlite:', SQLiteAdapter);