Files
crowd-funder-for-time-pwa/src/services/AbsurdSqlDatabaseService.ts
Matthew Raymer 8c0b547855 fix(typescript): resolve production build errors and add ESLint ignore comments
- Fix TypeScript compilation errors in deepLinks service by replacing logConsoleAndDb with logger.error
- Add ESLint disable comments for necessary 'any' type usage in worker polyfills and Vue mixins
- Add ESLint disable comments for console statements in test files and debugging code
- Production build now succeeds with npm run build:web:prod
- TypeScript compilation passes with npm run type-check

The deepLinks service was using undefined logConsoleAndDb function causing build failures.
Worker context polyfills and Vue mixin complexity require 'any' type usage in specific cases.
Console statements in test files and debugging code are intentionally used for development.
2025-08-12 09:12:46 +00:00

271 lines
8.4 KiB
TypeScript

// **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top
// This prevents "crypto is not defined" errors when running in worker context
if (typeof window === "undefined" && typeof crypto === "undefined") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).crypto = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRandomValues: (array: any) => {
// Simple fallback for worker context
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
return array;
},
};
}
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";
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); // DISABLED
logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
this.initializationPromise = null; // Reset on failure
throw error;
}
}
private async _initialize(): Promise<void> {
if (this.initialized) {
return;
}
// **PLATFORM CHECK**: AbsurdSqlDatabaseService should only run on web-based platforms
// This prevents SharedArrayBuffer checks and web-specific initialization on Electron/Capacitor
// Allow both 'web' (production) and 'development' (dev server) platforms
const webBasedPlatforms = ["web", "development"];
if (!webBasedPlatforms.includes(process.env.VITE_PLATFORM || "")) {
throw new Error(
`AbsurdSqlDatabaseService is only supported on web-based platforms (web, development). Current platform: ${process.env.VITE_PLATFORM}`,
);
}
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.absurd-sql";
// **SHARED ARRAY BUFFER FALLBACK**: Only needed for web platform
// This check handles Safari and other browsers without SharedArrayBuffer support
if (typeof SharedArrayBuffer === "undefined") {
logger.debug(
"[AbsurdSqlDatabaseService] SharedArrayBuffer not available, using fallback mode",
);
const stream = SQL.FS.open(path, "a+");
await stream.node.contents.readIfFallback();
SQL.FS.close(stream);
} else {
logger.debug(
"[AbsurdSqlDatabaseService] SharedArrayBuffer available, using optimized mode",
);
}
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;`);
// Create wrapper functions that match the expected signatures
const sqlExec = async (sql: string, params?: unknown[]): Promise<void> => {
await this.db!.run(sql, params);
};
const sqlQuery = this.db.exec.bind(this.db);
// Extract the migration names for the absurd-sql format
const extractMigrationNames: (result: QueryExecResult[]) => Set<string> = (
result,
) => {
// Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me).
const names = result?.[0]?.values.map((row) => row[0] as string) || [];
return new Set(names);
};
// Run migrations
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
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;
}
operation.resolve(result);
} catch (error) {
logger.error(
"Error while processing SQL queue:",
error,
" ... for sql:",
operation.sql,
" ... with params:",
operation.params,
);
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);
}
}
// Export the service class for lazy instantiation
// The singleton will only be created when actually needed (web platform only)
export default AbsurdSqlDatabaseService;