agent-ecosystem/src/renderer/hooks/useBranchSync.ts
iliya 52ef9fd0a8 feat: add FAQ section to README and improve cross-platform handling in code
- Introduced a comprehensive FAQ section in the README to address common user queries regarding app installation, code handling, agent communication, and project management.
- Enhanced cross-platform keyboard shortcut handling in the Electron app for better user experience on macOS and Windows/Linux.
- Updated signal handling in the standalone process to ensure proper shutdown behavior across platforms.
- Improved WSL user resolution logic to support default user retrieval for better compatibility.
- Enhanced notification handling to support cross-platform features and improve user feedback.
- Refactored SSH connection management to include additional key file types and improve authentication handling.
- Updated team management services to ensure consistent process termination across platforms.
- Improved project path handling in team provisioning to accommodate different operating systems.
- Enhanced editor components to utilize shared utility functions for path management, improving code maintainability.
2026-03-02 22:56:56 +02:00

128 lines
4.4 KiB
TypeScript

/**
* Centralized git branch polling hook.
*
* Provides two modes:
* - `live: false` (default) — one-shot fetch on mount / path change
* - `live: true` — continuous polling with ref-counted shared timer
*
* Data is stored in the Zustand store (`branchByPath`) so any component
* can read it via `useStore(s => s.branchByPath)`.
*
* The module-level polling manager guarantees:
* - A single shared `setInterval` across all live subscribers
* - Deduplication: N components subscribing to the same path = 1 poll
* - Automatic cleanup: timer stops when all subscribers unmount
*/
import { useEffect, useMemo } from 'react';
import { useStore } from '@renderer/store';
import { normalizePath } from '@renderer/utils/pathNormalize';
// =============================================================================
// Constants
// =============================================================================
const POLL_INTERVAL_MS = 6_000;
// =============================================================================
// Module-level polling manager (singleton, outside React lifecycle)
// =============================================================================
const livePaths = new Map<string, { actualPath: string; refCount: number }>();
let pollTimer: ReturnType<typeof setInterval> | null = null;
function startPollingIfNeeded(): void {
if (pollTimer || livePaths.size === 0) return;
pollTimer = setInterval(() => {
const paths = Array.from(livePaths.values()).map((v) => v.actualPath);
void useStore.getState().fetchBranches(paths);
}, POLL_INTERVAL_MS);
}
function stopPollingIfEmpty(): void {
if (pollTimer && livePaths.size === 0) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function subscribe(normalizedKey: string, actualPath: string): void {
const entry = livePaths.get(normalizedKey);
if (entry) {
entry.refCount++;
} else {
livePaths.set(normalizedKey, { actualPath, refCount: 1 });
}
startPollingIfNeeded();
}
function unsubscribe(normalizedKey: string): void {
const entry = livePaths.get(normalizedKey);
if (!entry) return;
entry.refCount--;
if (entry.refCount <= 0) {
livePaths.delete(normalizedKey);
}
stopPollingIfEmpty();
}
// =============================================================================
// Hook
// =============================================================================
/**
* Sync git branch data for the given project paths into the store.
*
* @param paths - Raw project paths to resolve branches for
* @param options.live - When true, keeps polling every 6s while mounted
*/
export function useBranchSync(paths: string[], options?: { live?: boolean }): void {
const live = options?.live ?? false;
const fetchBranches = useStore((s) => s.fetchBranches);
// Deduplicate and normalize paths into [normalizedKey, actualPath] entries.
// `paths` identity should be stabilized by the caller via useMemo.
const pathEntries = useMemo(() => {
const map = new Map<string, string>();
for (const p of paths) {
const trimmed = p.trim();
if (trimmed) {
const key = normalizePath(trimmed);
if (!map.has(key)) map.set(key, trimmed);
}
}
return Array.from(map.entries());
}, [paths]);
// Stable string key for useEffect deps — avoids re-running on same set of paths
const pathsKey = useMemo(
() =>
pathEntries
.map(([k]) => k)
.sort((a, b) => a.localeCompare(b))
.join('\n'),
[pathEntries]
);
// Initial fetch on mount and whenever paths change (both live and one-shot modes)
useEffect(() => {
if (pathEntries.length === 0) return;
void fetchBranches(pathEntries.map(([, actual]) => actual));
// eslint-disable-next-line react-hooks/exhaustive-deps -- pathsKey is a stable string derived from pathEntries, avoids re-fetching on array identity change
}, [pathsKey, fetchBranches]);
// Live subscription: register paths with the ref-counted polling manager
useEffect(() => {
if (!live || pathEntries.length === 0) return;
for (const [key, actual] of pathEntries) {
subscribe(key, actual);
}
return () => {
for (const [key] of pathEntries) {
unsubscribe(key);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- pathsKey is a stable string key; pathEntries excluded to prevent re-subscribing on array identity change
}, [live, pathsKey]);
}