import { useCallback, useEffect, useState } from "react"; import type { MachinesResponse } from "../types/machines"; import { usePageLiveActivity } from "./internal"; export const EMPTY_MACHINES: MachinesResponse = { activeSandboxId: null, activeEpoch: 0, machines: [], }; export type SharedMachinesSnapshot = { data: MachinesResponse | null; loading: boolean; error: Error | null; }; type Subscriber = { load: (signal?: AbortSignal) => Promise; pollIntervalMs: number | undefined; enabled: boolean; pageLive: boolean; onChange: () => void; }; type Share = { snapshot: SharedMachinesSnapshot; subscribers: Set; timer: ReturnType | null; disposeTimer: ReturnType | null; abort: AbortController | null; inFlight: Promise | null; trailing: Promise | null; generation: number; /** The last read was refused (401/403/404). Polling stays stopped until the * read is disabled (a permission/workspace change) or explicitly refreshed. */ halted: boolean; }; const shares = new Map(); /** A refusal that repeating the identical read cannot fix: the caller lacks * `enrollments:read` (403), is signed out (401), or the workspace/feature is * gone (404). Structural so host adapters with their own error types work. */ export function isMachinesAccessRefusal(cause: unknown): boolean { if (!cause || typeof cause !== "object" || !("status" in cause)) return false; const status = (cause as { status: unknown }).status; return status === 401 || status === 403 || status === 404; } function emit(share: Share): void { for (const subscriber of share.subscribers) { subscriber.onChange(); } } function effective(share: Share): { enabled: boolean; pollIntervalMs: number | undefined } { let enabled = false; let pollIntervalMs: number | undefined; for (const subscriber of share.subscribers) { if (!subscriber.enabled || !subscriber.pageLive) continue; enabled = true; if (subscriber.pollIntervalMs !== undefined && subscriber.pollIntervalMs > 0) { pollIntervalMs = pollIntervalMs === undefined ? subscriber.pollIntervalMs : Math.min(pollIntervalMs, subscriber.pollIntervalMs); } } return { enabled, pollIntervalMs }; } function latestLoad(share: Share): ((signal?: AbortSignal) => Promise) | null { let load: ((signal?: AbortSignal) => Promise) | null = null; for (const subscriber of share.subscribers) { if (subscriber.enabled && subscriber.pageLive) load = subscriber.load; } return load; } function stopTimer(share: Share): void { if (share.timer !== null) { clearTimeout(share.timer); share.timer = null; } } function cancelDispose(share: Share): void { if (share.disposeTimer !== null) { clearTimeout(share.disposeTimer); share.disposeTimer = null; } } function arm(share: Share): void { stopTimer(share); if (share.halted) return; const { enabled, pollIntervalMs } = effective(share); if (!enabled || pollIntervalMs === undefined) return; share.timer = setTimeout(() => { share.timer = null; void run(share).finally(() => arm(share)); }, pollIntervalMs); } async function runFresh(share: Share): Promise { const load = latestLoad(share); if (!load) return; const ticket = ++share.generation; const abort = new AbortController(); share.abort = abort; try { const data = await load(abort.signal); if (ticket !== share.generation || abort.signal.aborted) return; share.halted = false; share.snapshot = { data, loading: false, error: null }; emit(share); } catch (cause) { if (ticket !== share.generation || abort.signal.aborted) return; if (isMachinesAccessRefusal(cause)) { share.halted = true; stopTimer(share); } share.snapshot = { data: share.snapshot.data, loading: false, error: cause instanceof Error ? cause : new Error(String(cause)), }; emit(share); } finally { if (share.abort === abort) share.abort = null; } } function run(share: Share, force = false): Promise { if (!force && share.inFlight) return share.inFlight; if (force && share.inFlight) { if (share.trailing) return share.trailing; let trailing!: Promise; trailing = share.inFlight .then(async () => { if (!effective(share).enabled || share.halted) return; await run(share); }) .finally(() => { if (share.trailing === trailing) share.trailing = null; }); share.trailing = trailing; return trailing; } let promise!: Promise; promise = runFresh(share).finally(() => { if (share.inFlight === promise) share.inFlight = null; }); share.inFlight = promise; return promise; } function reconcile(share: Share): void { const { enabled } = effective(share); if (!enabled) { share.generation += 1; share.abort?.abort(); share.abort = null; share.inFlight = null; stopTimer(share); if (share.halted) { // Disabling is the host's permission/workspace signal. Forget the // refusal so a later re-enable reads once immediately. share.halted = false; share.snapshot = { data: share.snapshot.data, loading: false, error: null }; emit(share); return; } if (share.snapshot.loading) { share.snapshot = { ...share.snapshot, loading: false }; emit(share); } return; } if (share.halted) return; if (share.snapshot.data !== null || share.snapshot.error) { arm(share); return; } share.snapshot = { ...share.snapshot, loading: true }; emit(share); void run(share).finally(() => arm(share)); } function getOrCreate(shareKey: string): Share { const existing = shares.get(shareKey); if (existing) { cancelDispose(existing); return existing; } const created: Share = { snapshot: { data: null, loading: true, error: null }, subscribers: new Set(), timer: null, disposeTimer: null, abort: null, inFlight: null, trailing: null, generation: 0, halted: false, }; shares.set(shareKey, created); return created; } export async function refreshSharedMachinesList(shareKey: string): Promise { const share = shares.get(shareKey); if (!share) return; // An explicit refresh (user action, post-mutation reconcile) may retry once; // a repeated refusal halts polling again. const wasHalted = share.halted; share.halted = false; await run(share, true); if (wasHalted) arm(share); } export function useSharedMachinesList( shareKey: string, load: (signal?: AbortSignal) => Promise, options: { pollIntervalMs?: number | undefined; enabled?: boolean | undefined } = {}, ): SharedMachinesSnapshot & { refresh: () => Promise } { const enabled = options.enabled ?? true; const pageLive = usePageLiveActivity(); const [, setRevision] = useState(0); const live = shares.get(shareKey)?.snapshot; const view = live ?? { data: null, loading: enabled && pageLive, error: null }; useEffect(() => { const share = getOrCreate(shareKey); const subscriber: Subscriber = { load, pollIntervalMs: options.pollIntervalMs, enabled, pageLive, onChange: () => setRevision((revision) => revision + 1), }; share.subscribers.add(subscriber); setRevision((revision) => revision + 1); reconcile(share); return () => { share.subscribers.delete(subscriber); if (share.subscribers.size > 0) { reconcile(share); return; } // Abort immediately so a session switch does not keep the old GET alive. // Keep the snapshot one tick so a same-key remount (poll-interval change) // does not flash an empty fleet. share.generation += 1; share.abort?.abort(); share.abort = null; share.inFlight = null; share.trailing = null; stopTimer(share); cancelDispose(share); share.disposeTimer = setTimeout(() => { share.disposeTimer = null; if (share.subscribers.size === 0 && shares.get(shareKey) === share) { shares.delete(shareKey); } }, 0); }; }, [shareKey, load, options.pollIntervalMs, enabled, pageLive]); const refresh = useCallback(() => refreshSharedMachinesList(shareKey), [shareKey]); return { ...view, refresh }; }