fix(electron): correct SQLite IPC bridge implementation

- Replace generic execute method with specific IPC handlers
- Fix database operations by using proper IPC methods (createConnection, query, run, execute)
- Update type definitions to match actual IPC bridge interface
- Fix "Must provide statements" error by using correct method signatures

This change ensures proper communication between renderer and main processes
for SQLite operations, resolving database initialization and query issues.
This commit is contained in:
Matthew Raymer
2025-06-05 06:52:26 +00:00
parent b6d9b29720
commit 0b4e885edd
5 changed files with 127 additions and 13 deletions

View File

@@ -0,0 +1,14 @@
/**
* Custom error class for SQLite operations
* Provides additional context and error tracking for SQLite operations
*/
export class SQLiteError extends Error {
constructor(
message: string,
public operation: string,
public cause?: unknown
) {
super(message);
this.name = 'SQLiteError';
}
}

View File

@@ -266,12 +266,16 @@ const verifyTransactionState = async (database: string): Promise<boolean> => {
// Check if we're in a transaction
const isActive = await pluginState.instance.isTransactionActive({ database });
transactionState.isActive = isActive;
transactionState.lastVerified = new Date();
transactionState.database = database;
// Only update state if it's different
if (isActive !== transactionState.isActive || transactionState.database !== database) {
transactionState.isActive = isActive;
transactionState.lastVerified = new Date();
transactionState.database = isActive ? database : null;
}
return true;
} catch (error) {
// Reset state on error
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
@@ -896,4 +900,71 @@ export function setupSQLiteHandlers(): void {
});
logger.info('SQLite IPC handlers setup complete');
}
}
// Update transaction management to be more careful
const beginTransaction = async (database: string): Promise<void> => {
if (!pluginState.instance || !pluginState.isAvailable) {
throw new SQLiteError('Database not available', 'beginTransaction');
}
// Verify current state first
await verifyTransactionState(database);
if (transactionState.isActive) {
throw new SQLiteError('Transaction already active', 'beginTransaction');
}
try {
await pluginState.instance.beginTransaction({ database });
transactionState.isActive = true;
transactionState.lastVerified = new Date();
transactionState.database = database;
} catch (error) {
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
throw new SQLiteError('Failed to begin transaction', 'beginTransaction', error);
}
};
const commitTransaction = async (database: string): Promise<void> => {
if (!pluginState.instance || !pluginState.isAvailable) {
throw new SQLiteError('Database not available', 'commitTransaction');
}
// Verify current state first
await verifyTransactionState(database);
if (!transactionState.isActive || transactionState.database !== database) {
throw new SQLiteError('No active transaction', 'commitTransaction');
}
try {
await pluginState.instance.commitTransaction({ database });
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
} catch (error) {
// Don't reset state on error - let rollback handle it
throw new SQLiteError('Failed to commit transaction', 'commitTransaction', error);
}
};
const rollbackTransaction = async (database: string): Promise<void> => {
if (!pluginState.instance || !pluginState.isAvailable) {
return; // Just return if plugin not available
}
// Only attempt rollback if we think we're in a transaction
if (transactionState.isActive && transactionState.database === database) {
try {
await pluginState.instance.rollbackTransaction({ database });
} catch (error) {
logger.error('Rollback failed:', error);
}
}
// Always reset state after rollback attempt
transactionState.isActive = false;
transactionState.lastVerified = new Date();
transactionState.database = null;
};

View File

@@ -646,17 +646,29 @@ export async function saveNewIdentity(
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
// If no secret exists, create one
let secretBase64: string;
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
// Generate a new secret
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
secretBase64 = arrayBufferToBase64(randomBytes);
// Store the new secret
await platformService.dbExec(
`INSERT INTO secret (id, secretBase64) VALUES (1, ?)`,
[secretBase64]
);
} else {
secretBase64 = secrets.values[0][0] as string;
}
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const encryptedIdentity = await simpleEncrypt(identity, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
await platformService.dbExec(
`INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`,

View File

@@ -288,21 +288,38 @@ export class ElectronPlatformService implements PlatformService {
if (this.isInitialized) return;
if (this.sqliteReadyPromise) await this.sqliteReadyPromise;
if (!window.CapacitorSQLite) {
throw new Error("CapacitorSQLite not available");
if (!window.electron?.sqlite) {
throw new Error("SQLite IPC bridge not available");
}
this.sqlite = window.CapacitorSQLite as unknown as SQLiteOperations;
// 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({
await this.sqlite!.createConnection({
database: this.dbName,
encrypted: false,
mode: "no-encryption",
});
// Optionally, test the connection
await this.sqlite.query({
await this.sqlite!.query({
database: this.dbName,
statement: "SELECT 1",
});

View File

@@ -16,7 +16,7 @@ declare global {
electron: {
sqlite: {
isAvailable: () => Promise<boolean>;
execute: (method: string, ...args: unknown[]) => Promise<unknown>;
execute: (options: { method: string; database?: string; statement?: string; values?: unknown[]; statements?: string; encrypted?: boolean; mode?: string }) => Promise<unknown>;
};
// Add other electron IPC methods as needed
getPath: (pathType: string) => string;