close #2183 Reusing a room means whoever enters first becomes the host, and moving the session to someone else's library meant everyone leaving and re-entering in the right order. The host can now promote any connected guest from the participant list (Watch Together screen and the in-player session sheet), with playback, control mode, and reconnect identities carried across. The relay owns the swap: a new `transferHost` message validates the sender is the live host and the target a connected modern-protocol guest, then swaps `HostPeerID` and the reconnect verifiers (each peer keeps its own token), persists the room, and broadcasts `hostChanged` to every peer. Clients flip roles in place: the controller swaps its role engine while keeping the session, message queue, and player attachment. A promoted guest seeds the coordinator with the room's last known intent (a paused room stays paused) and the known-peer roster so the fresh epoch re-gates instead of solo-starting; a demoted host falls back to a reconciler with a fresh clock sync and asks the new authority for state. Guests re-pin the host identity, reset their sequence, and re-converge their clocks. A rejected transfer (`not_host` / `peer_not_found`) surfaces as a toast instead of tearing the session down. Pre-transfer app builds in the room ignore `hostChanged` and stop following the room at the next transfer; targets on an old sync protocol are not offered the action.
57 lines
1.8 KiB
Go
57 lines
1.8 KiB
Go
// Code generated by scripts/codegen/generate_relay_protocol.py. DO NOT EDIT.
|
|
|
|
package main
|
|
|
|
const (
|
|
relayProtocolVersion = 2
|
|
legacyRelayProtocolVersion = 0
|
|
|
|
relayTypeCreate = "create"
|
|
relayTypeJoin = "join"
|
|
relayTypeBroadcast = "broadcast"
|
|
relayTypeSendTo = "sendTo"
|
|
relayTypePing = "ping"
|
|
relayTypeLeave = "leave"
|
|
relayTypeEndSession = "endSession"
|
|
relayTypeTransferHost = "transferHost"
|
|
relayTypeCreated = "created"
|
|
relayTypeJoined = "joined"
|
|
relayTypePeerJoined = "peerJoined"
|
|
relayTypePeerLeft = "peerLeft"
|
|
relayTypeMessage = "message"
|
|
relayTypeError = "error"
|
|
relayTypePong = "pong"
|
|
relayTypeLeft = "left"
|
|
relayTypeEnded = "ended"
|
|
relayTypeHostChanged = "hostChanged"
|
|
relayErrorRateLimited = "rate_limited"
|
|
relayErrorInvalidMessage = "invalid_message"
|
|
relayErrorRoomExists = "room_exists"
|
|
relayErrorRoomNotFound = "room_not_found"
|
|
relayErrorRoomFull = "room_full"
|
|
relayErrorNotInRoom = "not_in_room"
|
|
relayErrorAlreadyInRoom = "already_in_room"
|
|
relayErrorPeerIdUnavailable = "peer_id_unavailable"
|
|
relayErrorProtocolMismatch = "protocol_mismatch"
|
|
relayErrorNotHost = "not_host"
|
|
relayErrorPeerNotFound = "peer_not_found"
|
|
|
|
maxRoomSize = 8
|
|
maxMessageSize = 65536
|
|
maxSessionIDLength = 64
|
|
maxPeerIDLength = 128
|
|
)
|
|
|
|
func validRelayID(value string, maxLength int) bool {
|
|
if len(value) == 0 || len(value) > maxLength {
|
|
return false
|
|
}
|
|
for _, ch := range value {
|
|
if (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') &&
|
|
(ch < '0' || ch > '9') && ch != '_' && ch != '-' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|