refactor(orbit + shortcuts): unify host/guest heartbeat, document shortcut contract (Phase I) (#693)

* refactor(orbit): unify host/guest outbox heartbeat into a shared hook (Phase I)

The outbox-heartbeat effect was duplicated near-verbatim in useOrbitHost
and useOrbitGuest — same 10 s interval, same writeOrbitHeartbeat call,
same cleanup; the only difference is whose name owns the outbox
(OrbitState.host vs the active-server username).

Extract it into useOrbitOutboxHeartbeat(active, outboxPlaylistId,
sessionId, ownName). Host and guest each pass their own name source.
The push/pull state-tick logic stays untouched — that asymmetry is the
real host/guest difference, not duplication.

Behaviour-preserving: the owner name is now a reactive hook arg instead
of a getState() read inside the effect, so the heartbeat starts as soon
as the name is available rather than waiting for an unrelated dep to
change — a strict improvement, unreachable in practice since host name
and username are fixed per session.

* docs(shortcuts): document the shortcut-actions contract (Phase I)

Add a contract reference block to the shortcutActions barrel — the three
independent trigger surfaces (inApp / global / runInMiniWindow), the
surface-independent cli + run fields, the dispatch entry points — and
per-field doc comments on ShortcutActionMeta / ShortcutSlot /
ActionContext / CliContext in shortcutTypes.ts.

Pure documentation, no code change.
This commit is contained in:
Frank Stellmacher
2026-05-14 15:37:26 +02:00
committed by GitHub
parent 4b1dd3c29f
commit 946528350c
5 changed files with 109 additions and 41 deletions
+7 -22
View File
@@ -4,12 +4,10 @@ import { useEffect, useRef } from 'react';
import { useOrbitStore } from '../store/orbitStore';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
import {
readOrbitState,
writeOrbitHeartbeat,
} from '../utils/orbit';
import { orbitOutboxPlaylistName, estimateLivePosition, type OrbitState } from '../api/orbit';
import { readOrbitState } from '../utils/orbit';
import { estimateLivePosition, type OrbitState } from '../api/orbit';
import { pushOrbitEvent } from '../utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from './useOrbitOutboxHeartbeat';
/**
* Orbit — guest-side tick hook.
@@ -29,7 +27,6 @@ import { pushOrbitEvent } from '../utils/orbitDiag';
*/
const STATE_READ_TICK_MS = 2_500;
const HEARTBEAT_TICK_MS = 10_000;
/**
* Host must be quiet (no state writes) for this long before we treat the
* session as dead and auto-leave. Well above any normal network blip —
@@ -44,6 +41,7 @@ export function useOrbitGuest(): void {
const sessionPlaylistId = useOrbitStore(s => s.sessionPlaylistId);
const outboxPlaylistId = useOrbitStore(s => s.outboxPlaylistId);
const sessionId = useOrbitStore(s => s.sessionId);
const myName = useAuthStore(s => s.getActiveServer()?.username);
const active = role === 'guest' && phase === 'active' && !!sessionPlaylistId;
@@ -362,20 +360,7 @@ export function useOrbitGuest(): void {
};
}, [active, sessionPlaylistId]);
// ── Heartbeat ────────────────────────────────────────────────────────
useEffect(() => {
if (!active || !outboxPlaylistId || !sessionId) return;
const me = useAuthStore.getState().getActiveServer()?.username;
if (!me) return;
const outboxName = orbitOutboxPlaylistName(sessionId, me);
const beat = async () => {
try { await writeOrbitHeartbeat(outboxPlaylistId, outboxName); }
catch { /* best-effort */ }
};
void beat();
const id = window.setInterval(() => { void beat(); }, HEARTBEAT_TICK_MS);
return () => window.clearInterval(id);
}, [active, outboxPlaylistId, sessionId]);
// Outbox heartbeat — shared with the host hook; the guest's outbox is keyed
// by its own active-server username.
useOrbitOutboxHeartbeat(active, outboxPlaylistId, sessionId, myName);
}