feat: implement pending file event handling using durable queue with acknowledgment support in folder synchronization

This commit is contained in:
Anish Sarkar 2026-04-03 00:40:49 +05:30
parent b46c5532b3
commit e0b35cfbab
6 changed files with 175 additions and 35 deletions

View file

@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { documentsApiService } from "@/lib/apis/documents-api.service";
interface FileChangedEvent {
id: string;
rootFolderId: number | null;
searchSpaceId: number;
folderPath: string;
@ -15,25 +16,35 @@ interface FileChangedEvent {
}
const DEBOUNCE_MS = 2000;
interface QueueItem {
event: FileChangedEvent;
ackIds: string[];
}
export function useFolderSync() {
const queueRef = useRef<FileChangedEvent[]>([]);
const queueRef = useRef<QueueItem[]>([]);
const processingRef = useRef(false);
const debounceTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
const pendingByKey = useRef<Map<string, QueueItem>>(new Map());
const isMountedRef = useRef(false);
async function processQueue() {
if (processingRef.current) return;
processingRef.current = true;
while (queueRef.current.length > 0) {
const event = queueRef.current.shift()!;
const item = queueRef.current.shift()!;
try {
await documentsApiService.folderIndexFile(event.searchSpaceId, {
folder_path: event.folderPath,
folder_name: event.folderName,
search_space_id: event.searchSpaceId,
target_file_path: event.fullPath,
root_folder_id: event.rootFolderId,
await documentsApiService.folderIndexFile(item.event.searchSpaceId, {
folder_path: item.event.folderPath,
folder_name: item.event.folderName,
search_space_id: item.event.searchSpaceId,
target_file_path: item.event.fullPath,
root_folder_id: item.event.rootFolderId,
});
const api = typeof window !== "undefined" ? window.electronAPI : null;
if (api?.acknowledgeFileEvents && item.ackIds.length > 0) {
await api.acknowledgeFileEvents(item.ackIds);
}
} catch (err) {
console.error("[FolderSync] Failed to trigger re-index:", err);
}
@ -41,34 +52,63 @@ export function useFolderSync() {
processingRef.current = false;
}
function enqueueWithDebounce(event: FileChangedEvent) {
const key = `${event.folderPath}:${event.relativePath}`;
const existing = pendingByKey.current.get(key);
const ackSet = new Set(existing?.ackIds ?? []);
ackSet.add(event.id);
pendingByKey.current.set(key, {
event,
ackIds: Array.from(ackSet),
});
const existingTimeout = debounceTimers.current.get(key);
if (existingTimeout) clearTimeout(existingTimeout);
const timeout = setTimeout(() => {
debounceTimers.current.delete(key);
const pending = pendingByKey.current.get(key);
if (!pending) return;
pendingByKey.current.delete(key);
queueRef.current.push(pending);
processQueue();
}, DEBOUNCE_MS);
debounceTimers.current.set(key, timeout);
}
useEffect(() => {
isMountedRef.current = true;
const api = typeof window !== "undefined" ? window.electronAPI : null;
if (!api?.onFileChanged) return;
if (!api?.onFileChanged) {
return () => {
isMountedRef.current = false;
};
}
// Signal to main process that the renderer is ready to receive events
api.signalRendererReady?.();
// Drain durable outbox first so events survive renderer startup gaps and restarts
void api.getPendingFileEvents?.().then((pendingEvents) => {
if (!isMountedRef.current || !pendingEvents?.length) return;
for (const event of pendingEvents) {
enqueueWithDebounce(event);
}
});
const cleanup = api.onFileChanged((event: FileChangedEvent) => {
const key = `${event.folderPath}:${event.fullPath}`;
const existing = debounceTimers.current.get(key);
if (existing) clearTimeout(existing);
const timeout = setTimeout(() => {
debounceTimers.current.delete(key);
queueRef.current.push(event);
processQueue();
}, DEBOUNCE_MS);
debounceTimers.current.set(key, timeout);
enqueueWithDebounce(event);
});
return () => {
isMountedRef.current = false;
cleanup();
for (const timeout of debounceTimers.current.values()) {
clearTimeout(timeout);
}
debounceTimers.current.clear();
pendingByKey.current.clear();
};
}, []);
}