- 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.
271 lines
8.4 KiB
TypeScript
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;
|