import * as sqlite3 from '../sqlite3.pure'; import { ErrorCallback, tick } from './util'; import * as bindings from './bindings'; const DEFAULT_MODE = sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE; const registry = new FinalizationRegistry((heldValue: { value: number | undefined }) => { if (heldValue.value == null) { return; } try { bindings.sqlite3_close(heldValue.value); } catch { // ignore } }); export default class Database { open = false; private _handle: { value: number | undefined } = { value: undefined }; private _tasks = new Set(); private _waitCallbacks: Array<() => void> = []; emit!: (event: string, data?: any) => void; constructor(file: string, callback?: ErrorCallback); constructor(file: string, mode?: number, callback?: ErrorCallback); constructor(file: string, mode?: any, callback?: ErrorCallback) { const dbMode: number = typeof mode === 'number' ? mode : DEFAULT_MODE; const defaultCallback = (err?: Error | null) => { if (err != null) { this.emit('error', err); } else { this.emit('open'); } }; const dbCallback: ErrorCallback = callback || (typeof mode === 'function' ? mode : defaultCallback); tick(() => { try { this._handle.value = bindings.sqlite3_open_v2(file, dbMode); } catch (error: any) { dbCallback(error); return; } registry.register(this, this._handle); this.open = true; dbCallback(null); }); } close(callback?: ErrorCallback) { const defaultCallback: ErrorCallback = (err) => { if (err != null) { this.emit('error', err); } else { this.emit('close'); } }; const closeCallback: ErrorCallback = callback || defaultCallback; tick(() => { const handle = this._handle.value; if (handle === undefined) { closeCallback(sqlite3.sqliteError('SQLITE_MISUSE', 'Database handle is closed', sqlite3.MISUSE)); return; } try { bindings.sqlite3_close(handle); } catch (error: any) { closeCallback(error); return; } this.open = false; this._handle.value = undefined; closeCallback(null); }); } configure() { // FIXME } exec(sql: string, callback?: ErrorCallback) { tick(() => { const handle = this._handle.value; if (handle == null) { callback && callback(new Error('Database handle is closed')); return; } try { bindings.sqlite3_exec(handle, sql); } catch (error: any) { callback && callback(error); return; } callback && callback(null); }); return this; } /* * This undocumented (sigh) method should invoke a callback once all "ongoing" stuff * for this database has finished. This implementation is most likely not correct, since not all * operations in a statement notify the _waitCallbacks "queue", but it is enough to pass tests. */ wait(cb?: () => void) { if (cb == null) { return; } if (cb != null && this._tasks.size > 0) { this._waitCallbacks.push(cb); } else { process.nextTick(cb); } } serialize(callback?: ErrorCallback) { tick(() => callback && callback(null)); } // for internal usage within the package _getHandle() { return this._handle?.value; } // for internal usage within the package _addTask() { const taskId = {}; this._tasks.add(taskId); return () => this._removeTask(taskId); } private _removeTask(taskId: {}) { const removed = this._tasks.delete(taskId); if (removed && this._tasks.size === 0) { this._waitCallbacks.forEach((cb) => process.nextTick(cb)); this._waitCallbacks = []; } } }