mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
chore(orbit): retry initial guest sync until it actually lands
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+49
-25
@@ -66,10 +66,10 @@ export function useOrbitGuest(): void {
|
|||||||
* past the deadline so a loading error doesn't leave the guest stuck
|
* past the deadline so a loading error doesn't leave the guest stuck
|
||||||
* on a silent pause.
|
* on a silent pause.
|
||||||
*/
|
*/
|
||||||
const syncToHost = async (trackId: string, hostState: OrbitState) => {
|
const syncToHost = async (trackId: string, hostState: OrbitState): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const song = await getSong(trackId);
|
const song = await getSong(trackId);
|
||||||
if (!song || cancelled) return;
|
if (!song || cancelled) return false;
|
||||||
const track = songToTrack(song);
|
const track = songToTrack(song);
|
||||||
// Clamp fraction to [0, 0.99] — if the host's positionAt is unusually
|
// Clamp fraction to [0, 0.99] — if the host's positionAt is unusually
|
||||||
// stale, estimateLivePosition can overshoot the track duration and a
|
// stale, estimateLivePosition can overshoot the track duration and a
|
||||||
@@ -79,37 +79,37 @@ export function useOrbitGuest(): void {
|
|||||||
const targetSec = Math.max(0, targetMs / 1000);
|
const targetSec = Math.max(0, targetMs / 1000);
|
||||||
return Math.max(0, Math.min(0.99, targetSec / Math.max(1, track.duration)));
|
return Math.max(0, Math.min(0.99, targetSec / Math.max(1, track.duration)));
|
||||||
};
|
};
|
||||||
const applyMirror = () => {
|
const applyMirror = (): boolean => {
|
||||||
const p = usePlayerStore.getState();
|
const p = usePlayerStore.getState();
|
||||||
if (cancelled || p.currentTrack?.id !== trackId) return;
|
if (cancelled || p.currentTrack?.id !== trackId) return false;
|
||||||
p.seek(calcFraction());
|
p.seek(calcFraction());
|
||||||
if (hostState.isPlaying && !p.isPlaying) p.resume();
|
if (hostState.isPlaying && !p.isPlaying) p.resume();
|
||||||
else if (!hostState.isPlaying && p.isPlaying) p.pause();
|
else if (!hostState.isPlaying && p.isPlaying) p.pause();
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const player = usePlayerStore.getState();
|
const player = usePlayerStore.getState();
|
||||||
if (player.currentTrack?.id === trackId) {
|
if (player.currentTrack?.id === trackId) {
|
||||||
applyMirror();
|
return applyMirror();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
player.playTrack(track, [track]);
|
player.playTrack(track, [track]);
|
||||||
|
|
||||||
// Poll until the engine has the track loaded AND matches host play
|
// Poll until the engine has the track loaded; fall back to a blind
|
||||||
// state; fall back to a blind apply after 2 s so a stuck load (audio
|
// apply after 2 s so a stuck load doesn't leave us spinning forever.
|
||||||
// error reverts isPlaying → false) doesn't leave the guest silent.
|
return await new Promise<boolean>(resolve => {
|
||||||
const deadline = Date.now() + 2000;
|
const deadline = Date.now() + 2000;
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
if (cancelled) return;
|
if (cancelled) { resolve(false); return; }
|
||||||
const p = usePlayerStore.getState();
|
const p = usePlayerStore.getState();
|
||||||
const trackReady = p.currentTrack?.id === trackId;
|
const trackReady = p.currentTrack?.id === trackId;
|
||||||
const stateMatches = p.isPlaying === hostState.isPlaying;
|
if (trackReady && p.isPlaying) { resolve(applyMirror()); return; }
|
||||||
if (trackReady && (stateMatches || p.isPlaying)) { applyMirror(); return; }
|
if (Date.now() >= deadline) { resolve(applyMirror()); return; }
|
||||||
if (Date.now() >= deadline) { applyMirror(); return; }
|
window.setTimeout(poll, 100);
|
||||||
|
};
|
||||||
window.setTimeout(poll, 100);
|
window.setTimeout(poll, 100);
|
||||||
};
|
});
|
||||||
window.setTimeout(poll, 100);
|
} catch { return false; }
|
||||||
} catch { /* silent */ }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pull = async () => {
|
const pull = async () => {
|
||||||
@@ -171,8 +171,18 @@ export function useOrbitGuest(): void {
|
|||||||
const last = lastAppliedRef.current;
|
const last = lastAppliedRef.current;
|
||||||
|
|
||||||
if (!last) {
|
if (!last) {
|
||||||
if (hostTrackId) void syncToHost(hostTrackId, state);
|
// Initial sync: only record `last` *after* syncToHost actually
|
||||||
lastAppliedRef.current = { trackId: hostTrackId, isPlaying: hostPlaying };
|
// landed. If the first attempt loses the race (engine not ready,
|
||||||
|
// stale audio state, network blip), a retry ticker below will try
|
||||||
|
// again every 500 ms until it succeeds. Without this, the first
|
||||||
|
// failed sync set `last` anyway and the guest was stuck on their
|
||||||
|
// pre-join state until they clicked Catch Up.
|
||||||
|
if (hostTrackId) {
|
||||||
|
const ok = await syncToHost(hostTrackId, state);
|
||||||
|
if (ok) lastAppliedRef.current = { trackId: hostTrackId, isPlaying: hostPlaying };
|
||||||
|
} else {
|
||||||
|
lastAppliedRef.current = { trackId: null, isPlaying: hostPlaying };
|
||||||
|
}
|
||||||
} else if (last.trackId !== hostTrackId) {
|
} else if (last.trackId !== hostTrackId) {
|
||||||
if (hostTrackId) void syncToHost(hostTrackId, state);
|
if (hostTrackId) void syncToHost(hostTrackId, state);
|
||||||
else if (player.isPlaying) player.pause();
|
else if (player.isPlaying) player.pause();
|
||||||
@@ -191,9 +201,23 @@ export function useOrbitGuest(): void {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void pull();
|
// Self-scheduling tick: fast-poll (500 ms) while we haven't locked in an
|
||||||
const id = window.setInterval(() => { void pull(); }, STATE_READ_TICK_MS);
|
// initial sync yet, fall back to the steady cadence once we're anchored.
|
||||||
return () => { cancelled = true; window.clearInterval(id); };
|
// Lets a failed first attempt retry quickly without spamming the network
|
||||||
|
// for the lifetime of the session.
|
||||||
|
let timer: number | null = null;
|
||||||
|
const tick = async () => {
|
||||||
|
timer = null;
|
||||||
|
await pull();
|
||||||
|
if (cancelled) return;
|
||||||
|
const delay = lastAppliedRef.current === null ? 500 : STATE_READ_TICK_MS;
|
||||||
|
timer = window.setTimeout(tick, delay);
|
||||||
|
};
|
||||||
|
void tick();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
if (timer !== null) window.clearTimeout(timer);
|
||||||
|
};
|
||||||
}, [active, sessionPlaylistId]);
|
}, [active, sessionPlaylistId]);
|
||||||
|
|
||||||
// ── Heartbeat ────────────────────────────────────────────────────────
|
// ── Heartbeat ────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user