Files
Psychotoxical-psysonic/src/utils/orbit.ts
T
Psychotoxical 78a177b1bc fix(orbit): outbox lost-update recovery and session hardening (#1159)
* fix(orbit): harden the guest outbox — recover lost suggestions, avoid a duplicate outbox

Two outbox robustness issues on the poll-based, non-atomic playlist
transport:

- Lost-update: a track a guest appends to its outbox can be wiped by a
  concurrent host sweep-clear before the host recorded it, leaving the
  suggestion lost AND stuck on "waiting on host" forever (it only clears
  once it reaches the host's play queue). The playlist read-modify-write
  can't be made atomic, so recover: re-send a pending suggestion the host
  hasn't recorded (absent from state.queue) past a grace window — the host
  dedupes by (user, trackId), so it's idempotent — and give up + toast past
  a 45s window so the row stops hanging. New pendingResend planner module
  with unit tests; suggest + tick share ensureTrackInOutbox.

- Duplicate outbox: a transient getPlaylists failure at join was swallowed
  to [], so the existing-outbox check missed and a second outbox was
  created. Distinguish "lookup failed" from "genuinely absent" and retry
  once before falling back to create.

New i18n key for the give-up toast across all locales.

* fix(orbit): reject non-http(s) schemes in share invites

normalizeShareServerUrl only prepends http:// when the server string doesn't
already start with "http", so an invite carrying a scheme like httpx:// or
https-phish:// slips through unchanged. Reject anything whose parsed protocol
isn't http(s) at parse time, before the known-server match. Add round-trip
tests covering the accepted and rejected schemes.

* fix(orbit): read exit-modal role fresh to avoid a stale keydown closure

The exit modal's Enter/Escape handler binds once per open (deps [isOpen]) and
closed over the role prop, so a role change while the modal was up would act
on a stale value. Read role from the store inside the action instead, and drop
the now-unused reactive subscription.

* docs(changelog): consolidate Orbit fixes into one block, add 1159
2026-06-22 17:32:53 +02:00

92 lines
2.6 KiB
TypeScript

/**
* Orbit — multi-user listen-together feature.
*
* This file is a re-export shim. Implementation lives under `utils/orbit/`:
*
* - `constants.ts` — cadence + TTL ms values.
* - `helpers.ts` — pure utilities (id gen, serialisation, key fns).
* - `stateMath.ts` — pure state transforms (shuffle, drift, sweep merge).
* - `remote.ts` — Navidrome playlist comment I/O.
* - `shareLink.ts` — invite magic-string encode/decode.
* - `host.ts` — host session lifecycle (start, end, settings, enqueue).
* - `moderation.ts` — host kicks / soft-removes / mutes.
* - `guest.ts` — guest join/leave + suggestion pipeline + host
* approve/decline reactions.
* - `sweep.ts` — host-side outbox sweep loop.
* - `cleanup.ts` — app-start orphan playlist sweep.
*
* Importers everywhere else in the codebase keep using `'../utils/orbit'`;
* this shim re-exports every public symbol unchanged.
*/
export {
ORBIT_HEARTBEAT_ALIVE_MS,
ORBIT_ORPHAN_TTL_MS,
ORBIT_REMOVED_TTL_MS,
ORBIT_SHUFFLE_INTERVAL_MS,
} from './orbit/constants';
export {
generateSessionId,
makeCoalescedRunner,
OrbitStateTooLarge,
serialiseOrbitState,
suggestionKey,
} from './orbit/helpers';
export { isOrbitPlaybackSyncActive } from './orbit/sessionActive';
export {
applyOutboxSnapshotsToState,
computeOrbitDriftMs,
effectiveShuffleIntervalMs,
maybeShuffleQueue,
patchOrbitState,
} from './orbit/stateMath';
export {
findSessionPlaylistId,
readOrbitState,
writeOrbitHeartbeat,
writeOrbitState,
} from './orbit/remote';
export {
readOrbitTransitionSettings,
applyOrbitTransitionSettings,
saveGuestTransitionsOnce,
restoreGuestTransitions,
} from './orbit/transitions';
export {
buildOrbitShareLink,
parseOrbitShareLink,
type OrbitShareLink,
} from './orbit/shareLink';
export {
endOrbitSession,
hostEnqueueToOrbit,
startOrbitSession,
triggerOrbitShuffleNow,
updateOrbitSettings,
type StartOrbitArgs,
} from './orbit/host';
export {
kickOrbitParticipant,
removeOrbitParticipant,
setOrbitSuggestionBlocked,
} from './orbit/moderation';
export {
approveOrbitSuggestion,
declineOrbitSuggestion,
ensureTrackInOutbox,
evaluateOrbitSuggestGate,
joinOrbitSession,
leaveOrbitSession,
OrbitJoinError,
OrbitSuggestBlockedError,
suggestOrbitTrack,
type OrbitSuggestGateReason,
} from './orbit/guest';
export {
forgetPendingSuggestion,
planPendingResends,
resetPendingResendState,
} from './orbit/pendingResend';
export { sweepGuestOutboxes } from './orbit/sweep';
export { cleanupOrphanedOrbitPlaylists } from './orbit/cleanup';