mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
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:
committed by
GitHub
parent
4b1dd3c29f
commit
946528350c
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useOrbitStore } from '../store/orbitStore';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import {
|
||||
writeOrbitState,
|
||||
writeOrbitHeartbeat,
|
||||
sweepGuestOutboxes,
|
||||
applyOutboxSnapshotsToState,
|
||||
maybeShuffleQueue,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
suggestionKey,
|
||||
} from '../utils/orbit';
|
||||
import {
|
||||
orbitOutboxPlaylistName,
|
||||
ORBIT_PLAY_QUEUE_LIMIT,
|
||||
type OrbitState,
|
||||
type OrbitQueueItem,
|
||||
@@ -21,6 +19,7 @@ import {
|
||||
import { showToast } from '../utils/ui/toast';
|
||||
import i18n from '../i18n';
|
||||
import { pushOrbitEvent } from '../utils/orbitDiag';
|
||||
import { useOrbitOutboxHeartbeat } from './useOrbitOutboxHeartbeat';
|
||||
|
||||
/**
|
||||
* Orbit — host-side tick hook.
|
||||
@@ -41,7 +40,6 @@ import { pushOrbitEvent } from '../utils/orbitDiag';
|
||||
*/
|
||||
|
||||
const STATE_TICK_MS = 2_500;
|
||||
const HEARTBEAT_TICK_MS = 10_000;
|
||||
|
||||
export function useOrbitHost(): void {
|
||||
const role = useOrbitStore(s => s.role);
|
||||
@@ -49,6 +47,7 @@ export function useOrbitHost(): void {
|
||||
const sessionPlaylistId = useOrbitStore(s => s.sessionPlaylistId);
|
||||
const outboxPlaylistId = useOrbitStore(s => s.outboxPlaylistId);
|
||||
const sessionId = useOrbitStore(s => s.sessionId);
|
||||
const hostName = useOrbitStore(s => s.state?.host);
|
||||
|
||||
// Refs hold the last values we used to build the patch — cheap to
|
||||
// recompute against, no need to subscribe to every playerStore tick.
|
||||
@@ -248,19 +247,7 @@ export function useOrbitHost(): void {
|
||||
};
|
||||
}, [active, sessionPlaylistId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !outboxPlaylistId || !sessionId) return;
|
||||
const server = useOrbitStore.getState().state?.host;
|
||||
if (!server) return;
|
||||
const outboxName = orbitOutboxPlaylistName(sessionId, server);
|
||||
|
||||
const pushHeartbeat = async () => {
|
||||
try { await writeOrbitHeartbeat(outboxPlaylistId, outboxName); }
|
||||
catch { /* best-effort */ }
|
||||
};
|
||||
void pushHeartbeat();
|
||||
|
||||
const id = window.setInterval(() => { void pushHeartbeat(); }, HEARTBEAT_TICK_MS);
|
||||
return () => window.clearInterval(id);
|
||||
}, [active, outboxPlaylistId, sessionId]);
|
||||
// Outbox heartbeat — shared with the guest hook; the host's outbox is keyed
|
||||
// by its own `OrbitState.host` name.
|
||||
useOrbitOutboxHeartbeat(active, outboxPlaylistId, sessionId, hostName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useEffect } from 'react';
|
||||
import { writeOrbitHeartbeat } from '../utils/orbit';
|
||||
import { orbitOutboxPlaylistName } from '../api/orbit';
|
||||
|
||||
const HEARTBEAT_TICK_MS = 10_000;
|
||||
|
||||
/**
|
||||
* Shared Orbit outbox heartbeat — used by both the host and guest tick hooks.
|
||||
*
|
||||
* Refreshes the caller's own outbox playlist comment with a fresh timestamp
|
||||
* every 10 s so the host's participant sweep sees the user as alive (and so
|
||||
* the host's own outbox is refreshed symmetrically). Host and guest differ
|
||||
* only in whose name owns the outbox: `ownName` is `OrbitState.host` for the
|
||||
* host and the active server username for a guest.
|
||||
*
|
||||
* Best-effort — a transient Navidrome outage just skips a beat; the next
|
||||
* interval tick retries.
|
||||
*/
|
||||
export function useOrbitOutboxHeartbeat(
|
||||
active: boolean,
|
||||
outboxPlaylistId: string | null,
|
||||
sessionId: string | null,
|
||||
ownName: string | null | undefined,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!active || !outboxPlaylistId || !sessionId || !ownName) return;
|
||||
const outboxName = orbitOutboxPlaylistName(sessionId, ownName);
|
||||
|
||||
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, ownName]);
|
||||
}
|
||||
Reference in New Issue
Block a user