mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-30 12:29:27 +00:00
133 lines
4.9 KiB
JavaScript
133 lines
4.9 KiB
JavaScript
import { CAPABILITIES, MAX_MEDIA_TIME } from './shared/constants.js';
|
|
|
|
export function validateCanonicalMediaState(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
if (!Number.isSafeInteger(value.revision) || value.revision < 1) return null;
|
|
if (value.playbackState !== 'playing' && value.playbackState !== 'paused') return null;
|
|
if (typeof value.currentTime !== 'number'
|
|
|| !Number.isFinite(value.currentTime)
|
|
|| value.currentTime < 0
|
|
|| value.currentTime > MAX_MEDIA_TIME) {
|
|
return null;
|
|
}
|
|
const normalized = {
|
|
revision: value.revision,
|
|
playbackState: value.playbackState,
|
|
currentTime: value.currentTime
|
|
};
|
|
if (typeof value.updatedBy === 'string' && value.updatedBy) {
|
|
normalized.updatedBy = value.updatedBy.substring(0, 16);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function canonicalMediaStateFromRoomData(roomData) {
|
|
const capabilities = Array.isArray(roomData?.capabilities) ? roomData.capabilities : [];
|
|
if (!capabilities.includes(CAPABILITIES.MEDIA_STATE_V1)) {
|
|
return { status: 'unsupported', mediaState: null };
|
|
}
|
|
if (roomData.mediaState === null || roomData.mediaState === undefined) {
|
|
return { status: 'empty', mediaState: null };
|
|
}
|
|
const mediaState = validateCanonicalMediaState(roomData.mediaState);
|
|
return mediaState
|
|
? { status: 'available', mediaState }
|
|
: { status: 'invalid', mediaState: null };
|
|
}
|
|
|
|
function normalizeRoomId(roomId) {
|
|
return typeof roomId === 'string' && roomId ? roomId : null;
|
|
}
|
|
|
|
export function createCanonicalMediaStateTracker() {
|
|
let roomId = null;
|
|
let knownRevision = 0;
|
|
let appliedRevision = 0;
|
|
let pending = null;
|
|
|
|
function adoptRoom(nextRoomId) {
|
|
const normalizedRoomId = normalizeRoomId(nextRoomId);
|
|
if (normalizedRoomId === roomId) return false;
|
|
roomId = normalizedRoomId;
|
|
knownRevision = 0;
|
|
appliedRevision = 0;
|
|
pending = null;
|
|
return true;
|
|
}
|
|
|
|
return {
|
|
adoptRoom,
|
|
|
|
beginRecovery(nextRoomId) {
|
|
adoptRoom(nextRoomId);
|
|
appliedRevision = 0;
|
|
pending = null;
|
|
},
|
|
|
|
receive(nextRoomId, value) {
|
|
adoptRoom(nextRoomId);
|
|
const mediaState = validateCanonicalMediaState(value);
|
|
if (!roomId || !mediaState) return { status: 'invalid' };
|
|
if (mediaState.revision < knownRevision) return { status: 'stale' };
|
|
if (mediaState.revision === knownRevision
|
|
&& (appliedRevision === mediaState.revision || pending?.mediaState.revision === mediaState.revision)) {
|
|
return { status: 'duplicate' };
|
|
}
|
|
knownRevision = Math.max(knownRevision, mediaState.revision);
|
|
pending = { roomId, mediaState };
|
|
return { status: 'pending', mediaState };
|
|
},
|
|
|
|
getPending(nextRoomId = roomId) {
|
|
return pending && pending.roomId === normalizeRoomId(nextRoomId)
|
|
? { roomId: pending.roomId, mediaState: { ...pending.mediaState } }
|
|
: null;
|
|
},
|
|
|
|
markHandled(nextRoomId, revision) {
|
|
if (normalizeRoomId(nextRoomId) !== roomId
|
|
|| !Number.isSafeInteger(revision)
|
|
|| revision < 1) {
|
|
return false;
|
|
}
|
|
knownRevision = Math.max(knownRevision, revision);
|
|
appliedRevision = Math.max(appliedRevision, revision);
|
|
if (pending?.mediaState.revision <= revision) pending = null;
|
|
return true;
|
|
},
|
|
|
|
clear() {
|
|
adoptRoom(null);
|
|
},
|
|
|
|
restore(value, currentRoomId) {
|
|
adoptRoom(currentRoomId);
|
|
if (!value || typeof value !== 'object' || value.roomId !== roomId || !roomId) return false;
|
|
knownRevision = Number.isSafeInteger(value.knownRevision) && value.knownRevision >= 0
|
|
? value.knownRevision
|
|
: 0;
|
|
appliedRevision = Number.isSafeInteger(value.appliedRevision) && value.appliedRevision >= 0
|
|
? Math.min(value.appliedRevision, knownRevision)
|
|
: 0;
|
|
const restoredPending = validateCanonicalMediaState(value.pending?.mediaState);
|
|
if (value.pending?.roomId === roomId
|
|
&& restoredPending
|
|
&& restoredPending.revision >= appliedRevision
|
|
&& restoredPending.revision >= knownRevision) {
|
|
pending = { roomId, mediaState: restoredPending };
|
|
knownRevision = restoredPending.revision;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
snapshot() {
|
|
return {
|
|
roomId,
|
|
knownRevision,
|
|
appliedRevision,
|
|
pending: pending ? { roomId: pending.roomId, mediaState: { ...pending.mediaState } } : null
|
|
};
|
|
}
|
|
};
|
|
}
|