Files
buzz/desktop/src/shared/api/useReconnectRelay.ts
T
499c5d349d fix(relay): preserve reconnect backoff (#2759)
## Why
Manual relay recovery polls every three seconds, repeatedly consuming
the session's exponential-backoff timer and leaving reconnect behavior
stuck or noisy on degraded networks.

## What
- Replace fixed-cadence Phase 3 polling with observation of the
RelayClient background reconnect loop
- Raise the fast-path deadline above the native websocket timeout and
enforce that contract in a regression test
- Keep the existing 120-second backstop as a soft UI timeout without
stopping background retries

## Risk Assessment
Medium — this changes live relay recovery timing, but removes a
competing retry loop rather than adding one. The existing
connection-state subscription remains the success signal.

## References
- Stacked on #2310 (`lazyjoe/reconnect-testability-refactor`)
- Investigation: `RESEARCH/BUG_RELAY_RECONNECT_HANG.md`
- `just ci`
- `cd desktop && pnpm typecheck && pnpm check && pnpm test` (3485 pass)
- `git diff --check`

Generated with Codex

Signed-off-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1x4hk035p3p9q39a3fcrd2fe30lpkrhr5dwe0cqzzjphxyyh8m0gsq4vqap <356f67c681884a0897b14e06d527317fc361dc746bb2fc0042906e6212e7dbd1@sprout-oss.stage.blox.sqprod.co>
2026-07-25 10:00:47 -07:00

90 lines
3.1 KiB
TypeScript

/**
* React binding for the relay reconnect controller.
*
* Delegates all reconnect logic to the module-level `relayReconnectController`
* singleton so that all mounted hook instances (banner + sidebar card) share
* a single in-flight state. Deliberately uses `preconnect()` rather than the
* full `reconnectCommunity()` path to avoid unmounting the React tree and
* clearing draft state.
*
* See `relayReconnectController.ts` for the three-phase strategy details.
*/
import * as React from "react";
import { useQueryClient } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { toast } from "sonner";
import { relayClient } from "@/shared/api/relayClient";
import { isRelayDependentQuery } from "@/shared/api/relayQueryInvalidation";
import { relayReconnectController } from "@/shared/api/relayReconnectController";
function buildDeps(onSuccess: () => void, onBackstop: () => void) {
return {
preconnect: () => relayClient.preconnect(),
hookConfigured: () => invoke<boolean>("relay_reconnect_hook_configured"),
runHook: () => invoke<void>("relay_reconnect_hook"),
subscribeToConnectionState: (listener: (state: string) => void) =>
relayClient.subscribeToConnectionState(listener),
onSuccess,
onBackstop,
setTimeout: window.setTimeout.bind(window),
clearTimeout: window.clearTimeout.bind(window),
};
}
export function useReconnectRelay(): {
reconnect: () => Promise<boolean>;
isPending: boolean;
isWaitingOnReconnectHook: boolean;
} {
const queryClient = useQueryClient();
const [controllerState, setControllerState] = React.useState(() =>
relayReconnectController.getState(),
);
// Subscribe to controller state changes for the lifetime of this component.
// Multiple hook instances receive the same state from the shared singleton.
React.useEffect(() => {
return relayReconnectController.subscribe(setControllerState);
}, []);
// Stable mutable refs for callbacks — updated every render so stale closures
// are never captured, but the reconnect callback itself never changes identity.
const onSuccessRef = React.useRef<(() => void) | null>(null);
const onBackstopRef = React.useRef<(() => void) | null>(null);
onSuccessRef.current = React.useCallback(() => {
// Defer query invalidation so callers render the recovered state first.
window.setTimeout(() => {
void queryClient
.invalidateQueries({ predicate: isRelayDependentQuery })
.catch((err) => {
console.error(
"[useReconnectRelay] failed to refresh queries after reconnect:",
err,
);
});
}, 0);
}, [queryClient]);
onBackstopRef.current = React.useCallback(() => {
toast("Still trying to reconnect — check your network.");
}, []);
const reconnect = React.useCallback(async () => {
const deps = buildDeps(
() => onSuccessRef.current?.(),
() => onBackstopRef.current?.(),
);
return relayReconnectController.start(deps);
}, []);
return {
reconnect,
isPending: controllerState.isPending,
isWaitingOnReconnectHook: controllerState.isWaitingOnReconnectHook,
};
}