Browse Source

refactor: update electron preload script and type definitions

This commit updates the Electron preload script and type definitions to improve
SQLite integration and IPC communication. The changes include:

- Enhanced preload script (electron/src/preload.ts):
  * Added detailed logging for SQLite operations and IPC communication
  * Implemented retry logic for SQLite operations (3 attempts, 1s delay)
  * Added proper type definitions for SQLite connection options
  * Defined strict channel validation for IPC communication
  * Improved error handling and logging throughout

- Updated type definitions (src/types/electron.d.ts):
  * Aligned ElectronAPI interface with actual implementation
  * Added proper typing for all SQLite operations
  * Added environment variables (platform, isDev)
  * Structured IPC renderer interface with proper method signatures

Current Status:
- Preload script initializes successfully
- SQLite availability check works (returns true)
- SQLite ready signal is properly received
- Database operations are failing with two types of errors:
  1. "CapacitorSQLite not available" during initialization
  2. "Cannot read properties of undefined" for SQLite methods

Next Steps:
- Verify context bridge exposure in renderer process
- Check main process SQLite handlers
- Debug database initialization
- Address Content Security Policy warning

Affected Files:
- Modified: electron/src/preload.ts
- Modified: src/types/electron.d.ts

Note: This is a transitional commit. While the preload script and type
definitions are now properly structured, database operations are not yet
functional. Further debugging and fixes are required to resolve the
SQLite integration issues.
sql-absurd-sql-further
Matthew Raymer 5 days ago
parent
commit
3ec2364394
  1. 117
      electron/src/preload.ts
  2. 17
      scripts/build-electron.cjs
  3. 22
      src/types/electron.d.ts
  4. 7
      tsconfig.electron.json
  5. 1
      vite.config.electron.mts

117
electron/src/preload.ts

