mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 08:02:37 +00:00
fix(mobile): remove 30s idle-close timer
The timer existed to clean up sockets when the user navigated away from a host for 30+ seconds. In practice it produced false 'Disconnected' flashes when the user navigated home → host → home faster than React could re-acquire on the home side: refcount drops to 0 between the host-detail unmount and the home re-render's useEffect, the timer fires 30s later, the WebSocket gets torn down, and the home card flips to 'Disconnected' with no user action. Connections still close on: - Explicit user Disconnect (closeEntry) - Host removal (closeEntry) - App backgrounding (OS-level socket suspension) - Provider unmount (app shutdown) - Real WebSocket failure (handled by rpc-client reconnect path) A foregrounded app with an idle authenticated WebSocket costs near-zero battery / data; the 'don't leak sockets' problem the timer was trying to solve is already handled by the OS, the desktop reaper, and the cleanup paths above. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -23,13 +23,10 @@ import { connect, type RpcClient } from './rpc-client'
|
||||
import { loadHosts } from './host-store'
|
||||
import type { ConnectionState, HostProfile } from './types'
|
||||
|
||||
const IDLE_CLOSE_MS = 30_000
|
||||
|
||||
type StoreEntry = {
|
||||
client: RpcClient
|
||||
state: ConnectionState
|
||||
refCount: number
|
||||
idleTimer: ReturnType<typeof setTimeout> | null
|
||||
unsubState: () => void
|
||||
}
|
||||
|
||||
@@ -83,7 +80,6 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
|
||||
const closeEntry = useCallback((hostId: string) => {
|
||||
const entry = storeRef.current.get(hostId)
|
||||
if (!entry) return
|
||||
if (entry.idleTimer) clearTimeout(entry.idleTimer)
|
||||
entry.unsubState()
|
||||
entry.client.close()
|
||||
storeRef.current.delete(hostId)
|
||||
@@ -150,7 +146,6 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
|
||||
client,
|
||||
state: client.getState(),
|
||||
refCount: 0,
|
||||
idleTimer: null,
|
||||
unsubState
|
||||
}
|
||||
storeRef.current.set(hostId, entry)
|
||||
@@ -175,10 +170,6 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
|
||||
const existing = storeRef.current.get(hostId)
|
||||
if (existing) {
|
||||
existing.refCount += 1
|
||||
if (existing.idleTimer) {
|
||||
clearTimeout(existing.idleTimer)
|
||||
existing.idleTimer = null
|
||||
}
|
||||
return existing.client
|
||||
}
|
||||
// Trigger async open. The acquire-side will return null this tick and
|
||||
@@ -197,36 +188,30 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
|
||||
for (const host of hosts) primedHostsRef.current.set(host.id, host)
|
||||
}, [])
|
||||
|
||||
const release = useCallback(
|
||||
(hostId: string) => {
|
||||
const entry = storeRef.current.get(hostId)
|
||||
if (!entry) return
|
||||
entry.refCount = Math.max(0, entry.refCount - 1)
|
||||
if (entry.refCount > 0) return
|
||||
if (entry.idleTimer) clearTimeout(entry.idleTimer)
|
||||
entry.idleTimer = setTimeout(() => {
|
||||
// Why: only close if still idle when the timer fires. A late acquire
|
||||
// would have cleared the timer.
|
||||
const cur = storeRef.current.get(hostId)
|
||||
if (!cur || cur.refCount > 0) return
|
||||
closeEntry(hostId)
|
||||
}, IDLE_CLOSE_MS)
|
||||
},
|
||||
[closeEntry]
|
||||
)
|
||||
// Why: refcount dropping to 0 no longer triggers an idle-close. The
|
||||
// app deliberately keeps live WebSockets open while the app itself is
|
||||
// foregrounded — closing on transient navigation gaps was producing
|
||||
// false 'disconnected' flashes when the user navigated home → host →
|
||||
// back to home faster than React could re-acquire on the home side.
|
||||
// Connections still close on: explicit user Disconnect, host removal,
|
||||
// app backgrounding (OS-level socket suspension), and provider
|
||||
// unmount (app shutdown).
|
||||
const release = useCallback((hostId: string) => {
|
||||
const entry = storeRef.current.get(hostId)
|
||||
if (!entry) return
|
||||
entry.refCount = Math.max(0, entry.refCount - 1)
|
||||
}, [])
|
||||
|
||||
const forceReconnect = useCallback(
|
||||
async (hostId: string) => {
|
||||
const entry = storeRef.current.get(hostId)
|
||||
// Why: if the entry was previously closed (e.g. user tapped
|
||||
// Disconnect), refCount is lost. Fall back to the number of active
|
||||
// state listeners as a proxy for "screens currently watching this
|
||||
// host," so the freshly-opened entry doesn't trip the idle-close
|
||||
// timer immediately.
|
||||
// Why: preserve refcount across the swap. If the user reaches
|
||||
// forceReconnect via the Disconnect → Reconnect path, the entry
|
||||
// was already closed and refCount=0; fall back to active listener
|
||||
// count as a proxy for "screens still watching this host."
|
||||
const listenerCount = stateListenersRef.current.get(hostId)?.size ?? 0
|
||||
const savedRefCount = entry?.refCount ?? Math.max(1, listenerCount)
|
||||
if (entry) {
|
||||
if (entry.idleTimer) clearTimeout(entry.idleTimer)
|
||||
entry.unsubState()
|
||||
entry.client.close()
|
||||
storeRef.current.delete(hostId)
|
||||
|
||||
Reference in New Issue
Block a user