@carverjs/multiplayer 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # @carverjs/multiplayer
2
+
3
+ [![npm](https://img.shields.io/npm/v/@carverjs/multiplayer)](https://www.npmjs.com/package/@carverjs/multiplayer)
4
+ [![license](https://img.shields.io/npm/l/@carverjs/multiplayer)](https://github.com/MoneyTales/carverjs/blob/main/LICENSE)
5
+ [![Discord](https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/5ymwfD4hYE)
6
+
7
+ Serverless peer-to-peer multiplayer for [CarverJS](https://www.npmjs.com/package/@carverjs/core) games. A WebRTC data-channel mesh with pluggable signaling (MQTT or Firebase), lobbies, host authority, and three sync engines — **no game server required**.
8
+
9
+ > **Beta:** CarverJS is under active development. APIs may change between minor versions until 1.0.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @carverjs/multiplayer
15
+ # optional — Firebase RTDB signaling (MQTT is the zero-config default)
16
+ npm install firebase
17
+ ```
18
+
19
+ Peer dependencies: `@carverjs/core`, `@react-three/fiber`, `react`, `react-dom`. `firebase` is an optional peer, needed only when you choose the Firebase strategy.
20
+
21
+ ## How it works
22
+
23
+ Signaling (MQTT or Firebase) is used only to introduce peers and relay SDP/ICE. Once the WebRTC connection is established, **all game data flows directly peer-to-peer** over data channels — the signaling backend never sees gameplay traffic. One peer acts as the authoritative host; host migrates automatically if it leaves.
24
+
25
+ ## Quick start
26
+
27
+ Wrap your game in a provider, join a room, and exchange typed events:
28
+
29
+ ```tsx
30
+ import {
31
+ MultiplayerProvider, MultiplayerBridge,
32
+ useRoom, usePlayers, useNetworkEvents,
33
+ } from "@carverjs/multiplayer";
34
+ import { Game, World } from "@carverjs/core/components";
35
+
36
+ function App() {
37
+ return (
38
+ // Zero-config: free public MQTT brokers handle signaling
39
+ <MultiplayerProvider appId="my-game">
40
+ <Game mode="2d">
41
+ <MultiplayerBridge>
42
+ <World>
43
+ <Lobby />
44
+ </World>
45
+ </MultiplayerBridge>
46
+ </Game>
47
+ </MultiplayerProvider>
48
+ );
49
+ }
50
+
51
+ function Lobby() {
52
+ const room = useRoom("room-code-1234", { displayName: "Ada" });
53
+ const { players, self } = usePlayers();
54
+ const { broadcast, onEvent } = useNetworkEvents();
55
+
56
+ // room.connectionState, room.isHost, room.selfId, room.leave(), ...
57
+ return <span>{players.length} players · {room.isHost ? "host" : "client"}</span>;
58
+ }
59
+ ```
60
+
61
+ `MultiplayerBridge` connects the engine's render loop to the network layer; place it inside `<Game>` and around the scene that uses sync hooks.
62
+
63
+ ## Signaling strategies
64
+
65
+ ```tsx
66
+ // Free, zero-config (default): public MQTT brokers
67
+ <MultiplayerProvider appId="my-game">
68
+
69
+ // Firebase Realtime Database (bring your own project)
70
+ <MultiplayerProvider
71
+ appId="my-game"
72
+ strategy={{ type: "firebase", databaseURL: "https://your-project.firebaseio.com" }}
73
+ >
74
+ ```
75
+
76
+ ## STUN / TURN
77
+
78
+ Defaults to public STUN. Add a TURN relay so peers behind restrictive NATs or firewalls can still connect:
79
+
80
+ ```tsx
81
+ <MultiplayerProvider
82
+ appId="my-game"
83
+ iceServers={[
84
+ { urls: "stun:stun.cloudflare.com:3478" },
85
+ { urls: "turn:turn.cloudflare.com:3478", username: "...", credential: "..." },
86
+ ]}
87
+ >
88
+ ```
89
+
90
+ TURN is only used when a direct connection fails. For same-network testing, STUN alone is enough.
91
+
92
+ ## Sync modes
93
+
94
+ `useMultiplayer({ mode })` selects how world state is replicated:
95
+
96
+ | Mode | Use it for | How |
97
+ | --- | --- | --- |
98
+ | `events` | turn-based, sandbox, chat, infrequent state changes | typed messages over a reliable, ordered channel |
99
+ | `snapshot` | real-time movement | host broadcasts delta-compressed snapshots; clients interpolate |
100
+ | `prediction` | fast-paced action | client-side prediction with server reconciliation and rollback |
101
+
102
+ ```tsx
103
+ import { useMultiplayer } from "@carverjs/multiplayer";
104
+
105
+ function Scene() {
106
+ useMultiplayer({ mode: "snapshot", tickRate: 60 });
107
+ // ... actors marked networked are replicated automatically
108
+ }
109
+ ```
110
+
111
+ ## Hooks
112
+
113
+ | Hook | Purpose |
114
+ | --- | --- |
115
+ | `useRoom(roomId, opts)` | Join / leave a room; exposes connection state, host, and self id. |
116
+ | `useLobby()` | Browse advertised rooms. |
117
+ | `usePlayers()` | Live player list plus `self`. |
118
+ | `useHost()` | Host-only room controls — room state, lock, kick, host transfer. |
119
+ | `useMultiplayer({ mode })` | Drive the sync engine for a scene. |
120
+ | `useNetworkEvents()` | Typed `broadcast` / `sendEvent` / `onEvent` messaging. |
121
+ | `useNetworkState()` | Networked spawn / despawn helpers. |
122
+
123
+ ## Host authority & migration
124
+
125
+ Exactly one peer is the authoritative host. If it disconnects, the engine migrates host to another peer automatically, and host election is deterministic and consistent across all peers.
126
+
127
+ To pin a specific peer as host — for example the room creator that owns the world — advertise a host priority in player metadata. The lowest priority wins; peers that advertise none rank last (preserving the default lowest-peer-id election among them):
128
+
129
+ ```tsx
130
+ useRoom(roomId, {
131
+ displayName: name,
132
+ playerMetadata: { hostPriority: isCreator ? 0 : 1 },
133
+ });
134
+ ```
135
+
136
+ ## Advanced exports
137
+
138
+ For lower-level control, the package also exports:
139
+
140
+ - `MqttStrategy`, `FirebaseStrategy` — construct or inject a signaling strategy directly.
141
+ - `NetworkSimulator` — inject artificial latency and packet loss during development.
142
+ - `InterestManager` — area-of-interest filtering for large worlds.
143
+ - `InputBuffer`, `computeJustPressed` — input history and edge detection for prediction.
144
+ - `DebugOverlay` — on-screen network stats.
145
+
146
+ ## Links
147
+
148
+ - Documentation: [docs.carverjs.dev](https://docs.carverjs.dev)
149
+ - Community: [Discord](https://discord.gg/5ymwfD4hYE)
150
+ - Issues: [github.com/MoneyTales/carverjs/issues](https://github.com/MoneyTales/carverjs/issues)
151
+
152
+ ## License
153
+
154
+ [MIT](https://github.com/MoneyTales/carverjs/blob/main/LICENSE) — MoneyTales EduTech Private Limited
@@ -369,7 +369,7 @@ var FirebaseStrategy = class {
369
369
  this._joinGeneration++;
370
370
  this._roomId = roomId;
371
371
  this._peerMeta = peerMeta;
372
- const { ref, set, onChildAdded, onChildRemoved, onDisconnect, remove } = this._fb;
372
+ const { ref, set, onChildAdded, onChildRemoved, onDisconnect, onValue, remove } = this._fb;
373
373
  const paths = firebasePaths(this._appId, roomId, this.selfId);
374
374
  await remove(ref(this._db, paths.peerSignalInbox)).catch(() => {
375
375
  });
@@ -380,6 +380,18 @@ var FirebaseStrategy = class {
380
380
  ts: Date.now()
381
381
  });
382
382
  onDisconnect(presenceRef).remove();
383
+ const generation = this._joinGeneration;
384
+ const connectedRef = ref(this._db, ".info/connected");
385
+ const connectedUnsub = onValue(connectedRef, (snap) => {
386
+ if (snap.val() !== true) return;
387
+ if (this._destroyed || this._joinGeneration !== generation || this._roomId !== roomId) return;
388
+ onDisconnect(presenceRef).remove().then(() => {
389
+ if (this._destroyed || this._joinGeneration !== generation || this._roomId !== roomId) return;
390
+ return set(presenceRef, { peerId: this.selfId, meta: peerMeta, ts: Date.now() });
391
+ }).catch(() => {
392
+ });
393
+ });
394
+ this._listeners.push(() => connectedUnsub());
383
395
  const peersRef = ref(this._db, paths.peers);
384
396
  const addedUnsub = onChildAdded(peersRef, (snapshot) => {
385
397
  const data = snapshot.val();
@@ -609,4 +621,4 @@ export {
609
621
  MqttStrategy,
610
622
  FirebaseStrategy
611
623
  };
612
- //# sourceMappingURL=chunk-CBTAOVXP.mjs.map
624
+ //# sourceMappingURL=chunk-GOTAQDBJ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/strategy/utils.ts","../src/transport/strategy/mqtt.ts","../src/transport/strategy/firebase.ts"],"sourcesContent":["/** Generate a random peer ID (20 chars, URL-safe) */\nexport function generatePeerId(): string {\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n const bytes = new Uint8Array(20);\n crypto.getRandomValues(bytes);\n let id = '';\n for (let i = 0; i < 20; i++) {\n id += chars[bytes[i] % chars.length];\n }\n return id;\n}\n\n/** Build MQTT topic paths for a given appId + roomId */\nexport function mqttTopics(appId: string, roomId: string, peerId?: string) {\n const base = `carver/${appId}`;\n return {\n /** Lobby wildcard: subscribe to discover all room announcements */\n lobbyWildcard: `${base}/lobby/+`,\n /** Single room lobby entry */\n roomLobbyEntry: `${base}/lobby/${roomId}`,\n /** Wildcard for all peer presence in a room */\n roomPresenceWildcard: `${base}/room/${roomId}/presence/+`,\n /** This peer's presence topic */\n peerPresence: peerId ? `${base}/room/${roomId}/presence/${peerId}` : '',\n /** This peer's signal inbox */\n peerSignalInbox: peerId ? `${base}/room/${roomId}/signal/${peerId}` : '',\n };\n}\n\n/** Build Firebase RTDB paths for a given appId + roomId */\nexport function firebasePaths(appId: string, roomId: string, peerId?: string) {\n const base = `${appId}/__carver__`;\n return {\n lobby: `${base}/lobby`,\n roomLobbyEntry: `${base}/lobby/${roomId}`,\n peers: `${base}/rooms/${roomId}/peers`,\n peerPresence: peerId ? `${base}/rooms/${roomId}/peers/${peerId}` : '',\n peerSignalInbox: peerId ? `${base}/rooms/${roomId}/signals/${peerId}` : '',\n };\n}\n\n/** Default MQTT brokers (WebSocket endpoints, free/public) */\nexport const DEFAULT_MQTT_BROKERS = [\n 'wss://broker.emqx.io:8084/mqtt',\n 'wss://test.mosquitto.org:8081/mqtt',\n];\n\n/** Room announcement expiry time (30s without heartbeat = stale) */\nexport const ROOM_ANNOUNCE_EXPIRY_MS = 30_000;\n\n/** Room announcement heartbeat interval */\nexport const ROOM_ANNOUNCE_INTERVAL_MS = 10_000;\n\n/** Peer presence heartbeat interval */\nexport const PRESENCE_HEARTBEAT_MS = 5_000;\n\n/** Peer expiry: 3 missed heartbeats */\nexport const PEER_EXPIRY_MS = PRESENCE_HEARTBEAT_MS * 3;\n\n/** Rapid warmup announces for faster initial peer discovery */\nexport const PRESENCE_WARMUP_DELAYS_MS = [200, 500, 1500];\n\n/** Remove an item from an array by reference. Returns true if found. */\nexport function removeFromArray<T>(arr: T[], item: T): boolean {\n const idx = arr.indexOf(item);\n if (idx >= 0) {\n arr.splice(idx, 1);\n return true;\n }\n return false;\n}\n","import type {\n SignalingStrategy,\n PeerMetadata,\n RoomAnnouncement,\n MqttStrategyConfig,\n} from \"./types\";\nimport {\n generatePeerId,\n mqttTopics,\n removeFromArray,\n DEFAULT_MQTT_BROKERS,\n ROOM_ANNOUNCE_EXPIRY_MS,\n ROOM_ANNOUNCE_INTERVAL_MS,\n PRESENCE_HEARTBEAT_MS,\n PEER_EXPIRY_MS,\n PRESENCE_WARMUP_DELAYS_MS,\n} from \"./utils\";\n\n// mqtt.MqttClient type (avoid hard import at module level)\ntype MqttClient = {\n on(event: string, cb: (...args: any[]) => void): void;\n subscribe(topic: string | string[], opts: Record<string, unknown>, cb?: (err: Error | null) => void): void;\n unsubscribe(topic: string | string[]): void;\n publish(topic: string, payload: string | Buffer, opts?: Record<string, unknown>): void;\n end(force?: boolean): void;\n};\n\n/**\n * MQTT-based signaling strategy.\n *\n * Connects to public MQTT brokers over WebSocket. Peer discovery uses\n * retained presence messages with periodic heartbeats. SDP/ICE relay\n * uses per-peer signal topics. Room discovery uses retained lobby topics.\n *\n * Zero infrastructure cost -- uses free public brokers by default.\n */\nexport class MqttStrategy implements SignalingStrategy {\n readonly selfId: string;\n\n private _appId: string;\n private _config: MqttStrategyConfig;\n private _client: MqttClient | null = null;\n private _roomId: string | null = null;\n private _peerMeta: PeerMetadata = {};\n /** Monotonic counter to detect stale leaveRoom completions */\n private _joinGeneration = 0;\n\n // Lazy init\n private _initPromise: Promise<void> | null = null;\n\n // Callbacks\n private _onPeerDiscovered: ((peerId: string, meta: PeerMetadata) => void)[] = [];\n private _onPeerLeft: ((peerId: string) => void)[] = [];\n private _onSignal: ((fromPeerId: string, data: unknown) => void)[] = [];\n private _onLobby: ((rooms: RoomAnnouncement[]) => void)[] = [];\n\n // State\n private _knownPeers = new Map<string, { meta: PeerMetadata; lastSeen: number }>();\n private _lobbyRooms = new Map<string, RoomAnnouncement>();\n private _presenceTimer: ReturnType<typeof setInterval> | null = null;\n private _warmupTimers: ReturnType<typeof setTimeout>[] = [];\n private _lobbyAnnounceTimer: ReturnType<typeof setInterval> | null = null;\n private _peerExpiryTimer: ReturnType<typeof setInterval> | null = null;\n private _lobbySubscribed = false;\n private _destroyed = false;\n\n constructor(appId: string, config: MqttStrategyConfig = { type: 'mqtt' }) {\n this.selfId = generatePeerId();\n this._appId = appId;\n this._config = config;\n }\n\n // ── Public API ──\n\n async init(): Promise<void> {\n return this._ensureInit();\n }\n\n async joinRoom(roomId: string, peerMeta: PeerMetadata): Promise<void> {\n await this._ensureInit();\n if (!this._client) throw new Error('MQTT client not available');\n\n this._joinGeneration++;\n this._roomId = roomId;\n this._peerMeta = peerMeta;\n\n const topics = mqttTopics(this._appId, roomId, this.selfId);\n\n // Subscribe to room presence (discover peers) and own signal inbox\n await new Promise<void>((resolve, reject) => {\n this._client!.subscribe(\n [topics.roomPresenceWildcard, topics.peerSignalInbox],\n { qos: 1 },\n (err: Error | null) => (err ? reject(err) : resolve()),\n );\n });\n\n // Publish retained presence\n this._publishPresence();\n\n // Rapid warmup announces for fast peer discovery\n for (const delay of PRESENCE_WARMUP_DELAYS_MS) {\n this._warmupTimers.push(setTimeout(() => this._publishPresence(), delay));\n }\n\n // Periodic heartbeat\n this._presenceTimer = setInterval(() => this._publishPresence(), PRESENCE_HEARTBEAT_MS);\n\n // Periodic peer expiry check\n this._peerExpiryTimer = setInterval(() => this._checkPeerExpiry(), PRESENCE_HEARTBEAT_MS);\n }\n\n async leaveRoom(): Promise<void> {\n if (!this._client || !this._roomId) return;\n\n const generation = this._joinGeneration;\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._clearRoomTimers();\n\n // Clear retained presence\n this._client.publish(topics.peerPresence, '', { retain: true, qos: 1 });\n\n // Unsubscribe from room topics\n this._client.unsubscribe([topics.roomPresenceWildcard, topics.peerSignalInbox]);\n\n this._knownPeers.clear();\n\n // Only null out _roomId if no new joinRoom() has run since we started.\n if (this._joinGeneration === generation) {\n this._roomId = null;\n }\n }\n\n signal(targetPeerId: string, data: unknown): void {\n if (!this._client || !this._roomId) return;\n const targetTopic = `carver/${this._appId}/room/${this._roomId}/signal/${targetPeerId}`;\n this._client.publish(\n targetTopic,\n JSON.stringify({ from: this.selfId, data, ts: Date.now() }),\n { qos: 1 },\n );\n }\n\n subscribeToLobby(cb: (rooms: RoomAnnouncement[]) => void): () => void {\n this._onLobby.push(cb);\n\n // Subscribe to lobby topic (lazy -- waits for init)\n if (!this._lobbySubscribed) {\n this._lobbySubscribed = true;\n this._ensureInit().then(() => {\n if (this._client && !this._destroyed) {\n const lobbyTopic = mqttTopics(this._appId, '', '').lobbyWildcard;\n this._client.subscribe(lobbyTopic, { qos: 0 });\n }\n });\n }\n\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n\n private _lastAnnouncement: RoomAnnouncement | null = null;\n\n updateRoomOccupancy(roomId: string, playerCount: number, state?: 'lobby' | 'playing' | 'ended'): void {\n const ann = this._lastAnnouncement;\n if (!ann || ann.roomId !== roomId || !this._client) return;\n ann.playerCount = playerCount;\n if (state) ann.state = state;\n ann.lastSeen = Date.now();\n const topic = mqttTopics(this._appId, roomId, '').roomLobbyEntry;\n this._client.publish(topic, JSON.stringify(ann), { retain: true, qos: 1 });\n }\n\n announceRoom(announcement: RoomAnnouncement): void {\n if (!this._client) return;\n const topic = mqttTopics(this._appId, announcement.roomId, '').roomLobbyEntry;\n\n this._lastAnnouncement = announcement;\n announcement.lastSeen = Date.now();\n this._client.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });\n\n // Periodic heartbeat\n if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = setInterval(() => {\n announcement.lastSeen = Date.now();\n this._client?.publish(topic, JSON.stringify(announcement), { retain: true, qos: 1 });\n }, ROOM_ANNOUNCE_INTERVAL_MS);\n }\n\n removeRoomAnnouncement(roomId: string): void {\n if (!this._client) return;\n const topic = mqttTopics(this._appId, roomId, '').roomLobbyEntry;\n this._client.publish(topic, '', { retain: true, qos: 1 });\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n }\n\n onPeerDiscovered(cb: (peerId: string, meta: PeerMetadata) => void): () => void {\n this._onPeerDiscovered.push(cb);\n return () => { removeFromArray(this._onPeerDiscovered, cb); };\n }\n\n onPeerLeft(cb: (peerId: string) => void): () => void {\n this._onPeerLeft.push(cb);\n return () => { removeFromArray(this._onPeerLeft, cb); };\n }\n\n onSignal(cb: (fromPeerId: string, data: unknown) => void): () => void {\n this._onSignal.push(cb);\n return () => { removeFromArray(this._onSignal, cb); };\n }\n\n destroy(): void {\n this._destroyed = true;\n this._clearRoomTimers();\n\n // Clean up retained presence\n if (this._client && this._roomId) {\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._client.publish(topics.peerPresence, '', { retain: true, qos: 1 });\n }\n\n this._client?.end(true);\n this._client = null;\n this._knownPeers.clear();\n this._lobbyRooms.clear();\n this._onPeerDiscovered = [];\n this._onPeerLeft = [];\n this._onSignal = [];\n this._onLobby = [];\n }\n\n // ── Private ──\n\n private _ensureInit(): Promise<void> {\n if (!this._initPromise) {\n this._initPromise = this._doInit();\n }\n return this._initPromise;\n }\n\n private async _doInit(): Promise<void> {\n const mqtt = await import('mqtt');\n const brokers = this._config.brokerUrls ?? DEFAULT_MQTT_BROKERS;\n // Pick a random broker from the available pool\n const brokerUrl = brokers[Math.floor(Math.random() * brokers.length)];\n\n return new Promise<void>((resolve, reject) => {\n const connectFn = mqtt.default?.connect ?? mqtt.connect;\n this._client = connectFn(brokerUrl, {\n clientId: `carver_${this.selfId}`,\n clean: true,\n connectTimeout: 10_000,\n keepalive: 30,\n }) as MqttClient;\n\n this._client.on('connect', () => {\n if (!this._destroyed) resolve();\n });\n\n this._client.on('error', (err: Error) => {\n if (!this._initPromise) return; // already resolved\n reject(err);\n });\n\n this._client.on('message', (topic: string, payload: Buffer) => {\n this._handleMessage(topic, payload);\n });\n });\n }\n\n private _publishPresence(): void {\n if (!this._client || !this._roomId) return;\n const topics = mqttTopics(this._appId, this._roomId, this.selfId);\n this._client.publish(\n topics.peerPresence,\n JSON.stringify({ peerId: this.selfId, meta: this._peerMeta, ts: Date.now() }),\n { retain: true, qos: 1 },\n );\n }\n\n private _handleMessage(topic: string, payload: Buffer): void {\n const raw = payload.toString();\n\n // ── Presence message ──\n const presenceMatch = topic.match(/\\/room\\/[^/]+\\/presence\\/([^/]+)$/);\n if (presenceMatch) {\n const peerId = presenceMatch[1];\n if (peerId === this.selfId) return;\n\n if (!raw) {\n // Empty retained = peer left\n if (this._knownPeers.has(peerId)) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n return;\n }\n try {\n const msg = JSON.parse(raw);\n const isNew = !this._knownPeers.has(peerId);\n this._knownPeers.set(peerId, { meta: msg.meta ?? {}, lastSeen: msg.ts ?? Date.now() });\n if (isNew) {\n for (const cb of this._onPeerDiscovered) cb(peerId, msg.meta ?? {});\n }\n } catch { /* ignore malformed */ }\n return;\n }\n\n // ── Signal message (SDP / ICE) ──\n const signalMatch = topic.match(/\\/room\\/[^/]+\\/signal\\/([^/]+)$/);\n if (signalMatch) {\n try {\n const msg = JSON.parse(raw);\n if (msg.from && msg.from !== this.selfId) {\n for (const cb of this._onSignal) cb(msg.from, msg.data);\n }\n } catch { /* ignore malformed */ }\n return;\n }\n\n // ── Lobby announcement ──\n const lobbyMatch = topic.match(/\\/lobby\\/([^/]+)$/);\n if (lobbyMatch) {\n const roomId = lobbyMatch[1];\n if (!raw) {\n this._lobbyRooms.delete(roomId);\n } else {\n try {\n const ann = JSON.parse(raw) as RoomAnnouncement;\n if (Date.now() - ann.lastSeen < ROOM_ANNOUNCE_EXPIRY_MS) {\n this._lobbyRooms.set(roomId, ann);\n } else {\n this._lobbyRooms.delete(roomId);\n }\n } catch { /* ignore */ }\n }\n const rooms = Array.from(this._lobbyRooms.values());\n for (const cb of this._onLobby) cb(rooms);\n }\n }\n\n private _checkPeerExpiry(): void {\n const now = Date.now();\n for (const [peerId, data] of this._knownPeers) {\n if (now - data.lastSeen > PEER_EXPIRY_MS) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n }\n }\n\n private _clearRoomTimers(): void {\n if (this._presenceTimer) { clearInterval(this._presenceTimer); this._presenceTimer = null; }\n for (const t of this._warmupTimers) clearTimeout(t);\n this._warmupTimers = [];\n if (this._lobbyAnnounceTimer) { clearInterval(this._lobbyAnnounceTimer); this._lobbyAnnounceTimer = null; }\n if (this._peerExpiryTimer) { clearInterval(this._peerExpiryTimer); this._peerExpiryTimer = null; }\n }\n}\n","import type {\n SignalingStrategy,\n PeerMetadata,\n RoomAnnouncement,\n FirebaseStrategyConfig,\n} from \"./types\";\nimport {\n generatePeerId,\n firebasePaths,\n removeFromArray,\n ROOM_ANNOUNCE_EXPIRY_MS,\n ROOM_ANNOUNCE_INTERVAL_MS,\n} from \"./utils\";\n\n/**\n * Firebase Realtime Database signaling strategy.\n *\n * Requires the `firebase` package as a peer dependency.\n * Pass either a `databaseURL` (auto-creates a namespaced Firebase app)\n * or an existing `firebaseApp` instance.\n *\n * Presence cleanup is automatic via Firebase onDisconnect().\n */\nexport class FirebaseStrategy implements SignalingStrategy {\n readonly selfId: string;\n\n private _appId: string;\n private _config: FirebaseStrategyConfig;\n private _db: any = null;\n private _firebaseApp: any = null;\n private _ownApp = false;\n private _roomId: string | null = null;\n private _peerMeta: PeerMetadata = {};\n /** Monotonic counter to detect stale leaveRoom completions */\n private _joinGeneration = 0;\n\n // Lazy init\n private _initPromise: Promise<void> | null = null;\n\n // Firebase module references (filled after dynamic import)\n private _fb: {\n ref: any;\n set: any;\n push: any;\n remove: any;\n onValue: any;\n onChildAdded: any;\n onChildRemoved: any;\n onDisconnect: any;\n } | null = null;\n\n // Unsubscribe handles for Firebase listeners\n private _listeners: (() => void)[] = [];\n\n // Callbacks\n private _onPeerDiscovered: ((peerId: string, meta: PeerMetadata) => void)[] = [];\n private _onPeerLeft: ((peerId: string) => void)[] = [];\n private _onSignal: ((fromPeerId: string, data: unknown) => void)[] = [];\n private _onLobby: ((rooms: RoomAnnouncement[]) => void)[] = [];\n\n // State\n private _knownPeers = new Set<string>();\n private _lobbyAnnounceTimer: ReturnType<typeof setInterval> | null = null;\n private _lastAnnouncement: RoomAnnouncement | null = null;\n private _lobbyWired = false;\n private _destroyed = false;\n\n constructor(appId: string, config: FirebaseStrategyConfig) {\n this.selfId = generatePeerId();\n this._appId = appId;\n this._config = config;\n }\n\n // ── Public API ──\n\n async init(): Promise<void> {\n return this._ensureInit();\n }\n\n async joinRoom(roomId: string, peerMeta: PeerMetadata): Promise<void> {\n await this._ensureInit();\n if (!this._db || !this._fb) throw new Error('Firebase not initialized');\n\n // Bump generation so any in-flight leaveRoom from a prior call won't\n // null out _roomId after we set it here (React StrictMode race fix).\n this._joinGeneration++;\n\n this._roomId = roomId;\n this._peerMeta = peerMeta;\n const { ref, set, onChildAdded, onChildRemoved, onDisconnect, onValue, remove } = this._fb;\n const paths = firebasePaths(this._appId, roomId, this.selfId);\n\n // Clean stale signals from our inbox before listening (prevents\n // replaying SDP from a previous session that wasn't cleaned up).\n await remove(ref(this._db, paths.peerSignalInbox)).catch(() => {});\n\n // 1. Write presence with auto-cleanup on disconnect\n const presenceRef = ref(this._db, paths.peerPresence);\n await set(presenceRef, {\n peerId: this.selfId,\n meta: peerMeta,\n ts: Date.now(),\n });\n onDisconnect(presenceRef).remove();\n\n // 1b. Re-establish presence on EVERY reconnection to the RTDB. Firebase\n // onDisconnect handlers fire only once and remove our presence node; the\n // SDK does NOT re-create the value when it reconnects. Without this, a\n // transient RTDB disconnect (tab backgrounding, network handoff, server\n // recycling) permanently deletes our presence even though the P2P WebRTC\n // links stay healthy — every other peer then sees onChildRemoved, tears\n // down our connection, and the room fragments into isolated single-player\n // sessions. Canonical presence pattern: re-arm onDisconnect BEFORE\n // re-writing the value, on every transition to connected.\n const generation = this._joinGeneration;\n const connectedRef = ref(this._db, '.info/connected');\n const connectedUnsub = onValue(connectedRef, (snap: any) => {\n if (snap.val() !== true) return;\n if (this._destroyed || this._joinGeneration !== generation || this._roomId !== roomId) return;\n onDisconnect(presenceRef).remove()\n .then(() => {\n if (this._destroyed || this._joinGeneration !== generation || this._roomId !== roomId) return;\n return set(presenceRef, { peerId: this.selfId, meta: peerMeta, ts: Date.now() });\n })\n .catch(() => {});\n });\n this._listeners.push(() => connectedUnsub());\n\n // 2. Listen for peers joining\n const peersRef = ref(this._db, paths.peers);\n const addedUnsub = onChildAdded(peersRef, (snapshot: any) => {\n const data = snapshot.val();\n if (!data || data.peerId === this.selfId) return;\n if (!this._knownPeers.has(data.peerId)) {\n this._knownPeers.add(data.peerId);\n for (const cb of this._onPeerDiscovered) cb(data.peerId, data.meta ?? {});\n }\n });\n this._listeners.push(() => addedUnsub());\n\n // 3. Listen for peers leaving\n const removedUnsub = onChildRemoved(peersRef, (snapshot: any) => {\n const data = snapshot.val();\n const peerId = data?.peerId ?? snapshot.key;\n if (peerId && this._knownPeers.has(peerId)) {\n this._knownPeers.delete(peerId);\n for (const cb of this._onPeerLeft) cb(peerId);\n }\n });\n this._listeners.push(() => removedUnsub());\n\n // 4. Listen for signals addressed to us\n const signalRef = ref(this._db, paths.peerSignalInbox);\n const signalUnsub = onChildAdded(signalRef, (snapshot: any) => {\n const msg = snapshot.val();\n if (!msg || msg.from === this.selfId) return;\n for (const cb of this._onSignal) cb(msg.from, msg.data);\n // Remove processed signal to keep the inbox clean\n remove(snapshot.ref);\n });\n this._listeners.push(() => signalUnsub());\n }\n\n async leaveRoom(): Promise<void> {\n if (!this._db || !this._fb || !this._roomId) return;\n\n // Capture current state so async cleanup targets the correct room\n // even if joinRoom() is called concurrently (React StrictMode).\n const leavingRoomId = this._roomId;\n const generation = this._joinGeneration;\n\n const { ref, remove } = this._fb;\n const paths = firebasePaths(this._appId, leavingRoomId, this.selfId);\n\n // Detach listeners\n for (const unsub of this._listeners) unsub();\n this._listeners = [];\n\n // Remove presence and signal inbox\n await Promise.all([\n remove(ref(this._db, paths.peerPresence)),\n remove(ref(this._db, paths.peerSignalInbox)),\n ]).catch(() => {});\n\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n\n this._knownPeers.clear();\n\n // Only null out _roomId if no new joinRoom() has run since we started.\n // This prevents the StrictMode race: old leaveRoom completing after\n // new joinRoom already set _roomId to the fresh value.\n if (this._joinGeneration === generation) {\n this._roomId = null;\n }\n }\n\n signal(targetPeerId: string, data: unknown): void {\n if (!this._db || !this._fb || !this._roomId) return;\n const { ref, push } = this._fb;\n\n // Atomic push: single operation writes the key + data together.\n // Avoids the push() + set() two-step that can cause onChildAdded to\n // fire with null if the listener catches the intermediate state.\n const inboxPath = firebasePaths(this._appId, this._roomId, targetPeerId).peerSignalInbox;\n push(ref(this._db, inboxPath), {\n from: this.selfId,\n data: sanitizeForFirebase(data),\n ts: Date.now(),\n });\n }\n\n subscribeToLobby(cb: (rooms: RoomAnnouncement[]) => void): () => void {\n this._onLobby.push(cb);\n\n // Wire the underlying RTDB listener exactly once — repeated subscribe/\n // unsubscribe cycles (React StrictMode, route changes) must not stack\n // duplicate onValue listeners.\n if (this._lobbyWired) {\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n this._lobbyWired = true;\n\n this._ensureInit().then(() => {\n if (!this._db || !this._fb || this._destroyed) return;\n const { ref, onValue } = this._fb;\n const paths = firebasePaths(this._appId, '', '');\n const lobbyRef = ref(this._db, paths.lobby);\n\n const unsub = onValue(lobbyRef, (snapshot: any) => {\n const data = snapshot.val();\n if (!data) {\n for (const lcb of this._onLobby) lcb([]);\n return;\n }\n const now = Date.now();\n const rooms: RoomAnnouncement[] = Object.values(data).filter(\n (r: any) => r && now - (r.lastSeen ?? 0) < ROOM_ANNOUNCE_EXPIRY_MS,\n ) as RoomAnnouncement[];\n for (const lcb of this._onLobby) lcb(rooms);\n });\n this._listeners.push(() => unsub());\n });\n\n return () => {\n removeFromArray(this._onLobby, cb);\n };\n }\n\n announceRoom(announcement: RoomAnnouncement): void {\n if (!this._db || !this._fb) return;\n const { ref, set } = this._fb;\n const paths = firebasePaths(this._appId, announcement.roomId, '');\n\n this._lastAnnouncement = announcement;\n announcement.lastSeen = Date.now();\n set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(announcement));\n\n // Periodic heartbeat\n if (this._lobbyAnnounceTimer) clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = setInterval(() => {\n announcement.lastSeen = Date.now();\n set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(announcement));\n }, ROOM_ANNOUNCE_INTERVAL_MS);\n }\n\n updateRoomOccupancy(roomId: string, playerCount: number, state?: 'lobby' | 'playing' | 'ended'): void {\n const ann = this._lastAnnouncement;\n if (!ann || ann.roomId !== roomId || !this._db || !this._fb) return;\n ann.playerCount = playerCount;\n if (state) ann.state = state;\n ann.lastSeen = Date.now();\n const { ref, set } = this._fb;\n const paths = firebasePaths(this._appId, roomId, '');\n set(ref(this._db, paths.roomLobbyEntry), sanitizeForFirebase(ann));\n }\n\n removeRoomAnnouncement(roomId: string): void {\n if (!this._db || !this._fb) return;\n const { ref, remove } = this._fb;\n remove(ref(this._db, firebasePaths(this._appId, roomId, '').roomLobbyEntry));\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n }\n\n onPeerDiscovered(cb: (peerId: string, meta: PeerMetadata) => void): () => void {\n this._onPeerDiscovered.push(cb);\n return () => { removeFromArray(this._onPeerDiscovered, cb); };\n }\n\n onPeerLeft(cb: (peerId: string) => void): () => void {\n this._onPeerLeft.push(cb);\n return () => { removeFromArray(this._onPeerLeft, cb); };\n }\n\n onSignal(cb: (fromPeerId: string, data: unknown) => void): () => void {\n this._onSignal.push(cb);\n return () => { removeFromArray(this._onSignal, cb); };\n }\n\n destroy(): void {\n this._destroyed = true;\n for (const unsub of this._listeners) unsub();\n this._listeners = [];\n\n if (this._lobbyAnnounceTimer) {\n clearInterval(this._lobbyAnnounceTimer);\n this._lobbyAnnounceTimer = null;\n }\n\n // Best-effort cleanup\n if (this._db && this._fb && this._roomId) {\n const { ref, remove } = this._fb;\n const paths = firebasePaths(this._appId, this._roomId, this.selfId);\n remove(ref(this._db, paths.peerPresence)).catch(() => {});\n remove(ref(this._db, paths.peerSignalInbox)).catch(() => {});\n }\n\n // Delete own Firebase app if we created it\n if (this._ownApp && this._firebaseApp) {\n import('firebase/app').then(({ deleteApp }) => {\n deleteApp(this._firebaseApp).catch(() => {});\n });\n }\n\n this._db = null;\n this._firebaseApp = null;\n this._fb = null;\n this._knownPeers.clear();\n this._onPeerDiscovered = [];\n this._onPeerLeft = [];\n this._onSignal = [];\n this._onLobby = [];\n }\n\n // ── Private ──\n\n private _ensureInit(): Promise<void> {\n if (!this._initPromise) {\n this._initPromise = this._doInit();\n }\n return this._initPromise;\n }\n\n private async _doInit(): Promise<void> {\n const { initializeApp, getApps } = await import('firebase/app');\n const {\n getDatabase,\n ref,\n set,\n push,\n remove,\n onValue,\n onChildAdded,\n onChildRemoved,\n onDisconnect,\n } = await import('firebase/database');\n\n this._fb = { ref, set, push, remove, onValue, onChildAdded, onChildRemoved, onDisconnect };\n\n if (this._config.firebaseApp) {\n this._firebaseApp = this._config.firebaseApp;\n this._ownApp = false;\n } else {\n const appName = `carver_${this._appId}`;\n const existing = getApps().find((a: any) => a.name === appName);\n if (existing) {\n this._firebaseApp = existing;\n this._ownApp = false;\n } else {\n this._firebaseApp = initializeApp({ databaseURL: this._config.databaseURL }, appName);\n this._ownApp = true;\n }\n }\n\n this._db = getDatabase(this._firebaseApp);\n }\n}\n\n/**\n * Firebase RTDB deletes any key whose value is `null` (treats null as \"remove\").\n * ICE candidates from `toJSON()` can contain `null` fields (e.g. usernameFragment).\n * We recursively replace `null` with a sentinel so Firebase preserves the key.\n * On the receiving end, `_handleSignal` doesn't need to reverse this because\n * `new RTCIceCandidate()` / `new RTCSessionDescription()` accept missing fields.\n */\nfunction sanitizeForFirebase(obj: unknown): unknown {\n if (obj === null) return '__null__';\n if (Array.isArray(obj)) return obj.map(sanitizeForFirebase);\n if (typeof obj === 'object' && obj !== null) {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (value === undefined) continue; // RTDB rejects undefined anywhere in the payload\n result[key] = value === null ? '__null__' : sanitizeForFirebase(value);\n }\n return result;\n }\n return obj;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/webrtc/ice.ts","../src/transport/webrtc/peer.ts","../src/transport/webrtc/WebRTCTransport.ts"],"sourcesContent":["/** Default STUN servers (free, public) */\nconst DEFAULT_STUN_SERVERS: RTCIceServer[] = [\n { urls: 'stun:stun.l.google.com:19302' },\n { urls: 'stun:stun1.l.google.com:19302' },\n { urls: 'stun:stun2.l.google.com:19302' },\n { urls: 'stun:stun.cloudflare.com:3478' },\n];\n\n/**\n * Build RTCConfiguration from user-provided ICE servers.\n *\n * If the user provides `iceServers`, those are used as-is (STUN + TURN).\n * Otherwise, default public STUN servers are used.\n *\n * TURN servers should be included in the `iceServers` array by the user:\n * ```ts\n * iceServers: [\n * { urls: 'stun:stun.cloudflare.com:3478' },\n * { urls: 'turn:turn.cloudflare.com:3478', username: '...', credential: '...' },\n * ]\n * ```\n */\nexport function buildICEConfig(options?: {\n iceServers?: RTCIceServer[];\n iceTransportPolicy?: RTCIceTransportPolicy;\n}): RTCConfiguration {\n const servers: RTCIceServer[] =\n options?.iceServers && options.iceServers.length > 0\n ? options.iceServers\n : DEFAULT_STUN_SERVERS;\n\n return {\n iceServers: servers,\n iceCandidatePoolSize: 10,\n iceTransportPolicy: options?.iceTransportPolicy ?? 'all',\n };\n}\n","import type { ChannelOptions } from \"../../types\";\nimport type { PeerState } from \"../types\";\n\nexport interface PeerConnectionEvents {\n onStateChange: (state: PeerState) => void;\n onDataChannel: (channel: RTCDataChannel) => void;\n onIceCandidate: (candidate: RTCIceCandidate) => void;\n}\n\n/**\n * Manages a single RTCPeerConnection to one remote peer.\n *\n * ICE candidates that arrive before the remote description is set are\n * buffered and flushed automatically once setRemoteDescription completes.\n * This is critical for Firebase/MQTT signaling where offer, answer, and\n * candidates can arrive nearly simultaneously.\n */\nexport class PeerConnection {\n readonly peerId: string;\n private _connection: RTCPeerConnection;\n private _channels = new Map<string, RTCDataChannel>();\n private _events: PeerConnectionEvents;\n private _state: PeerState = 'connecting';\n private _remoteDescriptionSet = false;\n private _pendingCandidates: RTCIceCandidateInit[] = [];\n\n constructor(\n peerId: string,\n config: RTCConfiguration,\n events: PeerConnectionEvents,\n ) {\n this.peerId = peerId;\n this._events = events;\n this._connection = new RTCPeerConnection(config);\n\n this._connection.onicecandidate = (e) => {\n if (e.candidate) {\n this._events.onIceCandidate(e.candidate);\n }\n };\n\n this._connection.oniceconnectionstatechange = () => {\n this._updateState();\n };\n\n this._connection.onconnectionstatechange = () => {\n this._updateState();\n };\n\n this._connection.ondatachannel = (e) => {\n const channel = e.channel;\n this._channels.set(channel.label, channel);\n this._events.onDataChannel(channel);\n };\n }\n\n get state(): PeerState {\n return this._state;\n }\n\n get connection(): RTCPeerConnection {\n return this._connection;\n }\n\n private _updateState(): void {\n const iceState = this._connection.iceConnectionState;\n const connState = this._connection.connectionState;\n\n let newState: PeerState;\n if (connState === 'connected' || iceState === 'connected') {\n newState = 'connected';\n } else if (connState === 'failed' || iceState === 'failed') {\n newState = 'failed';\n } else if (connState === 'closed' || iceState === 'closed' || iceState === 'disconnected') {\n newState = 'disconnected';\n } else {\n newState = 'connecting';\n }\n\n if (newState !== this._state) {\n this._state = newState;\n this._events.onStateChange(newState);\n }\n }\n\n async createOffer(options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {\n // `options` carries `{ iceRestart: true }` when renegotiating a dropped\n // connection so a fresh ICE ufrag/pwd is generated (ICE restart).\n const offer = await this._connection.createOffer(options);\n await this._connection.setLocalDescription(offer);\n return offer;\n }\n\n async handleOffer(offer: RTCSessionDescriptionInit): Promise<RTCSessionDescriptionInit> {\n await this._connection.setRemoteDescription(new RTCSessionDescription(offer));\n this._remoteDescriptionSet = true;\n await this._flushPendingCandidates();\n const answer = await this._connection.createAnswer();\n await this._connection.setLocalDescription(answer);\n return answer;\n }\n\n async handleAnswer(answer: RTCSessionDescriptionInit): Promise<void> {\n await this._connection.setRemoteDescription(new RTCSessionDescription(answer));\n this._remoteDescriptionSet = true;\n await this._flushPendingCandidates();\n }\n\n async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {\n if (!this._remoteDescriptionSet) {\n // Buffer until remote description is set -- prevents silent drops\n this._pendingCandidates.push(candidate);\n return;\n }\n try {\n await this._connection.addIceCandidate(new RTCIceCandidate(candidate));\n } catch {\n // Ignore ICE candidate errors (can happen during race conditions)\n }\n }\n\n private async _flushPendingCandidates(): Promise<void> {\n const candidates = this._pendingCandidates;\n this._pendingCandidates = [];\n for (const c of candidates) {\n try {\n await this._connection.addIceCandidate(new RTCIceCandidate(c));\n } catch {\n // Ignore errors on individual candidates\n }\n }\n }\n\n createDataChannel(name: string, options?: ChannelOptions): RTCDataChannel {\n // If a channel with this label already exists (e.g. received via\n // ondatachannel from the remote peer), reuse it instead of creating\n // a duplicate that fragments send/receive across two channels.\n const existing = this._channels.get(name);\n if (existing && existing.readyState !== 'closed') {\n return existing;\n }\n\n const dcOptions: RTCDataChannelInit = {};\n if (options?.reliable === false) {\n dcOptions.ordered = options?.ordered ?? false;\n dcOptions.maxRetransmits = options?.maxRetransmits ?? 0;\n } else {\n dcOptions.ordered = options?.ordered ?? true;\n }\n const channel = this._connection.createDataChannel(name, dcOptions);\n this._channels.set(name, channel);\n return channel;\n }\n\n getDataChannel(name: string): RTCDataChannel | undefined {\n return this._channels.get(name);\n }\n\n close(): void {\n for (const channel of this._channels.values()) {\n try { channel.close(); } catch { /* ignore */ }\n }\n this._channels.clear();\n this._pendingCandidates = [];\n this._remoteDescriptionSet = false;\n try { this._connection.close(); } catch { /* ignore */ }\n this._state = 'disconnected';\n }\n}\n","import type {\n CarverTransport,\n CarverChannel,\n ChannelOptions,\n TransportConfig,\n Player,\n Room,\n RoomState,\n} from \"../../types\";\nimport type { TransportCallbacks, RateLimitConfig } from \"../types\";\nimport type { SignalingStrategy, PeerMetadata } from \"../strategy/types\";\nimport { buildICEConfig } from \"./ice\";\nimport { PeerConnection } from \"./peer\";\n\nconst ROOM_CONTROL_CHANNEL = 'carver:room-control';\n\ninterface ChannelState<T> {\n name: string;\n options: ChannelOptions;\n receivers: ((data: T, peerId: string) => void)[];\n}\n\n/** Room control messages exchanged over the room-control data channel */\ntype RoomControlMessage =\n | { type: 'player-updated'; player: Player }\n | { type: 'room-updated'; room: Partial<Room> }\n | { type: 'kick'; peerId: string; reason?: string }\n | { type: 'host-changed'; newHostId: string }\n | { type: 'request-ready'; ready: boolean }\n | { type: 'request-metadata'; metadata: Record<string, unknown> }\n | { type: 'request-room-metadata'; metadata: Record<string, unknown> }\n | { type: 'request-room-state'; state: RoomState }\n | { type: 'request-max-players'; maxPlayers: number }\n | { type: 'request-lock' }\n | { type: 'request-unlock' }\n | { type: 'request-transfer-host'; peerId: string }\n | { type: 'sync-state'; room: Room; players: Player[] };\n\n/**\n * Deterministic, globally-consistent host election.\n *\n * Lowest hostPriority wins; ties are broken by lowest peerId. A peer advertises\n * its priority via player metadata (`metadata.hostPriority`, lower = preferred)\n * so the room creator / world owner can pin itself as host regardless of its\n * random peerId. With no priorities advertised this reduces exactly to the\n * previous \"lowest peerId\" election (backwards compatible).\n */\nfunction electHost(peerIds: string[], rankOf: (peerId: string) => number): string {\n let best = peerIds[0];\n let bestRank = rankOf(best);\n for (const id of peerIds) {\n const rank = rankOf(id);\n if (rank < bestRank || (rank === bestRank && id < best)) {\n best = id;\n bestRank = rank;\n }\n }\n return best;\n}\n\n/**\n * Implements CarverTransport using WebRTC data channels for game data\n * and a pluggable SignalingStrategy for peer discovery + SDP/ICE relay.\n *\n * No WebSocket server required. The strategy handles signaling through\n * MQTT brokers, Firebase RTDB, or any other network.\n */\nexport class WebRTCTransport implements CarverTransport {\n private _strategy: SignalingStrategy;\n private _peers = new Map<string, PeerConnection>();\n private _peerSet = new Set<string>();\n private _peerId: string;\n private _hostId = '';\n private _isHost = false;\n private _callbacks: TransportCallbacks = {\n onPeerJoin: [],\n onPeerLeave: [],\n onPeerUpdated: [],\n onHostChanged: [],\n };\n private _roomUpdatedCallbacks: ((room: Room) => void)[] = [];\n private _channels = new Map<string, ChannelState<any>>();\n private _iceConfig: RTCConfiguration;\n private _rateLimitConfig: RateLimitConfig = { maxMessagesPerSecond: 60, windowMs: 1000 };\n private _rateLimitCounters = new Map<string, { count: number; resetAt: number }>();\n private _connected = false;\n private _room: Room | null = null;\n private _playerMap = new Map<string, Player>();\n private _initialPeers: Player[] = [];\n private _strategyUnsubs: (() => void)[] = [];\n\n /**\n * @param strategy Shared SignalingStrategy instance (managed by MultiplayerProvider)\n * @param iceServers Optional ICE servers (STUN + TURN). Defaults to public STUN.\n * @param iceTransportPolicy 'all' (default) or 'relay' (force TURN only).\n */\n constructor(\n strategy: SignalingStrategy,\n iceServers?: RTCIceServer[],\n iceTransportPolicy?: RTCIceTransportPolicy,\n ) {\n this._strategy = strategy;\n this._peerId = strategy.selfId;\n this._iceConfig = buildICEConfig({ iceServers, iceTransportPolicy });\n }\n\n // ── CarverTransport getters ──\n\n get peerId(): string { return this._peerId; }\n get peers(): ReadonlySet<string> { return this._peerSet; }\n get hostId(): string { return this._hostId; }\n get isHost(): boolean { return this._isHost; }\n get room(): Room | undefined { return this._room ?? undefined; }\n get initialPlayers(): Player[] { return this._initialPeers; }\n\n // ── Event registration ──\n\n onPeerJoin(cb: (peerId: string) => void): void { this._callbacks.onPeerJoin.push(cb); }\n onPeerLeave(cb: (peerId: string) => void): void { this._callbacks.onPeerLeave.push(cb); }\n onPeerUpdated(cb: (player: Player) => void): void { this._callbacks.onPeerUpdated.push(cb); }\n onRoomUpdated(cb: (room: Room) => void): void { this._roomUpdatedCallbacks.push(cb); }\n onHostChanged(cb: (newHostId: string) => void): void { this._callbacks.onHostChanged.push(cb); }\n\n // ── Channel management ──\n\n createChannel<T>(name: string, options?: ChannelOptions): CarverChannel<T> {\n // Idempotent: return existing channel if already created\n const existing = this._channels.get(name);\n if (existing) {\n return {\n send: (data: T, target?: string | string[]) => this._sendOnChannel(name, data, target),\n onReceive: (cb: (data: T, peerId: string) => void) => { existing.receivers.push(cb); },\n close: () => { this._channels.delete(name); },\n };\n }\n\n const state: ChannelState<T> = {\n name,\n options: options ?? { reliable: true, ordered: true },\n receivers: [],\n };\n this._channels.set(name, state);\n\n // Create data channels on existing peers if already connected\n if (this._connected) {\n for (const peer of this._peers.values()) {\n this._createDataChannelOnPeer(peer, name, state.options);\n }\n }\n\n return {\n send: (data: T, target?: string | string[]) => this._sendOnChannel(name, data, target),\n onReceive: (cb: (data: T, peerId: string) => void) => { state.receivers.push(cb); },\n close: () => { this._channels.delete(name); },\n };\n }\n\n // ── Connect / Disconnect ──\n\n async connect(roomId: string, config?: TransportConfig): Promise<void> {\n // Override ICE config if user passed custom servers\n if (config?.iceServers) {\n this._iceConfig = buildICEConfig({\n iceServers: config.iceServers,\n iceTransportPolicy: config.iceTransportPolicy,\n });\n }\n\n // Pre-register ALL standard channels so the initiator includes them\n // in the initial WebRTC offer. Channels created after the peer connection\n // is established won't get a proper data channel on the remote side.\n this._setupRoomControlChannel();\n this._preRegisterChannel('carver:events', { reliable: true, ordered: true });\n this._preRegisterChannel('carver:snapshots', { reliable: false, ordered: false });\n this._preRegisterChannel('carver:acks', { reliable: true, ordered: true });\n this._preRegisterChannel('carver:inputs', { reliable: true, ordered: true });\n this._preRegisterChannel('carver:network-state', { reliable: true, ordered: true });\n\n // Bind strategy callbacks (store unsubs for cleanup)\n this._strategyUnsubs.push(\n this._strategy.onPeerDiscovered((peerId, meta) => {\n this._onStrategyPeerDiscovered(peerId, meta);\n }),\n );\n this._strategyUnsubs.push(\n this._strategy.onPeerLeft((peerId) => {\n this._onStrategyPeerLeft(peerId);\n }),\n );\n this._strategyUnsubs.push(\n this._strategy.onSignal((fromPeerId, data) => {\n this._handleSignal(fromPeerId, data);\n }),\n );\n\n // Join room via strategy (publishes presence, subscribes to room)\n await this._strategy.joinRoom(roomId, {\n displayName: config?.displayName,\n ...(config?.playerMetadata ?? {}),\n });\n\n // Create self Player\n const selfPlayer: Player = {\n peerId: this._peerId,\n displayName: config?.displayName ?? `Player-${this._peerId.slice(0, 4)}`,\n isHost: false,\n isSelf: true,\n isReady: false,\n isConnected: true,\n metadata: config?.playerMetadata ?? {},\n latencyMs: 0,\n joinedAt: Date.now(),\n };\n this._playerMap.set(this._peerId, selfPlayer);\n\n // Elect host (may just be us if we're the first in the room)\n this._electAndSetHost();\n\n // Create initial Room object\n this._room = {\n id: roomId,\n name: roomId,\n hostId: this._hostId,\n playerCount: this._playerMap.size,\n maxPlayers: config?.maxPlayers ?? 8,\n isPrivate: false,\n metadata: {},\n createdAt: Date.now(),\n state: 'lobby',\n };\n\n this._initialPeers = Array.from(this._playerMap.values());\n this._connected = true;\n }\n\n disconnect(): void {\n this._connected = false;\n\n // Unsubscribe from strategy callbacks\n for (const unsub of this._strategyUnsubs) unsub();\n this._strategyUnsubs = [];\n\n // Close all peer connections\n for (const peer of this._peers.values()) peer.close();\n this._peers.clear();\n this._peerSet.clear();\n this._channels.clear();\n this._rateLimitCounters.clear();\n this._playerMap.clear();\n\n // Leave room via strategy (don't destroy -- provider manages lifecycle)\n this._strategy.leaveRoom().catch(() => {});\n\n this._hostId = '';\n this._isHost = false;\n this._room = null;\n }\n\n /** Expose strategy for lobby hooks */\n get strategy(): SignalingStrategy { return this._strategy; }\n\n // ── Channel pre-registration ──\n\n /**\n * Register a channel name and options without creating data channels yet.\n * When _connectToPeer runs, it iterates this._channels and creates data\n * channels for every registered name in the initial WebRTC offer.\n * Later, when EventSync/SnapshotSync call createChannel(), the idempotent\n * check returns the pre-registered entry and they just attach receivers.\n */\n private _preRegisterChannel(name: string, options: ChannelOptions): void {\n if (this._channels.has(name)) return;\n this._channels.set(name, { name, options, receivers: [] });\n }\n\n // ── Room management (over WebRTC data channels) ──\n\n setReady(ready: boolean): void {\n this._sendControlMessage({ type: 'request-ready', ready });\n }\n\n setMetadata(metadata: Record<string, unknown>): void {\n this._sendControlMessage({ type: 'request-metadata', metadata });\n }\n\n setRoomMetadata(metadata: Record<string, unknown>): void {\n if (!this._isHost) return;\n this._sendControlMessage({ type: 'request-room-metadata', metadata });\n }\n\n kick(peerId: string, reason?: string): void {\n if (!this._isHost) return;\n // Broadcast kick so the target peer and everyone else knows\n this._broadcastControlMessage({ type: 'kick', peerId, reason });\n }\n\n transferHost(peerId: string): void {\n if (!this._isHost) return;\n this._sendControlMessage({ type: 'request-transfer-host', peerId });\n }\n\n setRoomState(state: RoomState): void {\n if (!this._isHost) return;\n this._sendControlMessage({ type: 'request-room-state', state });\n }\n\n setMaxPlayers(n: number): void {\n if (!this._isHost) return;\n this._sendControlMessage({ type: 'request-max-players', maxPlayers: n });\n }\n\n lockRoom(): void {\n if (!this._isHost) return;\n this._sendControlMessage({ type: 'request-lock' });\n }\n\n unlockRoom(): void {\n if (!this._isHost) return;\n this._sendControlMessage({ type: 'request-unlock' });\n }\n\n /** No-op: lobby uses strategy.subscribeToLobby() directly */\n requestRoomList(): void {}\n\n // ── Private: Strategy callbacks ──\n\n private _onStrategyPeerDiscovered(peerId: string, meta: PeerMetadata): void {\n this._connectToPeer(peerId);\n this._peerSet.add(peerId);\n\n // Preserve prior player fields if this is a RE-discovery (e.g. the peer's\n // signaling presence flapped and was re-announced after a transient RTDB\n // reconnect) so we don't reset joinedAt / isReady / latency on a peer whose\n // P2P link never actually dropped.\n const existing = this._playerMap.get(peerId);\n const player: Player = {\n peerId,\n displayName: (meta.displayName as string) ?? existing?.displayName ?? `Player-${peerId.slice(0, 4)}`,\n isHost: false,\n isSelf: false,\n isReady: existing?.isReady ?? false,\n isConnected: true,\n metadata: meta as Record<string, unknown>,\n latencyMs: existing?.latencyMs ?? 0,\n joinedAt: existing?.joinedAt ?? Date.now(),\n };\n this._playerMap.set(peerId, player);\n this._electAndSetHost();\n\n for (const cb of this._callbacks.onPeerJoin) cb(peerId);\n for (const cb of this._callbacks.onPeerUpdated) cb(player);\n }\n\n // ── Private: Room control channel ──\n\n private _setupRoomControlChannel(): void {\n const ch = this.createChannel<RoomControlMessage>(ROOM_CONTROL_CHANNEL, {\n reliable: true,\n ordered: true,\n });\n ch.onReceive((msg, peerId) => {\n this._handleControlMessage(msg, peerId);\n });\n }\n\n private _handleControlMessage(msg: RoomControlMessage, fromPeerId: string): void {\n switch (msg.type) {\n case 'player-updated': {\n this._playerMap.set(msg.player.peerId, msg.player);\n for (const cb of this._callbacks.onPeerUpdated) cb(msg.player);\n break;\n }\n case 'room-updated': {\n if (this._room) {\n Object.assign(this._room, msg.room);\n for (const cb of this._roomUpdatedCallbacks) cb(this._room);\n }\n break;\n }\n case 'kick': {\n if (msg.peerId === this._peerId) {\n // We were kicked\n this.disconnect();\n }\n break;\n }\n case 'host-changed': {\n this._hostId = msg.newHostId;\n this._isHost = msg.newHostId === this._peerId;\n for (const cb of this._callbacks.onHostChanged) cb(msg.newHostId);\n break;\n }\n case 'sync-state': {\n // Full state sync from host (sent to newly connected peers)\n this._room = msg.room;\n for (const p of msg.players) {\n this._playerMap.set(p.peerId, { ...p, isSelf: p.peerId === this._peerId });\n for (const cb of this._callbacks.onPeerUpdated) cb(p);\n }\n for (const cb of this._roomUpdatedCallbacks) cb(msg.room);\n break;\n }\n\n // Host processes requests from peers\n case 'request-ready': {\n if (!this._isHost) break;\n const p = this._playerMap.get(fromPeerId);\n if (p) {\n p.isReady = msg.ready;\n this._broadcastControlMessage({ type: 'player-updated', player: p });\n }\n break;\n }\n case 'request-metadata': {\n if (!this._isHost) break;\n const pm = this._playerMap.get(fromPeerId);\n if (pm) {\n pm.metadata = { ...pm.metadata, ...msg.metadata };\n this._broadcastControlMessage({ type: 'player-updated', player: pm });\n }\n break;\n }\n case 'request-room-metadata': {\n if (!this._isHost || !this._room) break;\n this._room.metadata = { ...this._room.metadata, ...msg.metadata };\n this._broadcastControlMessage({ type: 'room-updated', room: this._room });\n break;\n }\n case 'request-room-state': {\n if (!this._isHost || !this._room) break;\n this._room.state = msg.state;\n this._strategy.updateRoomOccupancy?.(this._room.id, this._room.playerCount, this._room.state);\n this._broadcastControlMessage({ type: 'room-updated', room: this._room });\n break;\n }\n case 'request-max-players': {\n if (!this._isHost || !this._room) break;\n this._room.maxPlayers = msg.maxPlayers;\n this._broadcastControlMessage({ type: 'room-updated', room: this._room });\n break;\n }\n case 'request-lock': {\n if (!this._isHost || !this._room) break;\n (this._room as any).locked = true;\n this._broadcastControlMessage({ type: 'room-updated', room: this._room });\n break;\n }\n case 'request-unlock': {\n if (!this._isHost || !this._room) break;\n (this._room as any).locked = false;\n this._broadcastControlMessage({ type: 'room-updated', room: this._room });\n break;\n }\n case 'request-transfer-host': {\n if (!this._isHost) break;\n this._hostId = msg.peerId;\n this._isHost = false;\n this._broadcastControlMessage({ type: 'host-changed', newHostId: msg.peerId });\n break;\n }\n }\n }\n\n private _sendControlMessage(msg: RoomControlMessage): void {\n if (this._isHost && msg.type.startsWith('request-')) {\n // Host processes locally and broadcasts result\n this._handleControlMessage(msg, this._peerId);\n return;\n }\n // Non-host: send to host\n if (this._hostId && this._hostId !== this._peerId) {\n this._sendOnChannel(ROOM_CONTROL_CHANNEL, msg, this._hostId);\n }\n }\n\n private _broadcastControlMessage(msg: RoomControlMessage): void {\n this._sendOnChannel(ROOM_CONTROL_CHANNEL, msg);\n // Handle locally too so host updates its own state\n this._handleControlMessage(msg, this._peerId);\n }\n\n // ── Private: Host election ──\n\n /**\n * Host-election rank for a peer: lower wins. Reads `metadata.hostPriority`\n * (advertised via player metadata / signaling presence, so every peer sees\n * the same value). Peers that don't advertise a priority rank last, which\n * preserves the legacy lowest-peerId election among them.\n */\n private _hostRank(peerId: string): number {\n const meta = this._playerMap.get(peerId)?.metadata as Record<string, unknown> | undefined;\n const priority = meta?.hostPriority;\n return typeof priority === 'number' && Number.isFinite(priority)\n ? priority\n : Number.POSITIVE_INFINITY;\n }\n\n private _electAndSetHost(): void {\n const allIds = [this._peerId, ...this._peerSet];\n const newHostId = electHost(allIds, (id) => this._hostRank(id));\n const changed = newHostId !== this._hostId;\n this._hostId = newHostId;\n this._isHost = newHostId === this._peerId;\n\n for (const [id, p] of this._playerMap) {\n p.isHost = id === newHostId;\n }\n if (this._room) {\n this._room.hostId = newHostId;\n this._room.playerCount = this._playerMap.size;\n // Keep the lobby announcement's occupancy fresh (no-op on non-announcers)\n this._strategy.updateRoomOccupancy?.(this._room.id, this._room.playerCount, this._room.state);\n }\n\n if (changed) {\n for (const cb of this._callbacks.onHostChanged) cb(newHostId);\n }\n }\n\n // ── Private: WebRTC peer management ──\n\n /** Grace timers for transient ICE 'disconnected' states */\n private _disconnectTimers = new Map<string, ReturnType<typeof setTimeout>>();\n /** Sends queued while a data channel is not yet open: key = peerId\u0000channel */\n private _pendingSends = new Map<string, (string | ArrayBuffer | Uint8Array)[]>();\n\n private _connectToPeer(peerId: string): void {\n if (this._peers.has(peerId)) return;\n\n const peer = new PeerConnection(peerId, this._iceConfig, {\n onStateChange: (state) => {\n if (state === 'connected') {\n // ICE recovered — cancel any pending transient-disconnect teardown\n const timer = this._disconnectTimers.get(peerId);\n if (timer) {\n clearTimeout(timer);\n this._disconnectTimers.delete(peerId);\n }\n }\n if (state === 'connected' && this._isHost && this._room) {\n // Send full state sync to the new peer\n const syncMsg: RoomControlMessage = {\n type: 'sync-state',\n room: this._room,\n players: Array.from(this._playerMap.values()),\n };\n setTimeout(() => {\n this._sendOnChannel(ROOM_CONTROL_CHANNEL, syncMsg, peerId);\n }, 100);\n }\n if (state === 'failed' || state === 'disconnected') {\n this._handleConnectionDrop(peerId, state);\n }\n },\n onDataChannel: (channel) => {\n this._setupDataChannelReceiver(channel, peerId);\n },\n onIceCandidate: (candidate) => {\n this._strategy.signal(peerId, { type: 'ice-candidate', candidate: candidate.toJSON() });\n },\n });\n\n this._peers.set(peerId, peer);\n this._peerSet.add(peerId);\n\n // Deterministic initiator: lower peerId creates the offer\n if (this._peerId < peerId) {\n for (const [name, state] of this._channels) {\n this._createDataChannelOnPeer(peer, name, state.options);\n }\n peer.createOffer().then((offer) => {\n this._strategy.signal(peerId, { type: 'offer', sdp: offer });\n });\n }\n }\n\n private async _handleSignal(peerId: string, data: unknown): Promise<void> {\n try {\n const signal = data as {\n type: string;\n sdp?: RTCSessionDescriptionInit;\n candidate?: RTCIceCandidateInit;\n };\n\n let peer = this._peers.get(peerId);\n\n if (signal.type === 'offer') {\n if (!peer) {\n this._connectToPeer(peerId);\n peer = this._peers.get(peerId)!;\n }\n const answer = await peer.handleOffer(signal.sdp!);\n this._strategy.signal(peerId, { type: 'answer', sdp: answer });\n } else if (signal.type === 'answer' && peer) {\n await peer.handleAnswer(signal.sdp!);\n } else if (signal.type === 'ice-candidate' && peer) {\n await peer.addIceCandidate(signal.candidate!);\n }\n } catch (err) {\n if (typeof console !== 'undefined') {\n console.error('[CarverJS] Signal handling failed:', err);\n }\n }\n }\n\n // ── Private: Data channel helpers ──\n\n private _createDataChannelOnPeer(peer: PeerConnection, name: string, options: ChannelOptions): void {\n const channel = peer.createDataChannel(name, options);\n this._setupDataChannelReceiver(channel, peer.peerId);\n }\n\n private _setupDataChannelReceiver(dataChannel: RTCDataChannel, peerId: string): void {\n const channelName = dataChannel.label;\n dataChannel.onmessage = (event) => {\n if (!this._checkRateLimit(peerId)) return;\n const channelState = this._channels.get(channelName);\n if (!channelState) return;\n try {\n const data =\n typeof event.data === 'string' ? JSON.parse(event.data) : event.data;\n for (const receiver of channelState.receivers) receiver(data, peerId);\n } catch {\n // Ignore malformed messages\n }\n };\n // Flush any sends that were queued before this channel was usable\n dataChannel.onopen = () => this._flushPendingSends(peerId, channelName);\n if (dataChannel.readyState === 'open') this._flushPendingSends(peerId, channelName);\n }\n\n private _sendOnChannel<T>(channelName: string, data: T, target?: string | string[]): void {\n const serialized =\n typeof data === 'object' &&\n data !== null &&\n !(data instanceof ArrayBuffer) &&\n !(data instanceof Uint8Array)\n ? JSON.stringify(data)\n : data;\n\n const targets = target\n ? Array.isArray(target) ? target : [target]\n : Array.from(this._peers.keys());\n\n for (const pid of targets) {\n const peer = this._peers.get(pid);\n if (!peer) continue;\n const ch = peer.getDataChannel(channelName);\n if (ch?.readyState === 'open') {\n try { ch.send(serialized as string); } catch { /* closed between check and send */ }\n } else {\n // Channel not open yet (the answering side waits for ondatachannel).\n // Queue instead of silently dropping — flushed on channel open.\n const key = pid + '\u0000' + channelName;\n const q = this._pendingSends.get(key) ?? [];\n if (q.length < 200) q.push(serialized as string | ArrayBuffer | Uint8Array);\n this._pendingSends.set(key, q);\n }\n }\n }\n\n private _flushPendingSends(peerId: string, channelName: string): void {\n const key = peerId + '\u0000' + channelName;\n const q = this._pendingSends.get(key);\n if (!q || q.length === 0) return;\n const ch = this._peers.get(peerId)?.getDataChannel(channelName);\n if (ch?.readyState !== 'open') return;\n this._pendingSends.delete(key);\n for (const msg of q) {\n try { ch.send(msg as string); } catch { break; }\n }\n }\n\n /**\n * Signaling presence reports a peer left. This is NOT authoritative for an\n * established session: a transient signaling (Firebase/MQTT) disconnect can\n * remove a peer's presence while the direct WebRTC link is perfectly healthy.\n * Keep the peer if its connection is still 'connected'; genuine departures\n * are also surfaced by the WebRTC connection-state machine\n * (failed/disconnected -> _handleConnectionDrop), which tears the peer down.\n */\n private _onStrategyPeerLeft(peerId: string): void {\n const peer = this._peers.get(peerId);\n if (peer && peer.state === 'connected') return;\n this._teardownPeer(peerId);\n }\n\n /**\n * Self-heal a dropped P2P link instead of tearing it down immediately. ICE\n * 'disconnected' is frequently transient and 'failed' can often recover via\n * an ICE restart. The deterministic initiator (lower peerId) renegotiates by\n * sending a fresh offer with iceRestart; the answerer replies through the\n * existing _handleSignal('offer') path. A grace timer is the fallback: tear\n * the peer down only if it doesn't return to 'connected'. Recovery to\n * 'connected' cancels the timer (see onStateChange in _connectToPeer).\n */\n private _handleConnectionDrop(peerId: string, state: 'failed' | 'disconnected'): void {\n if (this._disconnectTimers.has(peerId)) return; // already self-healing\n\n // Initiator drives the ICE restart, mirroring initial-offer ownership.\n if (this._peerId < peerId) {\n const peer = this._peers.get(peerId);\n peer?.createOffer({ iceRestart: true })\n .then((offer) => {\n // Bail if the peer was replaced/removed while creating the offer.\n if (this._peers.get(peerId) === peer) {\n this._strategy.signal(peerId, { type: 'offer', sdp: offer });\n }\n })\n .catch(() => { /* restart failed; grace timer tears it down */ });\n }\n\n const graceMs = state === 'failed' ? 8000 : 5000;\n this._disconnectTimers.set(peerId, setTimeout(() => {\n this._disconnectTimers.delete(peerId);\n const p = this._peers.get(peerId);\n if (p && p.state !== 'connected') this._teardownPeer(peerId);\n }, graceMs));\n }\n\n private _teardownPeer(peerId: string): void {\n this._removePeer(peerId);\n this._playerMap.delete(peerId);\n this._electAndSetHost();\n for (const cb of this._callbacks.onPeerLeave) cb(peerId);\n }\n\n private _removePeer(peerId: string): void {\n const peer = this._peers.get(peerId);\n if (peer) { peer.close(); this._peers.delete(peerId); }\n this._peerSet.delete(peerId);\n this._rateLimitCounters.delete(peerId);\n const timer = this._disconnectTimers.get(peerId);\n if (timer) { clearTimeout(timer); this._disconnectTimers.delete(peerId); }\n for (const key of [...this._pendingSends.keys()]) {\n if (key.startsWith(peerId + '\u0000')) this._pendingSends.delete(key);\n }\n }\n\n private _checkRateLimit(peerId: string): boolean {\n const now = Date.now();\n let c = this._rateLimitCounters.get(peerId);\n if (!c || now >= c.resetAt) {\n c = { count: 0, resetAt: now + this._rateLimitConfig.windowMs };\n this._rateLimitCounters.set(peerId, c);\n }\n c.count++;\n return c.count <= this._rateLimitConfig.maxMessagesPerSecond;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -423,7 +423,7 @@ var FirebaseStrategy = class {
423
423
  this._joinGeneration++;
424
424
  this._roomId = roomId;
425
425
  this._peerMeta = peerMeta;
426
- const { ref, set, onChildAdded, onChildRemoved, onDisconnect, remove } = this._fb;
426
+ const { ref, set, onChildAdded, onChildRemoved, onDisconnect, onValue, remove } = this._fb;
427
427
  const paths = firebasePaths(this._appId, roomId, this.selfId);
428
428
  await remove(ref(this._db, paths.peerSignalInbox)).catch(() => {
429
429
  });
@@ -434,6 +434,18 @@ var FirebaseStrategy = class {
434
434
  ts: Date.now()
435
435
  });
436
436
  onDisconnect(presenceRef).remove();
437
+ const generation = this._joinGeneration;
438
+ const connectedRef = ref(this._db, ".info/connected");
439
+ const connectedUnsub = onValue(connectedRef, (snap) => {
440
+ if (snap.val() !== true) return;
441
+ if (this._destroyed || this._joinGeneration !== generation || this._roomId !== roomId) return;
442
+ onDisconnect(presenceRef).remove().then(() => {
443
+ if (this._destroyed || this._joinGeneration !== generation || this._roomId !== roomId) return;
444
+ return set(presenceRef, { peerId: this.selfId, meta: peerMeta, ts: Date.now() });
445
+ }).catch(() => {
446
+ });
447
+ });
448
+ this._listeners.push(() => connectedUnsub());
437
449
  const peersRef = ref(this._db, paths.peers);
438
450
  const addedUnsub = onChildAdded(peersRef, (snapshot) => {
439
451
  const data = snapshot.val();
@@ -1268,8 +1280,8 @@ var PeerConnection = class {
1268
1280
  this._events.onStateChange(newState);
1269
1281
  }
1270
1282
  }
1271
- async createOffer() {
1272
- const offer = await this._connection.createOffer();
1283
+ async createOffer(options) {
1284
+ const offer = await this._connection.createOffer(options);
1273
1285
  await this._connection.setLocalDescription(offer);
1274
1286
  return offer;
1275
1287
  }
@@ -1345,8 +1357,17 @@ var PeerConnection = class {
1345
1357
 
1346
1358
  // src/transport/webrtc/WebRTCTransport.ts
1347
1359
  var ROOM_CONTROL_CHANNEL = "carver:room-control";
1348
- function electHost(peerIds) {
1349
- return [...peerIds].sort()[0];
1360
+ function electHost(peerIds, rankOf) {
1361
+ let best = peerIds[0];
1362
+ let bestRank = rankOf(best);
1363
+ for (const id of peerIds) {
1364
+ const rank = rankOf(id);
1365
+ if (rank < bestRank || rank === bestRank && id < best) {
1366
+ best = id;
1367
+ bestRank = rank;
1368
+ }
1369
+ }
1370
+ return best;
1350
1371
  }
1351
1372
  var WebRTCTransport = class {
1352
1373
  /**
@@ -1474,10 +1495,7 @@ var WebRTCTransport = class {
1474
1495
  );
1475
1496
  this._strategyUnsubs.push(
1476
1497
  this._strategy.onPeerLeft((peerId) => {
1477
- this._removePeer(peerId);
1478
- this._playerMap.delete(peerId);
1479
- this._electAndSetHost();
1480
- for (const cb of this._callbacks.onPeerLeave) cb(peerId);
1498
+ this._onStrategyPeerLeft(peerId);
1481
1499
  })
1482
1500
  );
1483
1501
  this._strategyUnsubs.push(
@@ -1590,16 +1608,17 @@ var WebRTCTransport = class {
1590
1608
  _onStrategyPeerDiscovered(peerId, meta) {
1591
1609
  this._connectToPeer(peerId);
1592
1610
  this._peerSet.add(peerId);
1611
+ const existing = this._playerMap.get(peerId);
1593
1612
  const player = {
1594
1613
  peerId,
1595
- displayName: meta.displayName ?? `Player-${peerId.slice(0, 4)}`,
1614
+ displayName: meta.displayName ?? existing?.displayName ?? `Player-${peerId.slice(0, 4)}`,
1596
1615
  isHost: false,
1597
1616
  isSelf: false,
1598
- isReady: false,
1617
+ isReady: existing?.isReady ?? false,
1599
1618
  isConnected: true,
1600
1619
  metadata: meta,
1601
- latencyMs: 0,
1602
- joinedAt: Date.now()
1620
+ latencyMs: existing?.latencyMs ?? 0,
1621
+ joinedAt: existing?.joinedAt ?? Date.now()
1603
1622
  };
1604
1623
  this._playerMap.set(peerId, player);
1605
1624
  this._electAndSetHost();
@@ -1724,9 +1743,20 @@ var WebRTCTransport = class {
1724
1743
  this._handleControlMessage(msg, this._peerId);
1725
1744
  }
1726
1745
  // ── Private: Host election ──
1746
+ /**
1747
+ * Host-election rank for a peer: lower wins. Reads `metadata.hostPriority`
1748
+ * (advertised via player metadata / signaling presence, so every peer sees
1749
+ * the same value). Peers that don't advertise a priority rank last, which
1750
+ * preserves the legacy lowest-peerId election among them.
1751
+ */
1752
+ _hostRank(peerId) {
1753
+ const meta = this._playerMap.get(peerId)?.metadata;
1754
+ const priority = meta?.hostPriority;
1755
+ return typeof priority === "number" && Number.isFinite(priority) ? priority : Number.POSITIVE_INFINITY;
1756
+ }
1727
1757
  _electAndSetHost() {
1728
1758
  const allIds = [this._peerId, ...this._peerSet];
1729
- const newHostId = electHost(allIds);
1759
+ const newHostId = electHost(allIds, (id) => this._hostRank(id));
1730
1760
  const changed = newHostId !== this._hostId;
1731
1761
  this._hostId = newHostId;
1732
1762
  this._isHost = newHostId === this._peerId;
@@ -1763,14 +1793,8 @@ var WebRTCTransport = class {
1763
1793
  this._sendOnChannel(ROOM_CONTROL_CHANNEL, syncMsg, peerId);
1764
1794
  }, 100);
1765
1795
  }
1766
- if (state === "failed") {
1767
- this._teardownPeer(peerId);
1768
- } else if (state === "disconnected" && !this._disconnectTimers.has(peerId)) {
1769
- this._disconnectTimers.set(peerId, setTimeout(() => {
1770
- this._disconnectTimers.delete(peerId);
1771
- const p = this._peers.get(peerId);
1772
- if (p && p.state !== "connected") this._teardownPeer(peerId);
1773
- }, 5e3));
1796
+ if (state === "failed" || state === "disconnected") {
1797
+ this._handleConnectionDrop(peerId, state);
1774
1798
  }
1775
1799
  },
1776
1800
  onDataChannel: (channel) => {
@@ -1868,6 +1892,46 @@ var WebRTCTransport = class {
1868
1892
  }
1869
1893
  }
1870
1894
  }
1895
+ /**
1896
+ * Signaling presence reports a peer left. This is NOT authoritative for an
1897
+ * established session: a transient signaling (Firebase/MQTT) disconnect can
1898
+ * remove a peer's presence while the direct WebRTC link is perfectly healthy.
1899
+ * Keep the peer if its connection is still 'connected'; genuine departures
1900
+ * are also surfaced by the WebRTC connection-state machine
1901
+ * (failed/disconnected -> _handleConnectionDrop), which tears the peer down.
1902
+ */
1903
+ _onStrategyPeerLeft(peerId) {
1904
+ const peer = this._peers.get(peerId);
1905
+ if (peer && peer.state === "connected") return;
1906
+ this._teardownPeer(peerId);
1907
+ }
1908
+ /**
1909
+ * Self-heal a dropped P2P link instead of tearing it down immediately. ICE
1910
+ * 'disconnected' is frequently transient and 'failed' can often recover via
1911
+ * an ICE restart. The deterministic initiator (lower peerId) renegotiates by
1912
+ * sending a fresh offer with iceRestart; the answerer replies through the
1913
+ * existing _handleSignal('offer') path. A grace timer is the fallback: tear
1914
+ * the peer down only if it doesn't return to 'connected'. Recovery to
1915
+ * 'connected' cancels the timer (see onStateChange in _connectToPeer).
1916
+ */
1917
+ _handleConnectionDrop(peerId, state) {
1918
+ if (this._disconnectTimers.has(peerId)) return;
1919
+ if (this._peerId < peerId) {
1920
+ const peer = this._peers.get(peerId);
1921
+ peer?.createOffer({ iceRestart: true }).then((offer) => {
1922
+ if (this._peers.get(peerId) === peer) {
1923
+ this._strategy.signal(peerId, { type: "offer", sdp: offer });
1924
+ }
1925
+ }).catch(() => {
1926
+ });
1927
+ }
1928
+ const graceMs = state === "failed" ? 8e3 : 5e3;
1929
+ this._disconnectTimers.set(peerId, setTimeout(() => {
1930
+ this._disconnectTimers.delete(peerId);
1931
+ const p = this._peers.get(peerId);
1932
+ if (p && p.state !== "connected") this._teardownPeer(peerId);
1933
+ }, graceMs));
1934
+ }
1871
1935
  _teardownPeer(peerId) {
1872
1936
  this._removePeer(peerId);
1873
1937
  this._playerMap.delete(peerId);