mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-13 13:07:12 +00:00
feat(host-control-mode): explicit server capabilities for feature detection
Replace the implicit "hostPeerId present ⇒ feature supported" heuristic with an
explicit capabilities list the relay advertises in ROOM_DATA. Cleaner, self-
documenting, and the extensible hook for the planned co-host feature (owner
promotes guests to extra controllers) — a future 'co-host' capability + events
slot in without a protocol bump.
- shared: CAPABILITIES { HOST_CONTROL }.
- server: SERVER_CAPABILITIES advertised in ROOM_DATA.
- background: track serverCapabilities (empty against older relay), serverSupports(),
thread hostControlSupported through GET_STATUS / GET_CONTROL_MODE / CONTROL_MODE.
- popup: gate the host-control card on the explicit capability instead of hostPeerId.
Backwards-compatible both ways: old relay omits the field → feature stays hidden
(no errors); old client ignores the field. WS test asserts ROOM_DATA advertises
the capability. Adds docs/host-control-mode-TESTING.md (beta-server setup, wss
caveat, two-new-clients note, verification checklist).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+15
-5
@@ -1,4 +1,4 @@
|
||||
import { EVENTS, CONTROL_MODES, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js';
|
||||
import { EVENTS, CONTROL_MODES, CAPABILITIES, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js';
|
||||
import { generateUsername } from './shared/names.js';
|
||||
import { loadLocale, getMessage, getSystemLanguage } from './i18n.js';
|
||||
import { sameEpisode } from './episode-utils.js';
|
||||
@@ -74,6 +74,10 @@ const lastSeqBySender = {}; // senderId → last received seq (sta
|
||||
// --- Host Control Mode ---
|
||||
let controlMode = CONTROL_MODES.EVERYONE; // 'everyone' | 'host-only'
|
||||
let hostPeerId = null; // peerId of the room host (creator / fallback)
|
||||
// Features the connected relay advertises in ROOM_DATA. Empty against an older
|
||||
// relay (no capabilities field) → host-control UI/behavior stays unavailable.
|
||||
let serverCapabilities = [];
|
||||
function serverSupports(cap) { return Array.isArray(serverCapabilities) && serverCapabilities.includes(cap); }
|
||||
// Local peer's desync state (content.js reports it via HCM_DESYNC_STATE). Relayed
|
||||
// in heartbeats so the host's popup UI can show "Solo" instead of silently
|
||||
// appearing un-ACK'd.
|
||||
@@ -153,9 +157,10 @@ function ensureState() {
|
||||
if (data.history) history = [...history, ...data.history].slice(0, 20);
|
||||
if (data.currentRoom) {
|
||||
currentRoom = data.currentRoom;
|
||||
// Host Control Mode: restore role/mode from persisted room.
|
||||
// Host Control Mode: restore role/mode/capabilities from persisted room.
|
||||
controlMode = currentRoom.controlMode || CONTROL_MODES.EVERYONE;
|
||||
hostPeerId = currentRoom.hostPeerId || null;
|
||||
serverCapabilities = Array.isArray(currentRoom.capabilities) ? currentRoom.capabilities : [];
|
||||
}
|
||||
if (data.hcmDesynced !== undefined) hcmDesynced = data.hcmDesynced;
|
||||
if (data.lastActionState) lastActionState = data.lastActionState;
|
||||
@@ -472,6 +477,7 @@ async function leaveRoomAfterIdleGrace(reason) {
|
||||
currentRoom = null;
|
||||
controlMode = CONTROL_MODES.EVERYONE;
|
||||
hostPeerId = null;
|
||||
serverCapabilities = [];
|
||||
hcmDesynced = false;
|
||||
// Notify content.js/popup BEFORE currentTabId is cleared so they can reset
|
||||
// any stale guest-side HCM state (dialog/badge/desync) — H-2.
|
||||
@@ -695,7 +701,7 @@ function hcmEnforceDesyncInvariant() {
|
||||
function broadcastControlMode() {
|
||||
// Notify popup (role badge / host toggle) and the active content tab
|
||||
// (so it can enable/disable the host-only guest gate).
|
||||
const payload = { type: 'CONTROL_MODE', controlMode, hostPeerId, amHost: amHost() };
|
||||
const payload = { type: 'CONTROL_MODE', controlMode, hostPeerId, amHost: amHost(), hostControlSupported: serverSupports(CAPABILITIES.HOST_CONTROL) };
|
||||
chrome.runtime.sendMessage(payload).catch(() => {});
|
||||
if (currentTabId) {
|
||||
const tabId = parseInt(currentTabId);
|
||||
@@ -955,6 +961,7 @@ function handleServerEvent(event, data) {
|
||||
// Host Control Mode: adopt room role/mode on (re)join.
|
||||
controlMode = data.controlMode || CONTROL_MODES.EVERYONE;
|
||||
hostPeerId = data.hostPeerId || null;
|
||||
serverCapabilities = Array.isArray(data.capabilities) ? data.capabilities : [];
|
||||
hcmEnforceDesyncInvariant();
|
||||
broadcastControlMode();
|
||||
markRoomPotentiallyIdle();
|
||||
@@ -1588,6 +1595,7 @@ function leaveOldRoomIfSwitching(newRoomId) {
|
||||
currentRoom = null;
|
||||
controlMode = CONTROL_MODES.EVERYONE;
|
||||
hostPeerId = null;
|
||||
serverCapabilities = [];
|
||||
hcmDesynced = false;
|
||||
// Notify content.js/popup so they drop any guest-side HCM state from the
|
||||
// previous room (badge/dialog/desync) — H-2/H-3.
|
||||
@@ -1706,7 +1714,8 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
ping: currentPingMs,
|
||||
controlMode,
|
||||
hostPeerId,
|
||||
amHost: amHost()
|
||||
amHost: amHost(),
|
||||
hostControlSupported: serverSupports(CAPABILITIES.HOST_CONTROL)
|
||||
});
|
||||
} else if (message.type === 'SET_CONTROL_MODE') {
|
||||
// Popup (host) toggles the room control mode. Server validates host authority
|
||||
@@ -1727,7 +1736,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
// persisted desync state so a page reload re-adopts it — otherwise a fresh
|
||||
// content script would start synced while background keeps relaying us as
|
||||
// "Solo" to the host (stale-badge split-brain).
|
||||
sendResponse({ controlMode, hostPeerId, amHost: amHost(), desynced: hcmDesynced });
|
||||
sendResponse({ controlMode, hostPeerId, amHost: amHost(), desynced: hcmDesynced, hostControlSupported: serverSupports(CAPABILITIES.HOST_CONTROL) });
|
||||
} else if (message.type === 'REQUEST_HOST_SYNC') {
|
||||
// content.js resync: hand back the host's extrapolated current position.
|
||||
sendResponse({ target: getHostSyncTarget() });
|
||||
@@ -1768,6 +1777,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
currentRoom = null;
|
||||
controlMode = CONTROL_MODES.EVERYONE;
|
||||
hostPeerId = null;
|
||||
serverCapabilities = [];
|
||||
hcmDesynced = false;
|
||||
// Notify content.js/popup BEFORE currentTabId is cleared so they drop any
|
||||
// stale guest-side HCM state (dialog/badge/desync) — H-2/H-3.
|
||||
|
||||
+7
-7
@@ -249,7 +249,7 @@ async function init() {
|
||||
updatePingDisplay(res.ping);
|
||||
updatePeerList(res.peers);
|
||||
lastKnownPeers = res.peers || [];
|
||||
updateHostControlUI(res.controlMode, res.amHost, res.hostPeerId, res.status === 'connected');
|
||||
updateHostControlUI(res.controlMode, res.amHost, res.hostControlSupported, res.status === 'connected');
|
||||
if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers);
|
||||
|
||||
// If user has a room configured but background is not connected (disconnected or idle),
|
||||
@@ -313,14 +313,14 @@ function toggleUIState(inRoom) {
|
||||
// True when we're a guest in a host-only room → remote-control buttons are locked.
|
||||
let hcmGuestLocked = false;
|
||||
|
||||
function updateHostControlUI(controlMode, amHost, hostPeerId, inRoom) {
|
||||
function updateHostControlUI(controlMode, amHost, hostControlSupported, inRoom) {
|
||||
const card = elements.hostControlCard;
|
||||
if (!card) return;
|
||||
const hostOnly = controlMode === 'host-only';
|
||||
// The server only sends hostPeerId once it supports host control. Against an
|
||||
// older relay it's absent → the feature is unavailable, so hide the card
|
||||
// Explicit capability advertised by the relay in ROOM_DATA. Against an older
|
||||
// relay it's false/absent → the feature is unavailable, so hide the card
|
||||
// entirely instead of showing a misleading "Guest".
|
||||
const serverSupportsHostControl = !!hostPeerId;
|
||||
const serverSupportsHostControl = !!hostControlSupported;
|
||||
// Only show the card when it's actually meaningful:
|
||||
// - host: always (so they can enable/disable host-only)
|
||||
// - guest: only while host-only is active (explains why they can't control)
|
||||
@@ -1745,7 +1745,7 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (msg.peers) detectPeerChanges(msg.peers);
|
||||
} else if (msg.type === 'CONTROL_MODE') {
|
||||
const inRoom = elements.sectionActive && elements.sectionActive.style.display === 'block';
|
||||
updateHostControlUI(msg.controlMode, msg.amHost, msg.hostPeerId, inRoom);
|
||||
updateHostControlUI(msg.controlMode, msg.amHost, msg.hostControlSupported, inRoom);
|
||||
} else if (msg.type === 'CONNECTION_STATUS') {
|
||||
if (msg.status === 'connected' || msg.status === 'disconnected') {
|
||||
if (joinBtnTimeout) { clearTimeout(joinBtnTimeout); joinBtnTimeout = null; }
|
||||
@@ -1763,7 +1763,7 @@ chrome.runtime.onMessage.addListener((msg) => {
|
||||
if (res.peers) updatePeerList(res.peers);
|
||||
if (res.lastActionState) updateLastActionUI(res.lastActionState, res.peers);
|
||||
updatePingDisplay(res.ping);
|
||||
updateHostControlUI(res.controlMode, res.amHost, res.hostPeerId, true);
|
||||
updateHostControlUI(res.controlMode, res.amHost, res.hostControlSupported, true);
|
||||
});
|
||||
}
|
||||
if (msg.status === 'disconnected') {
|
||||
|
||||
Reference in New Issue
Block a user