fix(electron): consolidate SQLite initialization and IPC handling
- Consolidate preload script with secure IPC bridge and channel validation - Ensure single initialization path in main process - Add robust error handling and user feedback - Fix race conditions in window creation and SQLite ready signal Current state: - SQLite initializes successfully in main process - IPC bridge is established and events are transmitted - Window creation and loading sequence is correct - Renderer receives ready signal and mounts app - Database operations still fail in renderer due to connection issues Known issues: - SQLite proxy not properly initialized in renderer - Database connection not established in renderer - Error logging attempts to use database before ready - Connection state management needs improvement This commit represents a stable point where IPC communication is working but database operations need to be fixed.
This commit is contained in:
@@ -40,68 +40,79 @@ if (electronIsDev) {
|
||||
// Run Application
|
||||
(async () => {
|
||||
try {
|
||||
// Wait for electron app to be ready.
|
||||
// Wait for electron app to be ready first
|
||||
await app.whenReady();
|
||||
|
||||
// Security - Set Content-Security-Policy based on whether or not we are in dev mode.
|
||||
console.log('[Electron Main Process] App is ready');
|
||||
|
||||
// Initialize SQLite plugin and handlers BEFORE creating any windows
|
||||
console.log('[Electron Main Process] Initializing SQLite...');
|
||||
setupSQLiteHandlers();
|
||||
await initializeSQLite();
|
||||
console.log('[Electron Main Process] SQLite initialization complete');
|
||||
|
||||
// Security - Set Content-Security-Policy
|
||||
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
|
||||
|
||||
// Initialize our app, build windows, and load content first
|
||||
// Initialize our app and create window
|
||||
console.log('[Electron Main Process] Starting app initialization...');
|
||||
await myCapacitorApp.init();
|
||||
console.log('[Electron Main Process] App initialization complete');
|
||||
|
||||
// Get the main window and wait for it to be ready
|
||||
// Get the main window
|
||||
const mainWindow = myCapacitorApp.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
throw new Error('Main window not available after app initialization');
|
||||
}
|
||||
|
||||
// Wait for window to be ready
|
||||
// Wait for window to be ready and loaded
|
||||
await new Promise<void>((resolve) => {
|
||||
if (mainWindow.isVisible()) {
|
||||
resolve();
|
||||
} else {
|
||||
mainWindow.once('show', () => resolve());
|
||||
}
|
||||
const handleReady = () => {
|
||||
console.log('[Electron Main Process] Window ready to show');
|
||||
mainWindow.show();
|
||||
|
||||
// Wait for window to finish loading
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
console.log('[Electron Main Process] Window finished loading');
|
||||
|
||||
// Send SQLite ready signal after window is fully loaded
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('sqlite-ready');
|
||||
console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
|
||||
} else {
|
||||
console.warn('[Electron Main Process] Window was destroyed before sending SQLite ready signal');
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
// Always use the event since isReadyToShow is not reliable
|
||||
mainWindow.once('ready-to-show', handleReady);
|
||||
});
|
||||
|
||||
// Now initialize SQLite after window is ready
|
||||
console.log('[Electron Main Process] Starting SQLite initialization...');
|
||||
try {
|
||||
// Register handlers first to prevent "no handler" errors
|
||||
setupSQLiteHandlers();
|
||||
console.log('[Electron Main Process] SQLite handlers registered');
|
||||
|
||||
// Then initialize the plugin
|
||||
await initializeSQLite();
|
||||
console.log('[Electron Main Process] SQLite plugin initialized successfully');
|
||||
|
||||
// Send SQLite ready signal since window is ready
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('sqlite-ready');
|
||||
console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
|
||||
} else {
|
||||
console.warn('[Electron Main Process] Could not send SQLite ready signal - window was destroyed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Electron Main Process] Failed to initialize SQLite:', error);
|
||||
// Notify renderer about database status
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('database-status', {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
// Don't proceed with app initialization if SQLite fails
|
||||
throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// Check for updates if we are in a packaged app.
|
||||
// Check for updates if we are in a packaged app
|
||||
if (!electronIsDev) {
|
||||
console.log('[Electron Main Process] Checking for updates...');
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
|
||||
// Handle window close
|
||||
mainWindow.on('closed', () => {
|
||||
console.log('[Electron Main Process] Main window closed');
|
||||
});
|
||||
|
||||
// Handle window close request
|
||||
mainWindow.on('close', (event) => {
|
||||
console.log('[Electron Main Process] Window close requested');
|
||||
if (mainWindow.webContents.isLoading()) {
|
||||
event.preventDefault();
|
||||
console.log('[Electron Main Process] Deferring window close due to loading state');
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
mainWindow.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Electron Main Process] Fatal error during initialization:', error);
|
||||
app.quit();
|
||||
|
||||
@@ -28,50 +28,81 @@ interface SQLiteConnectionOptions {
|
||||
[key: string]: unknown; // Allow other properties
|
||||
}
|
||||
|
||||
// Create a proxy for the CapacitorSQLite plugin
|
||||
// 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']
|
||||
};
|
||||
|
||||
// Create a secure IPC bridge
|
||||
const createSecureIPCBridge = () => {
|
||||
return {
|
||||
send: (channel: string, data: unknown) => {
|
||||
if (VALID_CHANNELS.send.includes(channel)) {
|
||||
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));
|
||||
} 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));
|
||||
} 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);
|
||||
} else {
|
||||
logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`);
|
||||
throw new Error(`Invalid channel: ${channel}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create SQLite proxy with retry logic
|
||||
const createSQLiteProxy = () => {
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY = 1000; // 1 second
|
||||
const RETRY_DELAY = 1000;
|
||||
|
||||
const withRetry = async <T>(operation: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T> => {
|
||||
let lastError: Error | null = null;
|
||||
const withRetry = async (operation: (...args: unknown[]) => Promise<unknown>, ...args: unknown[]) => {
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
return await operation(...args);
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
lastError = error;
|
||||
if (attempt < MAX_RETRIES) {
|
||||
logger.warn(`[CapacitorSQLite] 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(`[CapacitorSQLite] SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
|
||||
throw new Error(`SQLite operation failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`);
|
||||
};
|
||||
|
||||
const wrapOperation = (method: string) => {
|
||||
return async (...args: unknown[]): Promise<unknown> => {
|
||||
return async (...args: unknown[]) => {
|
||||
try {
|
||||
// For createConnection, ensure readOnly is false
|
||||
if (method === 'create-connection') {
|
||||
const options = args[0] as SQLiteConnectionOptions;
|
||||
if (options && typeof options === 'object') {
|
||||
// Set readOnly to false and ensure mode is rwc
|
||||
options.readOnly = false;
|
||||
options.mode = 'rwc';
|
||||
// Remove any lowercase readonly property if it exists
|
||||
delete options.readonly;
|
||||
}
|
||||
}
|
||||
return await withRetry(ipcRenderer.invoke, 'sqlite-' + method, ...args);
|
||||
const handlerName = `sqlite-${method}`;
|
||||
return await withRetry(ipcRenderer.invoke, handlerName, ...args);
|
||||
} catch (error) {
|
||||
logger.error(`[CapacitorSQLite] SQLite ${method} failed:`, error);
|
||||
throw new Error(`[CapacitorSQLite] Database operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
logger.error(`[Preload] SQLite ${method} failed:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create a proxy that matches the CapacitorSQLite interface
|
||||
return {
|
||||
echo: wrapOperation('echo'),
|
||||
createConnection: wrapOperation('create-connection'),
|
||||
@@ -80,24 +111,25 @@ const createSQLiteProxy = () => {
|
||||
query: wrapOperation('query'),
|
||||
run: wrapOperation('run'),
|
||||
isAvailable: wrapOperation('is-available'),
|
||||
getPlatform: () => Promise.resolve('electron'),
|
||||
// Add other methods as needed
|
||||
getPlatform: () => Promise.resolve('electron')
|
||||
};
|
||||
};
|
||||
|
||||
// Expose the Electron IPC API
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer: {
|
||||
on: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
|
||||
once: (channel: string, func: (...args: unknown[]) => void) => ipcRenderer.once(channel, (event, ...args) => func(...args)),
|
||||
send: (channel: string, data: unknown) => ipcRenderer.send(channel, data),
|
||||
invoke: (channel: string, ...args: unknown[]) => ipcRenderer.invoke(channel, ...args),
|
||||
},
|
||||
// Add other APIs as needed
|
||||
});
|
||||
try {
|
||||
// Expose the secure IPC bridge and SQLite proxy
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer: createSecureIPCBridge(),
|
||||
sqlite: createSQLiteProxy(),
|
||||
env: {
|
||||
platform: 'electron',
|
||||
isDev: process.env.NODE_ENV === 'development'
|
||||
}
|
||||
});
|
||||
|
||||
// Expose CapacitorSQLite proxy as before
|
||||
contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy());
|
||||
logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('[Preload] Failed to initialize IPC bridge:', error);
|
||||
}
|
||||
|
||||
// Log startup
|
||||
logger.log('[CapacitorSQLite] Preload script starting...');
|
||||
|
||||
@@ -531,6 +531,7 @@ export function setupSQLiteHandlers(): void {
|
||||
'sqlite-create-connection',
|
||||
'sqlite-execute',
|
||||
'sqlite-query',
|
||||
'sqlite-run',
|
||||
'sqlite-close-connection',
|
||||
'sqlite-get-error'
|
||||
];
|
||||
@@ -578,7 +579,69 @@ export function setupSQLiteHandlers(): void {
|
||||
}
|
||||
});
|
||||
|
||||
// ... other handlers ...
|
||||
// Add the sqlite-run handler
|
||||
ipcMain.handle('sqlite-run', async (_event, options) => {
|
||||
try {
|
||||
if (!await verifyPluginState()) {
|
||||
throw new SQLiteError('Plugin not available', 'sqlite-run');
|
||||
}
|
||||
|
||||
const { statement, values } = options;
|
||||
if (!statement) {
|
||||
throw new SQLiteError('No statement provided', 'sqlite-run');
|
||||
}
|
||||
|
||||
// Add database name to options
|
||||
const runOptions = {
|
||||
database: 'timesafari', // Use the same database name as in initialization
|
||||
statement,
|
||||
values: values || []
|
||||
};
|
||||
|
||||
logger.debug('Running SQL statement:', runOptions);
|
||||
const result = await pluginState.instance.run(runOptions);
|
||||
|
||||
if (!result) {
|
||||
throw new SQLiteError('Run operation returned no result', 'sqlite-run');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw handleError(error, 'sqlite-run');
|
||||
}
|
||||
});
|
||||
|
||||
// Add the sqlite-query handler
|
||||
ipcMain.handle('sqlite-query', async (_event, options) => {
|
||||
try {
|
||||
if (!await verifyPluginState()) {
|
||||
throw new SQLiteError('Plugin not available', 'sqlite-query');
|
||||
}
|
||||
|
||||
const { statement, values } = options;
|
||||
if (!statement) {
|
||||
throw new SQLiteError('No statement provided', 'sqlite-query');
|
||||
}
|
||||
|
||||
// Add database name to options
|
||||
const queryOptions = {
|
||||
database: 'timesafari', // Use the same database name as in initialization
|
||||
statement,
|
||||
values: values || []
|
||||
};
|
||||
|
||||
logger.debug('Executing SQL query:', queryOptions);
|
||||
const result = await pluginState.instance.query(queryOptions);
|
||||
|
||||
if (!result) {
|
||||
throw new SQLiteError('Query operation returned no result', 'sqlite-query');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw handleError(error, 'sqlite-query');
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('SQLite IPC handlers registered successfully');
|
||||
}
|
||||
@@ -105,8 +105,15 @@ export class ElectronCapacitorApp {
|
||||
defaultWidth: 1000,
|
||||
defaultHeight: 800,
|
||||
});
|
||||
// Setup preload script path and construct our main window.
|
||||
const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js');
|
||||
|
||||
// Setup preload script path based on environment
|
||||
const preloadPath = app.isPackaged
|
||||
? join(process.resourcesPath, 'preload.js')
|
||||
: join(__dirname, 'preload.js');
|
||||
|
||||
console.log('[Electron Main Process] Preload path:', preloadPath);
|
||||
console.log('[Electron Main Process] Preload exists:', require('fs').existsSync(preloadPath));
|
||||
|
||||
this.MainWindow = new BrowserWindow({
|
||||
icon,
|
||||
show: false,
|
||||
@@ -115,11 +122,12 @@ export class ElectronCapacitorApp {
|
||||
width: this.mainWindowState.width,
|
||||
height: this.mainWindowState.height,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
// Use preload to inject the electron varriant overrides for capacitor plugins.
|
||||
// preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"),
|
||||
sandbox: false,
|
||||
preload: preloadPath,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
},
|
||||
});
|
||||
this.mainWindowState.manage(this.MainWindow);
|
||||
|
||||
@@ -17,31 +17,56 @@ const app = initializeApp();
|
||||
|
||||
// Create a promise that resolves when SQLite is ready
|
||||
const sqliteReady = new Promise<void>((resolve, reject) => {
|
||||
if (!window.electron?.ipcRenderer) {
|
||||
logger.error("[Main Electron] IPC renderer not available");
|
||||
reject(new Error("IPC renderer not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a timeout to prevent hanging
|
||||
const timeout = setTimeout(() => {
|
||||
logger.error("[Main Electron] SQLite initialization timeout");
|
||||
reject(new Error("SQLite initialization timeout"));
|
||||
}, 30000); // 30 second timeout
|
||||
|
||||
window.electron.ipcRenderer.once('sqlite-ready', () => {
|
||||
clearTimeout(timeout);
|
||||
logger.info("[Main Electron] Received SQLite ready signal");
|
||||
resolve();
|
||||
});
|
||||
// Wait for electron bridge to be available
|
||||
const checkElectronBridge = () => {
|
||||
if (window.electron?.ipcRenderer) {
|
||||
logger.info("[Main Electron] IPC renderer bridge available");
|
||||
|
||||
// Listen for SQLite ready signal
|
||||
window.electron.ipcRenderer.once('sqlite-ready', () => {
|
||||
clearTimeout(timeout);
|
||||
logger.info("[Main Electron] Received SQLite ready signal");
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Also listen for database errors
|
||||
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => {
|
||||
clearTimeout(timeout);
|
||||
const status = args[0] as { status: string; error?: string };
|
||||
if (status.status === 'error') {
|
||||
reject(new Error(status.error || 'Database initialization failed'));
|
||||
// Also listen for database errors
|
||||
window.electron.ipcRenderer.once('database-status', (...args: unknown[]) => {
|
||||
clearTimeout(timeout);
|
||||
const status = args[0] as { status: string; error?: string };
|
||||
if (status.status === 'error') {
|
||||
logger.error("[Main Electron] Database error:", status.error);
|
||||
reject(new Error(status.error || 'Database initialization failed'));
|
||||
}
|
||||
});
|
||||
|
||||
// Check if SQLite is already available
|
||||
window.electron.ipcRenderer.invoke('sqlite-is-available')
|
||||
.then((result: unknown) => {
|
||||
const isAvailable = Boolean(result);
|
||||
if (isAvailable) {
|
||||
logger.info("[Main Electron] SQLite is already available");
|
||||
// Don't resolve here - wait for the ready signal
|
||||
// This prevents race conditions where the ready signal arrives after this check
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
logger.error("[Main Electron] Failed to check SQLite availability:", error);
|
||||
// Don't reject here - wait for either ready signal or timeout
|
||||
});
|
||||
} else {
|
||||
// Check again in 100ms if bridge isn't ready
|
||||
setTimeout(checkElectronBridge, 100);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Start checking for bridge
|
||||
checkElectronBridge();
|
||||
});
|
||||
|
||||
// Wait for SQLite to be ready before mounting
|
||||
@@ -49,17 +74,31 @@ sqliteReady
|
||||
.then(() => {
|
||||
logger.info("[Main Electron] SQLite ready, mounting app...");
|
||||
app.mount("#app");
|
||||
logger.info("[Main Electron] App mounted successfully");
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("[Main Electron] Failed to initialize SQLite:", error instanceof Error ? error.message : 'Unknown error');
|
||||
// Show error to user
|
||||
// Show error to user with retry option
|
||||
const errorDiv = document.createElement("div");
|
||||
errorDiv.style.cssText =
|
||||
"position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%;";
|
||||
"position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%; z-index: 9999;";
|
||||
errorDiv.innerHTML = `
|
||||
<h2>Failed to Initialize Application</h2>
|
||||
<p>There was an error initializing the database. Please try restarting.</p>
|
||||
<p>There was an error initializing the database. This could be due to:</p>
|
||||
<ul style="text-align: left; margin: 10px 0;">
|
||||
<li>Database file is locked by another process</li>
|
||||
<li>Insufficient permissions to access the database</li>
|
||||
<li>Database file is corrupted</li>
|
||||
</ul>
|
||||
<p>Error details: ${error instanceof Error ? error.message : 'Unknown error'}</p>
|
||||
<div style="margin-top: 15px;">
|
||||
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Retry
|
||||
</button>
|
||||
<button onclick="window.electron.ipcRenderer.send('sqlite-status', { action: 'reset' })" style="margin: 0 5px; padding: 8px 16px; background: #f57c00; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Reset Database
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(errorDiv);
|
||||
});
|
||||
|
||||
@@ -277,18 +277,31 @@ const initialPath = isElectron
|
||||
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
|
||||
: window.location.pathname;
|
||||
|
||||
logger.info("[Router] Initializing router", { isElectron, initialPath });
|
||||
|
||||
const history = isElectron
|
||||
? createMemoryHistory() // Memory history for Electron
|
||||
: createWebHistory("/"); // Add base path for web apps
|
||||
|
||||
/** @type {*} */
|
||||
const router = createRouter({
|
||||
history,
|
||||
routes,
|
||||
});
|
||||
|
||||
// Set initial route
|
||||
router.beforeEach((to, from, next) => {
|
||||
logger.info("[Router] Navigation", { to: to.path, from: from.path });
|
||||
next();
|
||||
});
|
||||
|
||||
// Replace initial URL to start at `/` if necessary
|
||||
router.replace(initialPath || "/");
|
||||
if (initialPath === "/" || !initialPath) {
|
||||
logger.info("[Router] Setting initial route to /");
|
||||
router.replace("/");
|
||||
} else {
|
||||
logger.info("[Router] Setting initial route to", initialPath);
|
||||
router.replace(initialPath);
|
||||
}
|
||||
|
||||
const errorHandler = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
Reference in New Issue
Block a user