mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(app): stabilize daemon lifecycle setup (#2177)
This commit is contained in:
@@ -135,7 +135,84 @@ describe('useDaemonLifecycle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('retry scheduling', () => {
|
||||
it('runs a scheduled retry using the latest daemon action', async () => {
|
||||
const { useDaemonLifecycle } = await import('../useDaemonLifecycle');
|
||||
const uid = freshUser('retry');
|
||||
resetUser(uid);
|
||||
setAutoStartEnabled(uid, true);
|
||||
setDaemonStatus(uid, 'error');
|
||||
incrementConnectionAttempts(uid);
|
||||
mockStartDaemon.mockResolvedValue({ result: { state: 'Running' }, logs: [] });
|
||||
|
||||
const { result } = renderHook(() => useDaemonLifecycle(uid));
|
||||
|
||||
expect(result.current.connectionAttempts).toBe(1);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
|
||||
expect(mockStartDaemon).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.connectionAttempts).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('background / foreground pause-resume', () => {
|
||||
it('keeps lifecycle setup stable across daemon state updates', async () => {
|
||||
const { useDaemonLifecycle } = await import('../useDaemonLifecycle');
|
||||
const uid = freshUser('stable-effect');
|
||||
resetUser(uid);
|
||||
setAutoStartEnabled(uid, true);
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
|
||||
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
||||
|
||||
try {
|
||||
const { unmount } = renderHook(() => useDaemonLifecycle(uid));
|
||||
|
||||
const lifecycleLogCount = (message: string) =>
|
||||
logSpy.mock.calls.filter(([logged]) => logged === message).length;
|
||||
const visibilityAddCount = () =>
|
||||
addEventListenerSpy.mock.calls.filter(([event]) => event === 'visibilitychange').length;
|
||||
const visibilityRemoveCount = () =>
|
||||
removeEventListenerSpy.mock.calls.filter(([event]) => event === 'visibilitychange')
|
||||
.length;
|
||||
|
||||
expect(lifecycleLogCount('[DaemonLifecycle] Setting up daemon lifecycle management')).toBe(
|
||||
1
|
||||
);
|
||||
expect(visibilityAddCount()).toBe(1);
|
||||
|
||||
act(() => {
|
||||
setDaemonStatus(uid, 'starting');
|
||||
setIsRecovering(uid, true);
|
||||
incrementConnectionAttempts(uid);
|
||||
setDaemonStatus(uid, 'running');
|
||||
setIsRecovering(uid, false);
|
||||
});
|
||||
|
||||
expect(lifecycleLogCount('[DaemonLifecycle] Setting up daemon lifecycle management')).toBe(
|
||||
1
|
||||
);
|
||||
expect(lifecycleLogCount('[DaemonLifecycle] Cleaning up daemon lifecycle management')).toBe(
|
||||
0
|
||||
);
|
||||
expect(visibilityAddCount()).toBe(1);
|
||||
expect(visibilityRemoveCount()).toBe(0);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(lifecycleLogCount('[DaemonLifecycle] Cleaning up daemon lifecycle management')).toBe(
|
||||
1
|
||||
);
|
||||
expect(visibilityRemoveCount()).toBe(1);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
addEventListenerSpy.mockRestore();
|
||||
removeEventListenerSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not invoke startDaemon while hidden, resumes auto-start on visible', async () => {
|
||||
const { useDaemonLifecycle } = await import('../useDaemonLifecycle');
|
||||
const uid = freshUser('vis');
|
||||
|
||||
@@ -25,7 +25,7 @@ const MAX_RETRY_DELAY_MS = 30000; // 30 seconds
|
||||
const AUTO_START_DELAY_MS = 3000; // 3 seconds after app start
|
||||
|
||||
export const useDaemonLifecycle = (userId?: string) => {
|
||||
const daemonHealth = useDaemonHealth(userId);
|
||||
const { startDaemon } = useDaemonHealth(userId);
|
||||
const daemonState = useDaemonUserState(userId);
|
||||
|
||||
const status = daemonState.status;
|
||||
@@ -38,6 +38,23 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
const autoStartTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
const latestLifecycleRef = useRef({
|
||||
isAutoStartEnabled,
|
||||
status,
|
||||
isRecovering,
|
||||
connectionAttempts,
|
||||
uid,
|
||||
startDaemon,
|
||||
});
|
||||
|
||||
latestLifecycleRef.current = {
|
||||
isAutoStartEnabled,
|
||||
status,
|
||||
isRecovering,
|
||||
connectionAttempts,
|
||||
uid,
|
||||
startDaemon,
|
||||
};
|
||||
|
||||
// Calculate exponential backoff delay
|
||||
const calculateRetryDelay = useCallback((attempt: number): number => {
|
||||
@@ -47,6 +64,9 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
|
||||
// Auto-start daemon if enabled and conditions are met
|
||||
const attemptAutoStart = useCallback(async () => {
|
||||
const { isAutoStartEnabled, status, isRecovering, connectionAttempts, uid, startDaemon } =
|
||||
latestLifecycleRef.current;
|
||||
|
||||
if (!isTauri() || !isAutoStartEnabled || !isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +77,7 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
|
||||
try {
|
||||
setIsRecovering(uid, true);
|
||||
const result = await daemonHealth.startDaemon();
|
||||
const result = await startDaemon();
|
||||
|
||||
if (result?.result && result.result.state === 'Running') {
|
||||
console.log('[DaemonLifecycle] Auto-start successful');
|
||||
@@ -73,10 +93,12 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
setIsRecovering(uid, false);
|
||||
}
|
||||
}
|
||||
}, [isAutoStartEnabled, status, isRecovering, connectionAttempts, uid, daemonHealth]);
|
||||
}, []);
|
||||
|
||||
// Retry connection with exponential backoff
|
||||
const scheduleRetry = useCallback(() => {
|
||||
const { connectionAttempts, status, isRecovering } = latestLifecycleRef.current;
|
||||
|
||||
if (!isTauri() || !isMountedRef.current || isRecovering) {
|
||||
return;
|
||||
}
|
||||
@@ -105,11 +127,13 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
retryTimeoutRef.current = setTimeout(async () => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const { uid, startDaemon } = latestLifecycleRef.current;
|
||||
|
||||
try {
|
||||
setIsRecovering(uid, true);
|
||||
incrementConnectionAttempts(uid);
|
||||
|
||||
const result = await daemonHealth.startDaemon();
|
||||
const result = await startDaemon();
|
||||
|
||||
if (result?.result && result.result.state === 'Running') {
|
||||
console.log('[DaemonLifecycle] Retry successful');
|
||||
@@ -125,12 +149,14 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
setIsRecovering(uid, false);
|
||||
}
|
||||
}, retryDelay);
|
||||
}, [connectionAttempts, status, isRecovering, calculateRetryDelay, uid, daemonHealth]);
|
||||
}, [calculateRetryDelay]);
|
||||
|
||||
// Handle visibility change (background/foreground)
|
||||
const handleVisibilityChange = useCallback(() => {
|
||||
if (!isTauri() || !isMountedRef.current) return;
|
||||
|
||||
const { isAutoStartEnabled, status, isRecovering } = latestLifecycleRef.current;
|
||||
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log('[DaemonLifecycle] App became visible - checking daemon status');
|
||||
|
||||
@@ -144,12 +170,13 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}, [isAutoStartEnabled, status, isRecovering, attemptAutoStart]);
|
||||
}, [attemptAutoStart]);
|
||||
|
||||
// Main lifecycle effect
|
||||
useEffect(() => {
|
||||
if (!isTauri()) return;
|
||||
|
||||
isMountedRef.current = true;
|
||||
console.log('[DaemonLifecycle] Setting up daemon lifecycle management');
|
||||
|
||||
// Setup auto-start with delay on mount
|
||||
@@ -181,7 +208,7 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
// Remove event listeners
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [isAutoStartEnabled, attemptAutoStart, handleVisibilityChange]);
|
||||
}, [isAutoStartEnabled, uid, attemptAutoStart, handleVisibilityChange]);
|
||||
|
||||
// Retry effect - triggers when daemon goes into error state or connection fails
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user