You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
721 lines
22 KiB
721 lines
22 KiB
import {
|
|
ImageResult,
|
|
PlatformService,
|
|
PlatformCapabilities,
|
|
QueryExecResult,
|
|
} from "../PlatformService";
|
|
import { logger } from "../../utils/logger";
|
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
|
import { ElectronAPI } from "../../utils/debug-electron";
|
|
|
|
import {
|
|
verifyElectronAPI,
|
|
testSQLiteOperations,
|
|
} from "../../utils/debug-electron";
|
|
|
|
// Extend the global Window interface
|
|
declare global {
|
|
interface Window {
|
|
electron: ElectronAPI;
|
|
}
|
|
}
|
|
|
|
interface Migration {
|
|
name: string;
|
|
sql: string;
|
|
}
|
|
|
|
export interface SQLiteQueryResult {
|
|
values?: Record<string, unknown>[];
|
|
changes?: { changes: number; lastId?: number };
|
|
}
|
|
|
|
/**
|
|
* Shared SQLite initialization state
|
|
* Used to coordinate initialization between main and service
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
export interface SQLiteInitState {
|
|
isReady: boolean;
|
|
isInitializing: boolean;
|
|
error?: Error;
|
|
lastReadyCheck?: number;
|
|
}
|
|
|
|
// Singleton instance for shared state
|
|
const sqliteInitState: SQLiteInitState = {
|
|
isReady: false,
|
|
isInitializing: false,
|
|
lastReadyCheck: 0,
|
|
};
|
|
|
|
/**
|
|
* Interface defining SQLite database operations
|
|
* @author Matthew Raymer
|
|
*/
|
|
interface SQLiteOperations {
|
|
createConnection: (options: {
|
|
database: string;
|
|
encrypted: boolean;
|
|
mode: string;
|
|
}) => Promise<void>;
|
|
query: (options: {
|
|
database: string;
|
|
statement: string;
|
|
values?: unknown[];
|
|
}) => Promise<{ values?: unknown[] }>;
|
|
execute: (options: { database: string; statements: string }) => Promise<void>;
|
|
run: (options: {
|
|
database: string;
|
|
statement: string;
|
|
values?: unknown[];
|
|
}) => Promise<{ changes?: { changes: number; lastId?: number } }>;
|
|
}
|
|
|
|
/**
|
|
* Platform service implementation for Electron (desktop) platform.
|
|
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
|
* - File system operations (TODO)
|
|
* - Camera integration (TODO)
|
|
* - SQLite database operations
|
|
* - System-level features (TODO)
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
export class ElectronPlatformService implements PlatformService {
|
|
private sqlite: SQLiteOperations | null = null;
|
|
private dbName = "timesafari";
|
|
private isInitialized = false;
|
|
private dbFatalError = false;
|
|
private sqliteReadyPromise: Promise<void> | null = null;
|
|
private initializationTimeout: NodeJS.Timeout | null = null;
|
|
|
|
// SQLite initialization configuration
|
|
private static readonly SQLITE_CONFIG = {
|
|
INITIALIZATION: {
|
|
TIMEOUT_MS: 1000, // with retries, stay under 5 seconds
|
|
RETRY_ATTEMPTS: 3,
|
|
RETRY_DELAY_MS: 1000,
|
|
READY_CHECK_INTERVAL_MS: 100, // How often to check if SQLite is already ready
|
|
},
|
|
};
|
|
|
|
constructor() {
|
|
this.sqliteReadyPromise = new Promise<void>((resolve, reject) => {
|
|
let retryCount = 0;
|
|
|
|
const cleanup = () => {
|
|
if (this.initializationTimeout) {
|
|
clearTimeout(this.initializationTimeout);
|
|
this.initializationTimeout = null;
|
|
}
|
|
};
|
|
|
|
const checkExistingReadiness = async (): Promise<boolean> => {
|
|
try {
|
|
if (!window.electron?.ipcRenderer) {
|
|
return false;
|
|
}
|
|
|
|
// Check if SQLite is already available
|
|
const isAvailable = await window.electron.ipcRenderer.invoke(
|
|
"sqlite-is-available",
|
|
);
|
|
if (!isAvailable) {
|
|
return false;
|
|
}
|
|
|
|
// Check if database is already open
|
|
const isOpen = await window.electron.ipcRenderer.invoke(
|
|
"sqlite-is-db-open",
|
|
{
|
|
database: this.dbName,
|
|
},
|
|
);
|
|
|
|
if (isOpen) {
|
|
logger.info(
|
|
"[ElectronPlatformService] SQLite is already ready and database is open",
|
|
);
|
|
sqliteInitState.isReady = true;
|
|
sqliteInitState.isInitializing = false;
|
|
sqliteInitState.lastReadyCheck = Date.now();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
logger.warn(
|
|
"[ElectronPlatformService] Error checking existing readiness:",
|
|
error,
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const attemptInitialization = async () => {
|
|
cleanup(); // Clear any existing timeout
|
|
|
|
// Check if SQLite is already ready
|
|
if (await checkExistingReadiness()) {
|
|
this.isInitialized = true;
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
// If someone else is initializing, wait for them
|
|
if (sqliteInitState.isInitializing) {
|
|
logger.info(
|
|
"[ElectronPlatformService] Another initialization in progress, waiting...",
|
|
);
|
|
setTimeout(
|
|
attemptInitialization,
|
|
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
|
|
.READY_CHECK_INTERVAL_MS,
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
sqliteInitState.isInitializing = true;
|
|
|
|
// Verify Electron API exposure first
|
|
await verifyElectronAPI();
|
|
logger.info(
|
|
"[ElectronPlatformService] Electron API verification successful",
|
|
);
|
|
|
|
if (!window.electron?.ipcRenderer) {
|
|
logger.warn("[ElectronPlatformService] IPC renderer not available");
|
|
reject(new Error("IPC renderer not available"));
|
|
return;
|
|
}
|
|
|
|
// Set timeout for this attempt
|
|
this.initializationTimeout = setTimeout(() => {
|
|
if (
|
|
retryCount <
|
|
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
|
|
.RETRY_ATTEMPTS
|
|
) {
|
|
retryCount++;
|
|
logger.warn(
|
|
`[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`,
|
|
);
|
|
setTimeout(
|
|
attemptInitialization,
|
|
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
|
|
.RETRY_DELAY_MS,
|
|
);
|
|
} else {
|
|
cleanup();
|
|
sqliteInitState.isInitializing = false;
|
|
sqliteInitState.error = new Error(
|
|
"SQLite initialization timeout after all retries",
|
|
);
|
|
logger.error(
|
|
"[ElectronPlatformService] SQLite initialization failed after all retries",
|
|
);
|
|
reject(sqliteInitState.error);
|
|
}
|
|
}, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS);
|
|
|
|
// Set up ready signal handler
|
|
window.electron.ipcRenderer.once("sqlite-ready", async () => {
|
|
cleanup();
|
|
logger.info(
|
|
"[ElectronPlatformService] Received SQLite ready signal",
|
|
);
|
|
|
|
try {
|
|
// Test SQLite operations after receiving ready signal
|
|
await testSQLiteOperations();
|
|
logger.info(
|
|
"[ElectronPlatformService] SQLite operations test successful",
|
|
);
|
|
|
|
this.isInitialized = true;
|
|
sqliteInitState.isReady = true;
|
|
sqliteInitState.isInitializing = false;
|
|
sqliteInitState.lastReadyCheck = Date.now();
|
|
resolve();
|
|
} catch (error) {
|
|
sqliteInitState.error = error as Error;
|
|
sqliteInitState.isInitializing = false;
|
|
logger.error(
|
|
"[ElectronPlatformService] SQLite operations test failed:",
|
|
error,
|
|
);
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
// Set up error handler
|
|
window.electron.ipcRenderer.once(
|
|
"database-status",
|
|
(...args: unknown[]) => {
|
|
cleanup();
|
|
const status = args[0] as { status: string; error?: string };
|
|
if (status.status === "error") {
|
|
this.dbFatalError = true;
|
|
sqliteInitState.error = new Error(
|
|
status.error || "Database initialization failed",
|
|
);
|
|
sqliteInitState.isInitializing = false;
|
|
reject(sqliteInitState.error);
|
|
}
|
|
},
|
|
);
|
|
} catch (error) {
|
|
cleanup();
|
|
sqliteInitState.error = error as Error;
|
|
sqliteInitState.isInitializing = false;
|
|
logger.error(
|
|
"[ElectronPlatformService] Initialization failed:",
|
|
error,
|
|
);
|
|
reject(error);
|
|
}
|
|
};
|
|
|
|
// Start first initialization attempt
|
|
attemptInitialization();
|
|
});
|
|
}
|
|
|
|
private async initializeDatabase(): Promise<void> {
|
|
if (this.isInitialized) return;
|
|
if (this.sqliteReadyPromise) await this.sqliteReadyPromise;
|
|
|
|
if (!window.electron?.sqlite) {
|
|
throw new Error("SQLite IPC bridge not available");
|
|
}
|
|
|
|
// Use IPC bridge with specific methods
|
|
this.sqlite = {
|
|
createConnection: async (options) => {
|
|
await window.electron.ipcRenderer.invoke('sqlite-create-connection', options);
|
|
},
|
|
query: async (options) => {
|
|
return await window.electron.ipcRenderer.invoke('sqlite-query', options);
|
|
},
|
|
run: async (options) => {
|
|
return await window.electron.ipcRenderer.invoke('sqlite-run', options);
|
|
},
|
|
execute: async (options) => {
|
|
await window.electron.ipcRenderer.invoke('sqlite-execute', {
|
|
database: options.database,
|
|
statements: [{ statement: options.statements }]
|
|
});
|
|
}
|
|
} as SQLiteOperations;
|
|
|
|
// Create the connection (idempotent)
|
|
await this.sqlite!.createConnection({
|
|
database: this.dbName,
|
|
encrypted: false,
|
|
mode: "no-encryption",
|
|
});
|
|
|
|
// Optionally, test the connection
|
|
await this.sqlite!.query({
|
|
database: this.dbName,
|
|
statement: "SELECT 1",
|
|
});
|
|
|
|
// Run migrations if needed
|
|
await this.runMigrations();
|
|
logger.info("[ElectronPlatformService] Database initialized successfully");
|
|
}
|
|
|
|
private async runMigrations(): Promise<void> {
|
|
if (!this.sqlite) {
|
|
throw new Error("SQLite not initialized");
|
|
}
|
|
|
|
// Create migrations table if it doesn't exist
|
|
await this.sqlite.execute({
|
|
database: this.dbName,
|
|
statements: `CREATE TABLE IF NOT EXISTS migrations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);`,
|
|
});
|
|
|
|
// Get list of executed migrations
|
|
const result = await this.sqlite.query({
|
|
database: this.dbName,
|
|
statement: "SELECT name FROM migrations;",
|
|
});
|
|
const executedMigrations = new Set(
|
|
(result.values as unknown[][])?.map(
|
|
(row: unknown[]) => row[0] as string,
|
|
) || [],
|
|
);
|
|
// Run pending migrations in order
|
|
const migrations: Migration[] = [
|
|
{
|
|
name: "001_initial",
|
|
sql: `
|
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
dateCreated TEXT NOT NULL,
|
|
derivationPath TEXT,
|
|
did TEXT NOT NULL,
|
|
identityEncrBase64 TEXT,
|
|
mnemonicEncrBase64 TEXT,
|
|
passkeyCredIdHex TEXT,
|
|
publicKeyHex TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
|
|
|
|
CREATE TABLE IF NOT EXISTS secret (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
secretBase64 TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
accountDid TEXT,
|
|
activeDid TEXT,
|
|
apiServer TEXT,
|
|
filterFeedByNearby BOOLEAN,
|
|
filterFeedByVisible BOOLEAN,
|
|
finishedOnboarding BOOLEAN,
|
|
firstName TEXT,
|
|
hideRegisterPromptOnNewContact BOOLEAN,
|
|
isRegistered BOOLEAN,
|
|
lastName TEXT,
|
|
lastAckedOfferToUserJwtId TEXT,
|
|
lastAckedOfferToUserProjectsJwtId TEXT,
|
|
lastNotifiedClaimId TEXT,
|
|
lastViewedClaimId TEXT,
|
|
notifyingNewActivityTime TEXT,
|
|
notifyingReminderMessage TEXT,
|
|
notifyingReminderTime TEXT,
|
|
partnerApiServer TEXT,
|
|
passkeyExpirationMinutes INTEGER,
|
|
profileImageUrl TEXT,
|
|
searchBoxes TEXT,
|
|
showContactGivesInline BOOLEAN,
|
|
showGeneralAdvanced BOOLEAN,
|
|
showShortcutBvc BOOLEAN,
|
|
vapid TEXT,
|
|
warnIfProdServer BOOLEAN,
|
|
warnIfTestServer BOOLEAN,
|
|
webPushServer TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
|
|
|
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
|
|
|
CREATE TABLE IF NOT EXISTS contacts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
did TEXT NOT NULL,
|
|
name TEXT,
|
|
contactMethods TEXT,
|
|
nextPubKeyHashB64 TEXT,
|
|
notes TEXT,
|
|
profileImageUrl TEXT,
|
|
publicKeyBase64 TEXT,
|
|
seesMe BOOLEAN,
|
|
registered BOOLEAN
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
|
|
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
|
|
|
CREATE TABLE IF NOT EXISTS logs (
|
|
date TEXT PRIMARY KEY,
|
|
message TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS temp (
|
|
id TEXT PRIMARY KEY,
|
|
blobB64 TEXT
|
|
);
|
|
`,
|
|
},
|
|
];
|
|
for (const migration of migrations) {
|
|
if (!executedMigrations.has(migration.name)) {
|
|
await this.sqlite.execute({
|
|
database: this.dbName,
|
|
statements: migration.sql,
|
|
});
|
|
await this.sqlite.run({
|
|
database: this.dbName,
|
|
statement: "INSERT INTO migrations (name) VALUES (?)",
|
|
values: [migration.name],
|
|
});
|
|
logger.log(`Migration ${migration.name} executed successfully`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the capabilities of the Electron platform
|
|
* @returns Platform capabilities object
|
|
*/
|
|
getCapabilities(): PlatformCapabilities {
|
|
return {
|
|
hasFileSystem: false, // Not implemented yet
|
|
hasCamera: false, // Not implemented yet
|
|
isMobile: false,
|
|
isIOS: false,
|
|
hasFileDownload: false, // Not implemented yet
|
|
needsFileHandlingInstructions: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads a file from the filesystem.
|
|
* @param _path - Path to the file to read
|
|
* @returns Promise that should resolve to file contents
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement file reading using Electron's file system API
|
|
*/
|
|
async readFile(_path: string): Promise<string> {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Writes content to a file.
|
|
* @param _path - Path where to write the file
|
|
* @param _content - Content to write to the file
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement file writing using Electron's file system API
|
|
*/
|
|
async writeFile(_path: string, _content: string): Promise<void> {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Writes content to a file and opens the system share dialog.
|
|
* @param _fileName - Name of the file to create
|
|
* @param _content - Content to write to the file
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement using Electron's dialog and file system APIs
|
|
*/
|
|
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Deletes a file from the filesystem.
|
|
* @param _path - Path to the file to delete
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement file deletion using Electron's file system API
|
|
*/
|
|
async deleteFile(_path: string): Promise<void> {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Lists files in the specified directory.
|
|
* @param _directory - Path to the directory to list
|
|
* @returns Promise that should resolve to array of filenames
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement directory listing using Electron's file system API
|
|
*/
|
|
async listFiles(_directory: string): Promise<string[]> {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Should open system camera to take a picture.
|
|
* @returns Promise that should resolve to captured image data
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement camera access using Electron's media APIs
|
|
*/
|
|
async takePicture(): Promise<ImageResult> {
|
|
logger.error("takePicture not implemented in Electron platform");
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Should open system file picker for selecting an image.
|
|
* @returns Promise that should resolve to selected image data
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement file picker using Electron's dialog API
|
|
*/
|
|
async pickImage(): Promise<ImageResult> {
|
|
logger.error("pickImage not implemented in Electron platform");
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Should handle deep link URLs for the desktop application.
|
|
* @param _url - The deep link URL to handle
|
|
* @throws Error with "Not implemented" message
|
|
* @todo Implement deep link handling using Electron's protocol handler
|
|
*/
|
|
async handleDeepLink(_url: string): Promise<void> {
|
|
logger.error("handleDeepLink not implemented in Electron platform");
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
/**
|
|
* Executes a database query with proper connection lifecycle management.
|
|
* Opens connection, executes query, and ensures proper cleanup.
|
|
*
|
|
* @param sql - SQL query to execute
|
|
* @param params - Optional parameters for the query
|
|
* @returns Promise resolving to query results
|
|
* @throws Error if database operations fail
|
|
*/
|
|
async dbQuery<T = unknown>(
|
|
sql: string,
|
|
params: unknown[] = [],
|
|
): Promise<QueryExecResult<T>> {
|
|
logger.debug(
|
|
"[ElectronPlatformService] [dbQuery] TEMPORARY TEST: Returning empty result for query:",
|
|
{
|
|
sql,
|
|
params,
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
);
|
|
|
|
// TEMPORARY TEST: Return empty result
|
|
return {
|
|
columns: [],
|
|
values: [],
|
|
};
|
|
|
|
// Original implementation commented out for testing
|
|
/*
|
|
if (this.dbFatalError) {
|
|
throw new Error("Database is in a fatal error state. Please restart the app.");
|
|
}
|
|
|
|
if (!window.electron?.ipcRenderer) {
|
|
throw new Error("IPC renderer not available");
|
|
}
|
|
|
|
try {
|
|
// Check SQLite availability first
|
|
const isAvailable = await window.electron.ipcRenderer.invoke('sqlite-is-available');
|
|
if (!isAvailable) {
|
|
throw new Error('[ElectronPlatformService] [dbQuery] SQLite is not available');
|
|
}
|
|
logger.debug("[ElectronPlatformService] [dbQuery] SQLite is available");
|
|
|
|
// Create database connection
|
|
await window.electron.ipcRenderer.invoke('sqlite-create-connection', {
|
|
database: this.dbName,
|
|
version: 1
|
|
});
|
|
logger.debug("[ElectronPlatformService] [dbQuery] Database connection created");
|
|
|
|
// Open database
|
|
await window.electron.ipcRenderer.invoke('sqlite-open', {
|
|
database: this.dbName
|
|
});
|
|
logger.debug("[ElectronPlatformService] [dbQuery] Database opened");
|
|
|
|
// Verify database is open
|
|
const isOpen = await window.electron.ipcRenderer.invoke('sqlite-is-db-open', {
|
|
database: this.dbName
|
|
});
|
|
if (!isOpen) {
|
|
throw new Error('[ElectronPlatformService] [dbQuery] Database failed to open');
|
|
}
|
|
|
|
// Execute query
|
|
const result = await window.electron.ipcRenderer.invoke('sqlite-query', {
|
|
database: this.dbName,
|
|
statement: sql,
|
|
values: params
|
|
}) as SQLiteQueryResult;
|
|
logger.debug("[ElectronPlatformService] [dbQuery] Query executed successfully");
|
|
|
|
// Process results
|
|
const columns = result.values?.[0] ? Object.keys(result.values[0]) : [];
|
|
const processedResult = {
|
|
columns,
|
|
values: (result.values || []).map((row: Record<string, unknown>) => row as T)
|
|
};
|
|
|
|
return processedResult;
|
|
} catch (error) {
|
|
logger.error("[ElectronPlatformService] [dbQuery] Query failed:", error);
|
|
throw error;
|
|
} finally {
|
|
// Ensure proper cleanup
|
|
try {
|
|
// Close database
|
|
await window.electron.ipcRenderer.invoke('sqlite-close', {
|
|
database: this.dbName
|
|
});
|
|
logger.debug("[ElectronPlatformService] [dbQuery] Database closed");
|
|
|
|
// Close connection
|
|
await window.electron.ipcRenderer.invoke('sqlite-close-connection', {
|
|
database: this.dbName
|
|
});
|
|
logger.debug("[ElectronPlatformService] [dbQuery] Database connection closed");
|
|
} catch (closeError) {
|
|
logger.error("[ElectronPlatformService] [dbQuery] Failed to cleanup database:", closeError);
|
|
// Don't throw here - we want to preserve the original error if any
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* @see PlatformService.dbExec
|
|
*/
|
|
async dbExec(
|
|
sql: string,
|
|
params?: unknown[],
|
|
): Promise<{ changes: number; lastId?: number }> {
|
|
await this.initializeDatabase();
|
|
if (this.dbFatalError) {
|
|
throw new Error(
|
|
"Database is in a fatal error state. Please restart the app.",
|
|
);
|
|
}
|
|
if (!this.sqlite) {
|
|
throw new Error("SQLite not initialized");
|
|
}
|
|
const result = await this.sqlite.run({
|
|
database: this.dbName,
|
|
statement: sql,
|
|
values: params,
|
|
});
|
|
return {
|
|
changes: result.changes?.changes || 0,
|
|
lastId: result.changes?.lastId,
|
|
};
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
await this.initializeDatabase();
|
|
}
|
|
|
|
async execute(sql: string, params: unknown[] = []): Promise<void> {
|
|
await this.initializeDatabase();
|
|
if (this.dbFatalError) {
|
|
throw new Error(
|
|
"Database is in a fatal error state. Please restart the app.",
|
|
);
|
|
}
|
|
if (!this.sqlite) {
|
|
throw new Error("SQLite not initialized");
|
|
}
|
|
await this.sqlite.run({
|
|
database: this.dbName,
|
|
statement: sql,
|
|
values: params,
|
|
});
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
// Optionally implement close logic if needed
|
|
}
|
|
}
|
|
|