feat: add renderer readiness signaling and update IPC channels for folder sync

This commit is contained in:
Anish Sarkar 2026-04-02 22:20:11 +05:30
parent 1ef0d913e7
commit 5d6e3ffb7b
4 changed files with 48 additions and 20 deletions

View file

@ -16,4 +16,5 @@ export const IPC_CHANNELS = {
FOLDER_SYNC_WATCHER_READY: 'folder-sync:watcher-ready',
FOLDER_SYNC_PAUSE: 'folder-sync:pause',
FOLDER_SYNC_RESUME: 'folder-sync:resume',
FOLDER_SYNC_RENDERER_READY: 'folder-sync:renderer-ready',
} as const;

View file

@ -8,6 +8,7 @@ import {
getWatcherStatus,
pauseWatcher,
resumeWatcher,
markRendererReady,
} from '../modules/folder-watcher';
export function registerIpcHandlers(): void {
@ -44,4 +45,8 @@ export function registerIpcHandlers(): void {
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_PAUSE, () => pauseWatcher());
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_RESUME, () => resumeWatcher());
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_RENDERER_READY, () => {
markRendererReady();
});
}

View file

@ -9,7 +9,7 @@ export interface WatchedFolderConfig {
name: string;
excludePatterns: string[];
fileExtensions: string[] | null;
connectorId: number;
rootFolderId: number | null;
searchSpaceId: number;
active: boolean;
}
@ -34,6 +34,25 @@ let watchers: Map<string, WatcherEntry> = new Map();
*/
const mtimeMaps: Map<string, MtimeMap> = new Map();
let rendererReady = false;
const pendingEvents: any[] = [];
export function markRendererReady() {
rendererReady = true;
for (const event of pendingEvents) {
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, event);
}
pendingEvents.length = 0;
}
function sendFileChangedEvent(data: any) {
if (rendererReady) {
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, data);
} else {
pendingEvents.push(data);
}
}
async function getStore() {
if (!store) {
const { default: Store } = await import('electron-store');
@ -83,7 +102,6 @@ function walkFolderMtimes(config: WatchedFolderConfig): MtimeMap {
for (const entry of entries) {
const name = entry.name;
// Skip dotfiles/dotdirs and excluded names
if (name.startsWith('.') || excludes.has(name)) continue;
const full = path.join(dir, name);
@ -131,7 +149,6 @@ async function startWatcher(config: WatchedFolderConfig) {
return;
}
// Load persisted mtime map into memory before starting the watcher
const ms = await getMtimeStore();
const storedMap: MtimeMap = ms.get(config.path) ?? {};
mtimeMaps.set(config.path, { ...storedMap });
@ -156,45 +173,49 @@ async function startWatcher(config: WatchedFolderConfig) {
watcher.on('ready', () => {
ready = true;
// Detect offline changes by diffing current filesystem against stored mtime map
const currentMap = walkFolderMtimes(config);
const storedSnapshot = loadMtimeMap(config.path);
const now = Date.now();
// Track which files are unchanged so we can selectively update the mtime map
const unchangedMap: MtimeMap = {};
for (const [rel, currentMtime] of Object.entries(currentMap)) {
const storedMtime = storedSnapshot[rel];
if (storedMtime === undefined) {
// New file added while app was closed
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, {
connectorId: config.connectorId,
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
folderPath: config.path,
folderName: config.name,
relativePath: rel,
fullPath: path.join(config.path, rel),
action: 'add',
timestamp: now,
});
} else if (Math.abs(currentMtime - storedMtime) >= MTIME_TOLERANCE_S * 1000) {
// File modified while app was closed
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, {
connectorId: config.connectorId,
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
folderPath: config.path,
folderName: config.name,
relativePath: rel,
fullPath: path.join(config.path, rel),
action: 'change',
timestamp: now,
});
} else {
unchangedMap[rel] = currentMtime;
}
}
for (const rel of Object.keys(storedSnapshot)) {
if (!(rel in currentMap)) {
// File deleted while app was closed
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, {
connectorId: config.connectorId,
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
folderPath: config.path,
folderName: config.name,
relativePath: rel,
fullPath: path.join(config.path, rel),
action: 'unlink',
@ -203,12 +224,13 @@ async function startWatcher(config: WatchedFolderConfig) {
}
}
// Replace stored map with current filesystem state
mtimeMaps.set(config.path, currentMap);
// Only update the mtime map for unchanged files; changed files keep their
// stored mtime so they'll be re-detected if the app crashes before indexing.
mtimeMaps.set(config.path, unchangedMap);
persistMtimeMap(config.path);
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_WATCHER_READY, {
connectorId: config.connectorId,
rootFolderId: config.rootFolderId,
folderPath: config.path,
});
});
@ -226,7 +248,6 @@ async function startWatcher(config: WatchedFolderConfig) {
if (!config.fileExtensions.includes(ext)) return;
}
// Keep mtime map in sync with live changes
const map = mtimeMaps.get(config.path);
if (map) {
if (action === 'unlink') {
@ -241,10 +262,11 @@ async function startWatcher(config: WatchedFolderConfig) {
persistMtimeMap(config.path);
}
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, {
connectorId: config.connectorId,
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
folderPath: config.path,
folderName: config.name,
relativePath,
fullPath: filePath,
action,
@ -311,7 +333,6 @@ export async function removeWatchedFolder(
stopWatcher(folderPath);
// Clean up persisted mtime map for this folder
mtimeMaps.delete(folderPath);
const ms = await getMtimeStore();
ms.delete(folderPath);

View file

@ -44,4 +44,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
pauseWatcher: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_PAUSE),
resumeWatcher: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_RESUME),
signalRendererReady: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_RENDERER_READY),
});