forked from trent_larson/crowd-funder-for-time-pwa
- Remove unnecessary generic type parameter from AbsurdSqlDatabaseService - Fix type handling in operation queue and result processing - Correct WebPlatformService dbGetOneRow implementation to use imported databaseService - Add proper type annotations for database operation results This commit improves type safety and fixes several TypeScript errors that were preventing proper type checking in the database service layer.
235 lines
6.7 KiB
TypeScript
235 lines
6.7 KiB
TypeScript
import initSqlJs from "@jlongster/sql.js";
|
|
import { SQLiteFS } from "absurd-sql";
|
|
import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend";
|
|
|
|
import { runMigrations } from "../db-sql/migration";
|
|
import type { DatabaseService, QueryExecResult } from "../interfaces/database";
|
|
import { logger } from "@/utils/logger";
|
|
|
|
interface QueuedOperation {
|
|
type: "run" | "query" | "getOneRow" | "getAll";
|
|
sql: string;
|
|
params: unknown[];
|
|
resolve: (value: unknown) => void;
|
|
reject: (reason: unknown) => void;
|
|
}
|
|
|
|
interface AbsurdSqlDatabase {
|
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
|
run: (
|
|
sql: string,
|
|
params?: unknown[],
|
|
) => Promise<{ changes: number; lastId?: number }>;
|
|
}
|
|
|
|
class AbsurdSqlDatabaseService implements DatabaseService {
|
|
private static instance: AbsurdSqlDatabaseService | null = null;
|
|
private db: AbsurdSqlDatabase | null;
|
|
private initialized: boolean;
|
|
private initializationPromise: Promise<void> | null = null;
|
|
private operationQueue: Array<QueuedOperation> = [];
|
|
private isProcessingQueue: boolean = false;
|
|
|
|
private constructor() {
|
|
this.db = null;
|
|
this.initialized = false;
|
|
}
|
|
|
|
static getInstance(): AbsurdSqlDatabaseService {
|
|
if (!AbsurdSqlDatabaseService.instance) {
|
|
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
|
}
|
|
return AbsurdSqlDatabaseService.instance;
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
// If already initialized, return immediately
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
|
|
// If initialization is in progress, wait for it
|
|
if (this.initializationPromise) {
|
|
return this.initializationPromise;
|
|
}
|
|
|
|
// Start initialization
|
|
this.initializationPromise = this._initialize();
|
|
try {
|
|
await this.initializationPromise;
|
|
} catch (error) {
|
|
logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
|
|
this.initializationPromise = null; // Reset on failure
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async _initialize(): Promise<void> {
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
|
|
const SQL = await initSqlJs({
|
|
locateFile: (file: string) => {
|
|
return new URL(
|
|
`/node_modules/@jlongster/sql.js/dist/${file}`,
|
|
import.meta.url,
|
|
).href;
|
|
},
|
|
});
|
|
|
|
const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
|
|
SQL.register_for_idb(sqlFS);
|
|
|
|
SQL.FS.mkdir("/sql");
|
|
SQL.FS.mount(sqlFS, {}, "/sql");
|
|
|
|
const path = "/sql/timesafari.sqlite";
|
|
if (typeof SharedArrayBuffer === "undefined") {
|
|
const stream = SQL.FS.open(path, "a+");
|
|
await stream.node.contents.readIfFallback();
|
|
SQL.FS.close(stream);
|
|
}
|
|
|
|
this.db = new SQL.Database(path, { filename: true });
|
|
if (!this.db) {
|
|
throw new Error(
|
|
"The database initialization failed. We recommend you restart or reinstall.",
|
|
);
|
|
}
|
|
|
|
// An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)"
|
|
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
|
|
const sqlExec = this.db.exec.bind(this.db);
|
|
|
|
// Run migrations
|
|
await runMigrations(sqlExec);
|
|
|
|
this.initialized = true;
|
|
|
|
// Start processing the queue after initialization
|
|
this.processQueue();
|
|
}
|
|
|
|
private async processQueue(): Promise<void> {
|
|
if (this.isProcessingQueue || !this.initialized || !this.db) {
|
|
return;
|
|
}
|
|
|
|
this.isProcessingQueue = true;
|
|
|
|
while (this.operationQueue.length > 0) {
|
|
const operation = this.operationQueue.shift();
|
|
if (!operation) continue;
|
|
|
|
try {
|
|
let result: unknown;
|
|
switch (operation.type) {
|
|
case "run":
|
|
result = await this.db.run(operation.sql, operation.params);
|
|
break;
|
|
case "query":
|
|
result = await this.db.exec(operation.sql, operation.params);
|
|
break;
|
|
case "getOneRow":
|
|
const queryResult = await this.db.exec(operation.sql, operation.params);
|
|
result = queryResult[0]?.values[0];
|
|
break;
|
|
case "getAll":
|
|
const allResult = await this.db.exec(operation.sql, operation.params);
|
|
result = allResult[0]?.values || [];
|
|
break;
|
|
}
|
|
operation.resolve(result);
|
|
} catch (error) {
|
|
operation.reject(error);
|
|
}
|
|
}
|
|
|
|
this.isProcessingQueue = false;
|
|
}
|
|
|
|
private async queueOperation<R>(
|
|
type: QueuedOperation["type"],
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<R> {
|
|
return new Promise<R>((resolve, reject) => {
|
|
const operation: QueuedOperation = {
|
|
type,
|
|
sql,
|
|
params,
|
|
resolve: (value: unknown) => resolve(value as R),
|
|
reject,
|
|
};
|
|
this.operationQueue.push(operation);
|
|
|
|
// If we're already initialized, start processing the queue
|
|
if (this.initialized && this.db) {
|
|
this.processQueue();
|
|
}
|
|
});
|
|
}
|
|
|
|
private async waitForInitialization(): Promise<void> {
|
|
// If we have an initialization promise, wait for it
|
|
if (this.initializationPromise) {
|
|
await this.initializationPromise;
|
|
return;
|
|
}
|
|
|
|
// If not initialized and no promise, start initialization
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
return;
|
|
}
|
|
|
|
// If initialized but no db, something went wrong
|
|
if (!this.db) {
|
|
logger.error(
|
|
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
|
);
|
|
throw new Error(
|
|
`The database could not be initialized. We recommend you restart or reinstall.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Used for inserts, updates, and deletes
|
|
async run(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<{ changes: number; lastId?: number }> {
|
|
await this.waitForInitialization();
|
|
return this.queueOperation<{ changes: number; lastId?: number }>(
|
|
"run",
|
|
sql,
|
|
params,
|
|
);
|
|
}
|
|
|
|
// Note that the resulting array may be empty if there are no results from the query
|
|
async query(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
|
await this.waitForInitialization();
|
|
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
|
}
|
|
|
|
async getOneRow(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<unknown[] | undefined> {
|
|
await this.waitForInitialization();
|
|
return this.queueOperation<unknown[] | undefined>("getOneRow", sql, params);
|
|
}
|
|
|
|
async getAll(sql: string, params: unknown[] = []): Promise<unknown[][]> {
|
|
await this.waitForInitialization();
|
|
return this.queueOperation<unknown[][]>("getAll", sql, params);
|
|
}
|
|
}
|
|
|
|
// Create a singleton instance
|
|
const databaseService = AbsurdSqlDatabaseService.getInstance();
|
|
|
|
export default databaseService;
|