exp(orbit): periodic shuffle

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-23 01:03:18 +02:00
parent 60e0bbfa2a
commit cebf0e238d
2 changed files with 36 additions and 3 deletions
+7 -3
View File
@@ -6,6 +6,7 @@ import {
writeOrbitHeartbeat,
sweepGuestOutboxes,
applyOutboxSnapshotsToState,
maybeShuffleQueue,
} from '../utils/orbit';
import { orbitOutboxPlaylistName, type OrbitState } from '../api/orbit';
@@ -79,10 +80,13 @@ export function useOrbitHost(): void {
afterSweep = applyOutboxSnapshotsToState(base, snaps);
} catch { /* best-effort; keep old participants and queue */ }
// 2) Overlay the host's live playback snapshot.
const next: OrbitState = { ...afterSweep, ...snapshotPlayerPatch(base.host) };
// 2) Shuffle check — no-op unless >= 15 min since last shuffle.
const afterShuffle = maybeShuffleQueue(afterSweep);
// 3) Commit locally + push remote.
// 3) Overlay the host's live playback snapshot.
const next: OrbitState = { ...afterShuffle, ...snapshotPlayerPatch(base.host) };
// 4) Commit locally + push remote.
useOrbitStore.getState().setState(next);
try {
await writeOrbitState(sessionPlaylistId, next);
+29
View File
@@ -513,6 +513,35 @@ export async function sweepGuestOutboxes(sid: string, hostUsername: string): Pro
/** How long we consider a heartbeat still fresh. Longer than the guest tick so a single missed beat is tolerated. */
export const ORBIT_HEARTBEAT_ALIVE_MS = 30_000;
/** Shuffle cadence — queue is reshuffled once every interval. */
export const ORBIT_SHUFFLE_INTERVAL_MS = 15 * 60_000;
/**
* Host helper — applies a Fisher-Yates shuffle to `state.queue` iff enough
* time has passed since the last shuffle. Pure, returns a new state object.
* `currentTrack` is never touched.
*/
export function maybeShuffleQueue(state: OrbitState, nowMs: number = Date.now()): OrbitState {
if (nowMs - state.lastShuffle < ORBIT_SHUFFLE_INTERVAL_MS) return state;
if (state.queue.length < 2) {
// Still bump `lastShuffle` so the next eligible shuffle is 15 min away,
// preventing a tight retry loop right after a guest drops a single item in.
return { ...state, lastShuffle: nowMs };
}
const shuffled = state.queue.slice();
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return { ...state, queue: shuffled, lastShuffle: nowMs };
}
/** Drift between a guest's local playback and the host's estimated live position. */
export function computeOrbitDriftMs(state: OrbitState, guestPositionMs: number, nowMs: number = Date.now()): number {
const hostEstimated = state.positionMs + (state.isPlaying ? (nowMs - state.positionAt) : 0);
return guestPositionMs - hostEstimated;
}
/**
* Fold sweep results into an updated `OrbitState`.
*