Browse Source

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.
sql-absurd-sql-further
Matthew Raymer 1 month ago
parent
commit
0b4e885edd
  1. 14
      electron/src/rt/sqlite-error.ts
  2. 79
      electron/src/rt/sqlite-init.ts
  3. 18
      src/libs/util.ts
  4. 27
      src/services/platforms/ElectronPlatformService.ts
  5. 2
      src/types/global.d.ts

14
electron/src/rt/sqlite-error.ts

@ -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';
}
}

79
electron/src/rt/sqlite-init.ts

@ -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;
};

18
src/libs/util.ts

@ -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 (?, ?, ?, ?, ?, ?)`,

27
src/services/platforms/ElectronPlatformService.ts

@ -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",
});

2
src/types/global.d.ts

@ -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;

Loading…
Cancel
Save