@ -7,13 +7,21 @@
import { contextBridge, ipcRenderer } from 'electron';
// Simple logger for preload script
// Enhanced logger for preload script
const logger = {
log: (...args: unknown[]) => console.log('[Preload]', ...args),
error: (...args: unknown[]) => console.error('[Preload]', ...args),
info: (...args: unknown[]) => console.info('[Preload]', ...args),
warn: (...args: unknown[]) => console.warn('[Preload]', ...args),
debug: (...args: unknown[]) => console.debug('[Preload]', ...args),
sqlite: {
log: (operation: string, ...args: unknown[]) =>
console.log('[Preload][SQLite]', operation, ...args),
error: (operation: string, error: unknown) =>
console.error('[Preload][SQLite]', operation, 'failed:', error),
debug: (operation: string, ...args: unknown[]) =>
console.debug('[Preload][SQLite]', operation, ...args)
}
};
// Types for SQLite connection options
@ -30,38 +38,72 @@ interface SQLiteConnectionOptions {
// Define valid channels for security
const VALID_CHANNELS = {
send: ['toMain', 'sqlite-status'],
receive: ['fromMain', 'sqlite-ready', 'database-status'],
invoke: ['sqlite-is-available', 'sqlite-echo', 'sqlite-create-connection', 'sqlite-execute', 'sqlite-query', 'sqlite-run', 'sqlite-close-connection', 'get-path', 'get-base-path']
send: ['toMain', 'sqlite-status'] as const,
receive: ['fromMain', 'sqlite-ready', 'database-status'] as const,
invoke: [
'sqlite-is-available',
'sqlite-echo',
'sqlite-create-connection',
'sqlite-execute',
'sqlite-query',
'sqlite-run',
'sqlite-close-connection',
'get-path',
'get-base-path'
] as const
};
type ValidSendChannel = typeof VALID_CHANNELS.send[number];
type ValidReceiveChannel = typeof VALID_CHANNELS.receive[number];
type ValidInvokeChannel = typeof VALID_CHANNELS.invoke[number];
// Create a secure IPC bridge
const createSecureIPCBridge = () => {
return {
send: (channel: string, data: unknown) => {
if (VALID_CHANNELS.send.includes(channel)) {
if (VALID_CHANNELS.send.includes(channel as ValidSendChannel)) {
logger.debug('IPC Send:', channel, data);
ipcRenderer.send(channel, data);
} else {
logger.warn(`[Preload] Attempted to send on invalid channel: ${channel}`);
}
},
receive: (channel: string, func: (...args: unknown[]) => void) => {
if (VALID_CHANNELS.receive.includes(channel)) {
ipcRenderer.on(channel, (_event, ...args) => func(...args));
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
logger.debug('IPC Receive:', channel);
ipcRenderer.on(channel, (_event, ...args) => {
logger.debug('IPC Received:', channel, args);
func(...args);
});
} else {
logger.warn(`[Preload] Attempted to receive on invalid channel: ${channel}`);
}
},
once: (channel: string, func: (...args: unknown[]) => void) => {
if (VALID_CHANNELS.receive.includes(channel)) {
ipcRenderer.once(channel, (_event, ...args) => func(...args));
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
logger.debug('IPC Once:', channel);
ipcRenderer.once(channel, (_event, ...args) => {
logger.debug('IPC Received Once:', channel, args);
func(...args);
});
} else {
logger.warn(`[Preload] Attempted to receive once on invalid channel: ${channel}`);
}
},
invoke: async (channel: string, ...args: unknown[]) => {
if (VALID_CHANNELS.invoke.includes(channel)) {
return await ipcRenderer.invoke(channel, ...args);
if (VALID_CHANNELS.invoke.includes(channel as ValidInvokeChannel)) {
logger.debug('IPC Invoke:', channel, args);
try {
const result = await ipcRenderer.invoke(channel, ...args);
logger.debug('IPC Invoke Result:', channel, result);
return result;
} catch (error) {
logger.error('IPC Invoke Error:', channel, error);
throw error;
}
} else {
logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`);
throw new Error(`Invalid channel: ${channel}`);
@ -75,57 +117,60 @@ const createSQLiteProxy = () => {
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;
const withRetry = async (operation: (...args: unknown[]) => Promise<unknown>, ...args: unknown[]) => {
let lastError;
const withRetry = async <T>(operation: string, ...args: unknown[]): Promise<T> => {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
return await operation(...args);
logger.sqlite.debug(operation, 'attempt', attempt, args);
const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
logger.sqlite.log(operation, 'success', result);
return result as T;
} catch (error) {
lastError = error;
lastError = error instanceof Error ? error : new Error(String(error));
logger.sqlite.error(operation, error);
if (attempt < MAX_RETRIES) {
logger.warn(`[Preload] SQLite operation failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
}
}
}
throw new Error(`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`);
};
const wrapOperation = (method: string) => {
return async (...args: unknown[]) => {
try {
const handlerName = `sqlite-${method}`;
return await withRetry(ipcRenderer.invoke, handlerName, ...args);
} catch (error) {
logger.error(`[Preload] SQLite ${method} failed:`, error);
throw error;
}
};
throw new Error(`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`);
};
return {
echo: wrapOperation('echo'),
createConnection: wrapOperation('create-connection'),
closeConnection: wrapOperation('close-connection'),
execute: wrapOperation('execute'),
query: wrapOperation('query'),
run: wrapOperation('run'),
isAvailable: wrapOperation('is-available'),
isAvailable: () => withRetry('is-available'),
echo: (value: string) => withRetry('echo', { value }),
createConnection: (options: SQLiteConnectionOptions) => withRetry('create-connection', options),
closeConnection: (options: { database: string }) => withRetry('close-connection', options),
query: (options: { statement: string; values?: unknown[] }) => withRetry('query', options),
run: (options: { statement: string; values?: unknown[] }) => withRetry('run', options),
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => withRetry('execute', options),
getPlatform: () => Promise.resolve('electron')
};
};
try {
// Expose the secure IPC bridge and SQLite proxy
contextBridge.exposeInMainWorld('electron', {
const electronAPI = {
ipcRenderer: createSecureIPCBridge(),
sqlite: createSQLiteProxy(),
env: {
platform: 'electron',
isDev: process.env.NODE_ENV === 'development'
}
};
// Log the exposed API for debugging
logger.debug('Exposing Electron API:', {
hasIpcRenderer: !!electronAPI.ipcRenderer,
hasSqlite: !!electronAPI.sqlite,
sqliteMethods: Object.keys(electronAPI.sqlite),
env: electronAPI.env
});
contextBridge.exposeInMainWorld('electron', electronAPI);
logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully');
} catch (error) {
logger.error('[Preload] Failed to initialize IPC bridge:', error);

17
scripts/build-electron.cjs

@ -49,7 +49,7 @@ indexContent = indexContent.replace(
fs.writeFileSync(finalIndexPath, indexContent);
// Copy preload script to resources
const preloadSrc = path.join(electronDistPath, "preload.js");
const preloadSrc = path.join(electronDistPath, "preload.mjs");
const preloadDest = path.join(electronDistPath, "resources", "preload.js");
// Ensure resources directory exists
@ -59,10 +59,21 @@ if (!fs.existsSync(resourcesDir)) {
}
if (fs.existsSync(preloadSrc)) {
fs.copyFileSync(preloadSrc, preloadDest);
console.log("Preload script copied to resources directory");
// Read the preload script
let preloadContent = fs.readFileSync(preloadSrc, 'utf-8');
// Convert ESM to CommonJS if needed
preloadContent = preloadContent
.replace(/import\s*{\s*([^}]+)\s*}\s*from\s*['"]electron['"];?/g, 'const { $1 } = require("electron");')
.replace(/export\s*{([^}]+)};?/g, '')
.replace(/export\s+default\s+([^;]+);?/g, 'module.exports = $1;');
// Write the modified preload script
fs.writeFileSync(preloadDest, preloadContent);
console.log("Preload script copied and converted to resources directory");
} else {
console.error("Preload script not found at:", preloadSrc);
process.exit(1);
}
// Copy capacitor.config.json to dist-electron

22
src/types/electron.d.ts

@ -1,11 +1,23 @@
interface ElectronAPI {
sqlite: {
isAvailable: () => Promise<boolean>;
execute: (method: string, ...args: unknown[]) => Promise<unknown>;
echo: (value: string) => Promise<{ value: string }>;
createConnection: (options: {
database: string;
version?: number;
readOnly?: boolean;
readonly?: boolean;
encryption?: string;
mode?: string;
useNative?: boolean;
[key: string]: unknown;
}) => Promise<void>;
closeConnection: (options: { database: string }) => Promise<void>;
query: (options: { statement: string; values?: unknown[] }) => Promise<unknown>;
run: (options: { statement: string; values?: unknown[] }) => Promise<unknown>;
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<unknown>;
getPlatform: () => Promise<string>;
};
getPath: (pathType: string) => Promise<string>;
send: (channel: string, data: unknown) => void;
receive: (channel: string, func: (...args: unknown[]) => void) => void;
ipcRenderer: {
on: (channel: string, func: (...args: unknown[]) => void) => void;
once: (channel: string, func: (...args: unknown[]) => void) => void;
@ -14,7 +26,9 @@ interface ElectronAPI {
};
env: {
platform: string;
isDev: boolean;
};
getPath: (pathType: string) => Promise<string>;
getBasePath: () => Promise<string>;
}

7
tsconfig.electron.json

@ -5,7 +5,7 @@
"moduleResolution": "bundler",
"target": "ES2020",
"outDir": "dist-electron",
"rootDir": "src",
"rootDir": ".",
"sourceMap": true,
"esModuleInterop": true,
"allowJs": true,
@ -13,7 +13,7 @@
"isolatedModules": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"types": ["vite/client"],
"types": ["vite/client", "electron"],
"paths": {
"@/*": ["./src/*"]
},
@ -23,6 +23,7 @@
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
"src/**/*.vue",
"electron/src/**/*.ts"
]
}

1
vite.config.electron.mts

@ -7,6 +7,7 @@ export default defineConfig({
rollupOptions: {
input: {
main: path.resolve(__dirname, 'src/electron/main.ts'),
preload: path.resolve(__dirname, 'electron/src/preload.ts')
},
external: [
// Node.js built-ins

Loading…
Cancel
Save