Own the live socket outside React so leaving Home cannot orphan it
The socket belonged to whichever hook instance ran its effect first. Passive effects run children before parents, so on Home that was the brain graph rather than the shell: navigating to a sibling route unmounted the graph, which closed the socket, while the shell kept the reference count above zero. From there the page was deaf for the rest of its life, with nothing left to reconnect it. A module-level connection with a real refcount replaces it — connect on the first subscriber, disconnect on the last — and the hook is a thin subscription with the same signature, correct under StrictMode's mount/unmount/mount. A 1008 now reconnects instead of returning silently: the token is read afresh per attempt, and three consecutive rejections fall through to the caller's auth handler so a revoked session surfaces rather than spins. The snapshot's emit counts are read into a store of their own, apart from the live count, so a graph that connects into a busy engine is drawn as busy without every neuron claiming it just fired. The neuron and edge pulses now key off a change seen while they were mounted, so returning to Home no longer replays every emission of the session. Home gets a live indicator for the case none of this can fix: quiet while the socket is up, and named in words when it is down, since HTTP polling keeps the rest of the page looking current. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { Activity } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { useEffect, useState, useSyncExternalStore } from "react"
|
||||
|
||||
import { useLiveConnection } from "@/components/Flow/liveStore"
|
||||
import { connectionStore } from "@/lib/connectionStore"
|
||||
import { fadeIn } from "@/lib/motion"
|
||||
|
||||
/**
|
||||
* How long the socket may be down before it is worth saying so.
|
||||
*
|
||||
* The first reconnect attempt is a second away, so a dropped frame is back
|
||||
* before this elapses and nothing appears. Long enough to cover the opening
|
||||
* handshake too: a page that has just loaded is connecting, not disconnected.
|
||||
*/
|
||||
const GRACE = 3000
|
||||
|
||||
/**
|
||||
* Says when live values have stopped arriving.
|
||||
*
|
||||
* Everything else on Home is polled over HTTP, so a dead socket leaves the page
|
||||
* looking perfectly current while nothing on it moves any more — the neurons
|
||||
* stop pulsing and the values stop changing, with nothing to say why. Nothing
|
||||
* at all while the socket is up: a page that works needs no chip saying so.
|
||||
*
|
||||
* Quiet too while `ConnectionBanner` is up, since an installation that cannot
|
||||
* be reached has no socket either and one explanation of that is enough. Named
|
||||
* in words rather than coloured, and deliberately not terracotta: the brain
|
||||
* graph above it already owns that accent for a flow that cannot run.
|
||||
*/
|
||||
export function LiveIndicator() {
|
||||
const connected = useLiveConnection()
|
||||
const { offline } = useSyncExternalStore(
|
||||
connectionStore.subscribe,
|
||||
connectionStore.snapshot,
|
||||
connectionStore.snapshot,
|
||||
)
|
||||
const [down, setDown] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (connected) {
|
||||
setDown(false)
|
||||
return
|
||||
}
|
||||
const timer = setTimeout(() => setDown(true), GRACE)
|
||||
return () => clearTimeout(timer)
|
||||
}, [connected])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{down && !offline ? (
|
||||
<motion.div
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="hidden"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
data-testid="live-indicator"
|
||||
// Wraps rather than widens: three phrases in a row do not fit a
|
||||
// phone. See DESIGN-GUIDELINES.md → Responsive.
|
||||
className="flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-border px-4 py-2 text-sm shadow-e1"
|
||||
>
|
||||
<Activity className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="font-medium">Live updates disconnected</span>
|
||||
<span className="text-muted-foreground">
|
||||
values and activity are frozen — reconnecting…
|
||||
</span>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user