mirror of
https://github.com/earendil-works/pi.git
synced 2026-10-21 06:09:18 +08:00
122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
import { mkdir } from "node:fs/promises";
|
|
import { dirname } from "node:path";
|
|
import type { SQLInputValue, StatementSync } from "node:sqlite";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import type { SqliteDatabase, SqliteStatement, SqliteValue } from "./database.ts";
|
|
import { SqliteStorage } from "./storage.ts";
|
|
|
|
/** Node SQLite connection settings for a durable storage file. */
|
|
export type NodeSqliteStorageOptions = {
|
|
/** SQLite WAL auto-checkpoint threshold. SQLite and this adapter default to 1,000 pages; 0 disables it. */
|
|
readonly walAutoCheckpointPages?: number;
|
|
/** Time SQLite waits for a competing file lock. SQLite defaults to 0; this adapter defaults to 5,000 ms. */
|
|
readonly busyTimeoutMs?: number;
|
|
};
|
|
|
|
const DEFAULT_WAL_AUTO_CHECKPOINT_PAGES = 1_000;
|
|
const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
|
|
class NodeSqliteStatement implements SqliteStatement {
|
|
private readonly statement: StatementSync;
|
|
|
|
constructor(statement: StatementSync) {
|
|
this.statement = statement;
|
|
}
|
|
|
|
run(...params: SqliteValue[]): void {
|
|
this.statement.run(...(params as SQLInputValue[]));
|
|
}
|
|
|
|
get<T extends object>(...params: SqliteValue[]): T | undefined {
|
|
return this.statement.get(...(params as SQLInputValue[])) as T | undefined;
|
|
}
|
|
|
|
all<T extends object>(...params: SqliteValue[]): T[] {
|
|
return this.statement.all(...(params as SQLInputValue[])) as T[];
|
|
}
|
|
}
|
|
|
|
/** `SqliteDatabase` adapter backed by Node's built-in `node:sqlite`. */
|
|
export class NodeSqliteDatabase implements SqliteDatabase {
|
|
private readonly database: DatabaseSync;
|
|
private closed = false;
|
|
|
|
constructor(database: DatabaseSync) {
|
|
this.database = database;
|
|
}
|
|
|
|
exec(sql: string): void {
|
|
this.database.exec(sql);
|
|
}
|
|
|
|
prepare(sql: string): SqliteStatement {
|
|
return new NodeSqliteStatement(this.database.prepare(sql));
|
|
}
|
|
|
|
transaction<T>(callback: () => T): T {
|
|
this.database.exec("BEGIN IMMEDIATE");
|
|
try {
|
|
const result = callback();
|
|
if (
|
|
result !== null &&
|
|
(typeof result === "object" || typeof result === "function") &&
|
|
typeof Reflect.get(result, "then") === "function"
|
|
) {
|
|
throw new TypeError("SQLite transaction callbacks must be synchronous");
|
|
}
|
|
this.database.exec("COMMIT");
|
|
return result;
|
|
} catch (error) {
|
|
try {
|
|
this.database.exec("ROLLBACK");
|
|
} catch (rollbackError) {
|
|
throw new AggregateError([error, rollbackError], "SQLite transaction failed and rollback failed");
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
close(): void {
|
|
if (this.closed) return;
|
|
this.closed = true;
|
|
try {
|
|
this.database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
} finally {
|
|
this.database.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Open and configure a Node-backed SQLite database facade. */
|
|
export async function openNodeSqliteDatabase(
|
|
path: string,
|
|
options: NodeSqliteStorageOptions = {},
|
|
): Promise<NodeSqliteDatabase> {
|
|
const checkpointPages = options.walAutoCheckpointPages ?? DEFAULT_WAL_AUTO_CHECKPOINT_PAGES;
|
|
const timeout = options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;
|
|
if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
|
|
const database = new DatabaseSync(path, { timeout });
|
|
const adapter = new NodeSqliteDatabase(database);
|
|
try {
|
|
adapter.exec("PRAGMA journal_mode = WAL");
|
|
adapter.exec("PRAGMA synchronous = NORMAL");
|
|
adapter.exec(`PRAGMA wal_autocheckpoint = ${checkpointPages}`);
|
|
return adapter;
|
|
} catch (error) {
|
|
try {
|
|
adapter.close();
|
|
} catch {
|
|
// Preserve the configuration failure.
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/** Open or create file-backed durable storage using Node's built-in SQLite. */
|
|
export async function openNodeSqliteStorage(
|
|
path: string,
|
|
options: NodeSqliteStorageOptions = {},
|
|
): Promise<SqliteStorage> {
|
|
return SqliteStorage.open(await openNodeSqliteDatabase(path, options));
|
|
}
|