feat: add Tauri event hook and lore.db file watcher
Adds real-time updates when lore syncs new GitLab data by watching the lore.db file for changes. React hook (src/hooks/useTauriEvents.ts): - useTauriEvent(): Subscribe to a single Tauri event with auto-cleanup - useTauriEvents(): Subscribe to multiple events with a handler map - Typed payloads for each event type: - global-shortcut-triggered: toggle-window | quick-capture - lore-data-changed: void (refresh trigger) - sync-status: started | completed | failed - error-notification: code + message Rust watcher (src-tauri/src/watcher.rs): - Watches lore's data directory for lore.db modifications - Uses notify crate with 2-second poll interval - Emits "lore-data-changed" event to frontend on file change - Handles atomic writes by watching parent directory - Gracefully handles missing lore.db (logs warning, skips watcher) Test coverage: - Hook subscription and cleanup behavior - Focus store test fix: clear localStorage before each test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
104
src/hooks/useTauriEvents.ts
Normal file
104
src/hooks/useTauriEvents.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* React hook for Tauri event communication.
|
||||
*
|
||||
* Handles Rust→React events with automatic cleanup on unmount.
|
||||
* Used for file watcher triggers, sync status, error notifications.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
/** Event types emitted by the Rust backend */
|
||||
export type TauriEventType =
|
||||
| "global-shortcut-triggered"
|
||||
| "lore-data-changed"
|
||||
| "sync-status"
|
||||
| "error-notification";
|
||||
|
||||
/** Payload types for each event */
|
||||
export interface TauriEventPayloads {
|
||||
"global-shortcut-triggered": "toggle-window" | "quick-capture";
|
||||
"lore-data-changed": void;
|
||||
"sync-status": { status: "started" | "completed" | "failed"; message?: string };
|
||||
"error-notification": { code: string; message: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a Tauri event with automatic cleanup.
|
||||
*
|
||||
* @param eventName The event to listen for
|
||||
* @param handler Callback when event is received
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* useTauriEvent("lore-data-changed", () => {
|
||||
* refetch();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useTauriEvent<T extends TauriEventType>(
|
||||
eventName: T,
|
||||
handler: (payload: TauriEventPayloads[T]) => void
|
||||
): void {
|
||||
// Memoize handler to avoid re-subscribing on every render
|
||||
const stableHandler = useCallback(handler, [handler]);
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: UnlistenFn | undefined;
|
||||
|
||||
// Subscribe to the event
|
||||
listen<TauriEventPayloads[T]>(eventName, (event) => {
|
||||
stableHandler(event.payload);
|
||||
}).then((fn) => {
|
||||
unlisten = fn;
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
}
|
||||
};
|
||||
}, [eventName, stableHandler]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to multiple Tauri events.
|
||||
*
|
||||
* @param handlers Map of event names to handlers
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* useTauriEvents({
|
||||
* "lore-data-changed": () => refetch(),
|
||||
* "sync-status": (status) => setStatus(status),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useTauriEvents(
|
||||
handlers: Partial<{
|
||||
[K in TauriEventType]: (payload: TauriEventPayloads[K]) => void;
|
||||
}>
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const unlisteners: UnlistenFn[] = [];
|
||||
|
||||
// Subscribe to each event
|
||||
for (const [eventName, handler] of Object.entries(handlers)) {
|
||||
if (handler) {
|
||||
listen(eventName, (event) => {
|
||||
(handler as (p: unknown) => void)(event.payload);
|
||||
}).then((unlisten) => {
|
||||
unlisteners.push(unlisten);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup all subscriptions on unmount
|
||||
return () => {
|
||||
for (const unlisten of unlisteners) {
|
||||
unlisten();
|
||||
}
|
||||
};
|
||||
}, [handlers]);
|
||||
}
|
||||
Reference in New Issue
Block a user