@edge-base/react-native 0.2.6 → 0.2.7

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/dist/index.js CHANGED
@@ -750,7 +750,7 @@ var DatabaseLiveClient = class {
750
750
  (refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
751
751
  );
752
752
  if (!token) throw new EdgeBaseError(401, "No access token available. Sign in first.");
753
- this.sendRaw({ type: "auth", token, sdkVersion: "0.2.6" });
753
+ this.sendRaw({ type: "auth", token, sdkVersion: "0.2.7" });
754
754
  return new Promise((resolve, reject) => {
755
755
  const timeout = setTimeout(() => reject(new EdgeBaseError(401, "Auth timeout")), 1e4);
756
756
  const original = this.ws?.onmessage;
@@ -870,7 +870,7 @@ var DatabaseLiveClient = class {
870
870
  refreshAuth() {
871
871
  const token = this.tokenManager.currentAccessToken;
872
872
  if (!token || !this.ws || !this.connected) return;
873
- this.sendRaw({ type: "auth", token, sdkVersion: "0.2.6" });
873
+ this.sendRaw({ type: "auth", token, sdkVersion: "0.2.7" });
874
874
  }
875
875
  handleAuthStateChange(user) {
876
876
  if (user) {
@@ -1016,1110 +1016,6 @@ function matchesDatabaseLiveChannel(channel, change, messageChannel) {
1016
1016
  }
1017
1017
  return parts[3] === change.table && change.docId === parts[4];
1018
1018
  }
1019
-
1020
- // src/room-cloudflare-media.ts
1021
- function buildRemoteTrackKey(participantId, kind) {
1022
- return `${participantId}:${kind}`;
1023
- }
1024
- function installMessage(packageName) {
1025
- return `Install ${packageName} to use React Native room media transport. See https://edgebase.fun/docs/room/media`;
1026
- }
1027
- var RoomCloudflareMediaTransport = class {
1028
- room;
1029
- options;
1030
- localTracks = /* @__PURE__ */ new Map();
1031
- remoteTrackHandlers = [];
1032
- participantListeners = /* @__PURE__ */ new Map();
1033
- remoteTrackIds = /* @__PURE__ */ new Map();
1034
- clientFactoryPromise = null;
1035
- connectPromise = null;
1036
- lifecycleVersion = 0;
1037
- meeting = null;
1038
- sessionId = null;
1039
- joinedMapSubscriptionsAttached = false;
1040
- onParticipantJoined = (participant) => {
1041
- this.attachParticipant(participant);
1042
- };
1043
- onParticipantLeft = (participant) => {
1044
- this.detachParticipant(participant.id);
1045
- };
1046
- onParticipantsCleared = () => {
1047
- this.clearParticipantListeners();
1048
- };
1049
- onParticipantsUpdate = () => {
1050
- this.syncAllParticipants();
1051
- };
1052
- constructor(room, options) {
1053
- this.room = room;
1054
- this.options = {
1055
- autoSubscribe: options?.autoSubscribe ?? true,
1056
- mediaDevices: options?.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0),
1057
- clientFactory: options?.clientFactory
1058
- };
1059
- }
1060
- getSessionId() {
1061
- return this.sessionId;
1062
- }
1063
- getPeerConnection() {
1064
- return null;
1065
- }
1066
- async connect(payload) {
1067
- if (this.meeting && this.sessionId) {
1068
- return this.sessionId;
1069
- }
1070
- if (this.connectPromise) {
1071
- return this.connectPromise;
1072
- }
1073
- const connectPromise = (async () => {
1074
- const lifecycleVersion = this.lifecycleVersion;
1075
- const session = await this.room.media.cloudflareRealtimeKit.createSession(payload);
1076
- this.assertConnectStillActive(lifecycleVersion);
1077
- const factory = await this.resolveClientFactory();
1078
- this.assertConnectStillActive(lifecycleVersion);
1079
- const meeting = await factory.init({
1080
- authToken: session.authToken,
1081
- defaults: {
1082
- audio: false,
1083
- video: false
1084
- }
1085
- });
1086
- this.assertConnectStillActive(lifecycleVersion, meeting);
1087
- this.meeting = meeting;
1088
- this.sessionId = session.sessionId;
1089
- this.attachParticipantMapListeners();
1090
- try {
1091
- if (meeting.join) {
1092
- await meeting.join();
1093
- } else if (meeting.joinRoom) {
1094
- await meeting.joinRoom();
1095
- } else {
1096
- throw new Error("RealtimeKit client does not expose join()/joinRoom().");
1097
- }
1098
- this.assertConnectStillActive(lifecycleVersion, meeting);
1099
- this.syncAllParticipants();
1100
- return session.sessionId;
1101
- } catch (error) {
1102
- if (this.meeting === meeting) {
1103
- this.cleanupMeeting();
1104
- } else {
1105
- this.leaveMeetingSilently(meeting);
1106
- }
1107
- throw error;
1108
- }
1109
- })();
1110
- this.connectPromise = connectPromise;
1111
- try {
1112
- return await connectPromise;
1113
- } finally {
1114
- if (this.connectPromise === connectPromise) {
1115
- this.connectPromise = null;
1116
- }
1117
- }
1118
- }
1119
- async enableAudio(constraints = true) {
1120
- const meeting = await this.ensureMeeting();
1121
- const customTrack = await this.createUserMediaTrack("audio", constraints);
1122
- await meeting.self.enableAudio(customTrack ?? void 0);
1123
- const track = meeting.self.audioTrack ?? customTrack;
1124
- if (!track) {
1125
- throw new Error("RealtimeKit did not expose a local audio track after enabling audio.");
1126
- }
1127
- this.rememberLocalTrack("audio", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
1128
- await this.room.media.audio.enable?.({
1129
- trackId: track.id,
1130
- deviceId: this.localTracks.get("audio")?.deviceId,
1131
- providerSessionId: meeting.self.id
1132
- });
1133
- return track;
1134
- }
1135
- async enableVideo(constraints = true) {
1136
- const meeting = await this.ensureMeeting();
1137
- const customTrack = await this.createUserMediaTrack("video", constraints);
1138
- await meeting.self.enableVideo(customTrack ?? void 0);
1139
- const track = meeting.self.videoTrack ?? customTrack;
1140
- if (!track) {
1141
- throw new Error("RealtimeKit did not expose a local video track after enabling video.");
1142
- }
1143
- this.rememberLocalTrack("video", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
1144
- await this.room.media.video.enable?.({
1145
- trackId: track.id,
1146
- deviceId: this.localTracks.get("video")?.deviceId,
1147
- providerSessionId: meeting.self.id
1148
- });
1149
- return track;
1150
- }
1151
- async startScreenShare(_constraints = { video: true, audio: false }) {
1152
- const meeting = await this.ensureMeeting();
1153
- await meeting.self.enableScreenShare();
1154
- const track = meeting.self.screenShareTracks?.video;
1155
- if (!track) {
1156
- throw new Error("RealtimeKit did not expose a screen-share video track.");
1157
- }
1158
- track.addEventListener("ended", () => {
1159
- void this.stopScreenShare();
1160
- }, { once: true });
1161
- this.rememberLocalTrack("screen", track, track.getSettings().deviceId, false);
1162
- await this.room.media.screen.start?.({
1163
- trackId: track.id,
1164
- deviceId: track.getSettings().deviceId,
1165
- providerSessionId: meeting.self.id
1166
- });
1167
- return track;
1168
- }
1169
- async disableAudio() {
1170
- if (!this.meeting) return;
1171
- await this.meeting.self.disableAudio();
1172
- this.releaseLocalTrack("audio");
1173
- await this.room.media.audio.disable();
1174
- }
1175
- async disableVideo() {
1176
- if (!this.meeting) return;
1177
- await this.meeting.self.disableVideo();
1178
- this.releaseLocalTrack("video");
1179
- await this.room.media.video.disable();
1180
- }
1181
- async stopScreenShare() {
1182
- if (!this.meeting) return;
1183
- await this.meeting.self.disableScreenShare();
1184
- this.releaseLocalTrack("screen");
1185
- await this.room.media.screen.stop();
1186
- }
1187
- async setMuted(kind, muted) {
1188
- const localTrack = this.localTracks.get(kind)?.track ?? (kind === "audio" ? this.meeting?.self.audioTrack : this.meeting?.self.videoTrack);
1189
- if (localTrack) {
1190
- localTrack.enabled = !muted;
1191
- }
1192
- if (kind === "audio") {
1193
- await this.room.media.audio.setMuted?.(muted);
1194
- } else {
1195
- await this.room.media.video.setMuted?.(muted);
1196
- }
1197
- }
1198
- async switchDevices(payload) {
1199
- const meeting = await this.ensureMeeting();
1200
- if (payload.audioInputId) {
1201
- const audioDevice = await meeting.self.getDeviceById(payload.audioInputId, "audio");
1202
- await meeting.self.setDevice(audioDevice);
1203
- const audioTrack = meeting.self.audioTrack;
1204
- if (audioTrack) {
1205
- this.rememberLocalTrack("audio", audioTrack, payload.audioInputId, false);
1206
- }
1207
- }
1208
- if (payload.videoInputId) {
1209
- const videoDevice = await meeting.self.getDeviceById(payload.videoInputId, "video");
1210
- await meeting.self.setDevice(videoDevice);
1211
- const videoTrack = meeting.self.videoTrack;
1212
- if (videoTrack) {
1213
- this.rememberLocalTrack("video", videoTrack, payload.videoInputId, false);
1214
- }
1215
- }
1216
- await this.room.media.devices.switch(payload);
1217
- }
1218
- onRemoteTrack(handler) {
1219
- this.remoteTrackHandlers.push(handler);
1220
- return createSubscription(() => {
1221
- const index = this.remoteTrackHandlers.indexOf(handler);
1222
- if (index >= 0) {
1223
- this.remoteTrackHandlers.splice(index, 1);
1224
- }
1225
- });
1226
- }
1227
- destroy() {
1228
- this.lifecycleVersion += 1;
1229
- this.connectPromise = null;
1230
- for (const kind of this.localTracks.keys()) {
1231
- this.releaseLocalTrack(kind);
1232
- }
1233
- this.clearParticipantListeners();
1234
- this.detachParticipantMapListeners();
1235
- this.cleanupMeeting();
1236
- }
1237
- async ensureMeeting() {
1238
- if (!this.meeting) {
1239
- await this.connect();
1240
- }
1241
- if (!this.meeting) {
1242
- throw new Error("Cloudflare media transport is not connected");
1243
- }
1244
- return this.meeting;
1245
- }
1246
- async resolveClientFactory() {
1247
- if (this.options.clientFactory) {
1248
- return this.options.clientFactory;
1249
- }
1250
- this.clientFactoryPromise ??= import('@cloudflare/realtimekit-react-native').then((mod) => mod.default ?? mod).catch((error) => {
1251
- throw new Error(`${installMessage("@cloudflare/realtimekit-react-native")}
1252
- ${String(error)}`);
1253
- });
1254
- return this.clientFactoryPromise;
1255
- }
1256
- async createUserMediaTrack(kind, constraints) {
1257
- const devices = this.options.mediaDevices;
1258
- if (!devices?.getUserMedia || constraints === false) {
1259
- return null;
1260
- }
1261
- const stream = await devices.getUserMedia(
1262
- kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
1263
- );
1264
- return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
1265
- }
1266
- rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
1267
- this.releaseLocalTrack(kind);
1268
- this.localTracks.set(kind, {
1269
- kind,
1270
- track,
1271
- deviceId,
1272
- stopOnCleanup
1273
- });
1274
- }
1275
- releaseLocalTrack(kind) {
1276
- const local = this.localTracks.get(kind);
1277
- if (!local) return;
1278
- if (local.stopOnCleanup) {
1279
- local.track.stop();
1280
- }
1281
- this.localTracks.delete(kind);
1282
- }
1283
- attachParticipantMapListeners() {
1284
- const participantMap = this.getParticipantMap();
1285
- if (!participantMap || !this.meeting || this.joinedMapSubscriptionsAttached) {
1286
- return;
1287
- }
1288
- participantMap.on("participantJoined", this.onParticipantJoined);
1289
- participantMap.on("participantLeft", this.onParticipantLeft);
1290
- participantMap.on("participantsCleared", this.onParticipantsCleared);
1291
- participantMap.on("participantsUpdate", this.onParticipantsUpdate);
1292
- this.joinedMapSubscriptionsAttached = true;
1293
- }
1294
- detachParticipantMapListeners() {
1295
- const participantMap = this.getParticipantMap();
1296
- if (!participantMap || !this.meeting || !this.joinedMapSubscriptionsAttached) {
1297
- return;
1298
- }
1299
- participantMap.off("participantJoined", this.onParticipantJoined);
1300
- participantMap.off("participantLeft", this.onParticipantLeft);
1301
- participantMap.off("participantsCleared", this.onParticipantsCleared);
1302
- participantMap.off("participantsUpdate", this.onParticipantsUpdate);
1303
- this.joinedMapSubscriptionsAttached = false;
1304
- }
1305
- syncAllParticipants() {
1306
- const participantMap = this.getParticipantMap();
1307
- if (!participantMap || !this.meeting || !this.options.autoSubscribe) {
1308
- return;
1309
- }
1310
- for (const participant of participantMap.values()) {
1311
- this.attachParticipant(participant);
1312
- }
1313
- }
1314
- getParticipantMap() {
1315
- if (!this.meeting) {
1316
- return null;
1317
- }
1318
- return this.meeting.participants.active ?? this.meeting.participants.joined ?? null;
1319
- }
1320
- attachParticipant(participant) {
1321
- if (!this.options.autoSubscribe || !this.meeting) {
1322
- return;
1323
- }
1324
- if (participant.id === this.meeting.self.id || this.participantListeners.has(participant.id)) {
1325
- this.syncParticipantTracks(participant);
1326
- return;
1327
- }
1328
- const listenerSet = {
1329
- participant,
1330
- onAudioUpdate: ({ audioEnabled, audioTrack }) => {
1331
- this.handleRemoteTrackUpdate("audio", participant, audioTrack, audioEnabled);
1332
- },
1333
- onVideoUpdate: ({ videoEnabled, videoTrack }) => {
1334
- this.handleRemoteTrackUpdate("video", participant, videoTrack, videoEnabled);
1335
- },
1336
- onScreenShareUpdate: ({ screenShareEnabled, screenShareTracks }) => {
1337
- this.handleRemoteTrackUpdate("screen", participant, screenShareTracks.video, screenShareEnabled);
1338
- }
1339
- };
1340
- participant.on("audioUpdate", listenerSet.onAudioUpdate);
1341
- participant.on("videoUpdate", listenerSet.onVideoUpdate);
1342
- participant.on("screenShareUpdate", listenerSet.onScreenShareUpdate);
1343
- this.participantListeners.set(participant.id, listenerSet);
1344
- this.syncParticipantTracks(participant);
1345
- }
1346
- detachParticipant(participantId) {
1347
- const listenerSet = this.participantListeners.get(participantId);
1348
- if (!listenerSet) return;
1349
- listenerSet.participant.off("audioUpdate", listenerSet.onAudioUpdate);
1350
- listenerSet.participant.off("videoUpdate", listenerSet.onVideoUpdate);
1351
- listenerSet.participant.off("screenShareUpdate", listenerSet.onScreenShareUpdate);
1352
- this.participantListeners.delete(participantId);
1353
- for (const kind of ["audio", "video", "screen"]) {
1354
- this.remoteTrackIds.delete(buildRemoteTrackKey(participantId, kind));
1355
- }
1356
- }
1357
- clearParticipantListeners() {
1358
- for (const participantId of Array.from(this.participantListeners.keys())) {
1359
- this.detachParticipant(participantId);
1360
- }
1361
- this.remoteTrackIds.clear();
1362
- }
1363
- syncParticipantTracks(participant) {
1364
- this.handleRemoteTrackUpdate("audio", participant, participant.audioTrack, participant.audioEnabled === true);
1365
- this.handleRemoteTrackUpdate("video", participant, participant.videoTrack, participant.videoEnabled === true);
1366
- this.handleRemoteTrackUpdate("screen", participant, participant.screenShareTracks?.video, participant.screenShareEnabled === true);
1367
- }
1368
- handleRemoteTrackUpdate(kind, participant, track, enabled) {
1369
- const key = buildRemoteTrackKey(participant.id, kind);
1370
- if (!enabled || !track) {
1371
- this.remoteTrackIds.delete(key);
1372
- return;
1373
- }
1374
- const previousTrackId = this.remoteTrackIds.get(key);
1375
- if (previousTrackId === track.id) {
1376
- return;
1377
- }
1378
- this.remoteTrackIds.set(key, track.id);
1379
- const payload = {
1380
- kind,
1381
- track,
1382
- stream: new MediaStream([track]),
1383
- trackName: track.id,
1384
- providerSessionId: participant.id,
1385
- participantId: participant.id,
1386
- customParticipantId: participant.customParticipantId,
1387
- userId: participant.userId
1388
- };
1389
- for (const handler of this.remoteTrackHandlers) {
1390
- handler(payload);
1391
- }
1392
- }
1393
- cleanupMeeting() {
1394
- const meeting = this.meeting;
1395
- this.detachParticipantMapListeners();
1396
- this.clearParticipantListeners();
1397
- this.meeting = null;
1398
- this.sessionId = null;
1399
- this.leaveMeetingSilently(meeting);
1400
- }
1401
- assertConnectStillActive(lifecycleVersion, meeting) {
1402
- if (lifecycleVersion === this.lifecycleVersion) {
1403
- return;
1404
- }
1405
- if (meeting) {
1406
- this.leaveMeetingSilently(meeting);
1407
- }
1408
- throw new Error("Cloudflare media transport was destroyed during connect.");
1409
- }
1410
- leaveMeetingSilently(meeting) {
1411
- if (!meeting) {
1412
- return;
1413
- }
1414
- const leavePromise = meeting.leave?.() ?? meeting.leaveRoom?.();
1415
- if (leavePromise) {
1416
- void leavePromise.catch(() => {
1417
- });
1418
- }
1419
- }
1420
- };
1421
-
1422
- // src/room-p2p-media.ts
1423
- var DEFAULT_SIGNAL_PREFIX = "edgebase.media.p2p";
1424
- var DEFAULT_ICE_SERVERS = [
1425
- { urls: "stun:stun.l.google.com:19302" }
1426
- ];
1427
- var DEFAULT_MEMBER_READY_TIMEOUT_MS = 1e4;
1428
- function buildTrackKey(memberId, trackId) {
1429
- return `${memberId}:${trackId}`;
1430
- }
1431
- function buildExactDeviceConstraint(deviceId) {
1432
- return { deviceId: { exact: deviceId } };
1433
- }
1434
- function normalizeTrackKind(track) {
1435
- if (track.kind === "audio") return "audio";
1436
- if (track.kind === "video") return "video";
1437
- return null;
1438
- }
1439
- function serializeDescription(description) {
1440
- return {
1441
- type: description.type,
1442
- sdp: description.sdp ?? void 0
1443
- };
1444
- }
1445
- function serializeCandidate(candidate) {
1446
- if ("toJSON" in candidate && typeof candidate.toJSON === "function") {
1447
- return candidate.toJSON();
1448
- }
1449
- return candidate;
1450
- }
1451
- function installMessage2(packageName) {
1452
- return `Install ${packageName} to use React Native P2P media transport. See https://edgebase.fun/docs/room/media`;
1453
- }
1454
- var RoomP2PMediaTransport = class {
1455
- room;
1456
- options;
1457
- localTracks = /* @__PURE__ */ new Map();
1458
- peers = /* @__PURE__ */ new Map();
1459
- remoteTrackHandlers = [];
1460
- remoteTrackKinds = /* @__PURE__ */ new Map();
1461
- emittedRemoteTracks = /* @__PURE__ */ new Set();
1462
- pendingRemoteTracks = /* @__PURE__ */ new Map();
1463
- subscriptions = [];
1464
- localMemberId = null;
1465
- connected = false;
1466
- runtimePromise = null;
1467
- constructor(room, options) {
1468
- this.room = room;
1469
- this.options = {
1470
- rtcConfiguration: {
1471
- ...options?.rtcConfiguration,
1472
- iceServers: options?.rtcConfiguration?.iceServers && options.rtcConfiguration.iceServers.length > 0 ? options.rtcConfiguration.iceServers : DEFAULT_ICE_SERVERS
1473
- },
1474
- peerConnectionFactory: options?.peerConnectionFactory,
1475
- mediaDevices: options?.mediaDevices,
1476
- signalPrefix: options?.signalPrefix ?? DEFAULT_SIGNAL_PREFIX
1477
- };
1478
- }
1479
- getSessionId() {
1480
- return this.localMemberId;
1481
- }
1482
- getPeerConnection() {
1483
- if (this.peers.size !== 1) {
1484
- return null;
1485
- }
1486
- return this.peers.values().next().value?.pc ?? null;
1487
- }
1488
- async connect(payload) {
1489
- if (this.connected && this.localMemberId) {
1490
- return this.localMemberId;
1491
- }
1492
- if (payload && typeof payload === "object" && "sessionDescription" in payload) {
1493
- throw new Error(
1494
- "RoomP2PMediaTransport.connect() does not accept sessionDescription; use room.signals through the built-in transport instead."
1495
- );
1496
- }
1497
- await this.ensureRuntime();
1498
- const currentMember = await this.waitForCurrentMember();
1499
- if (!currentMember) {
1500
- throw new Error("Join the room before connecting a P2P media transport.");
1501
- }
1502
- this.localMemberId = currentMember.memberId;
1503
- this.connected = true;
1504
- this.hydrateRemoteTrackKinds();
1505
- this.attachRoomSubscriptions();
1506
- try {
1507
- for (const member of this.room.members.list()) {
1508
- if (member.memberId !== this.localMemberId) {
1509
- this.ensurePeer(member.memberId);
1510
- }
1511
- }
1512
- } catch (error) {
1513
- this.rollbackConnectedState();
1514
- throw error;
1515
- }
1516
- return this.localMemberId;
1517
- }
1518
- async waitForCurrentMember(timeoutMs = DEFAULT_MEMBER_READY_TIMEOUT_MS) {
1519
- const startedAt = Date.now();
1520
- while (Date.now() - startedAt < timeoutMs) {
1521
- const member = this.room.members.current();
1522
- if (member) {
1523
- return member;
1524
- }
1525
- await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
1526
- }
1527
- return this.room.members.current();
1528
- }
1529
- async enableAudio(constraints = true) {
1530
- const track = await this.createUserMediaTrack("audio", constraints);
1531
- if (!track) {
1532
- throw new Error("P2P transport could not create a local audio track.");
1533
- }
1534
- const providerSessionId = await this.ensureConnectedMemberId();
1535
- this.rememberLocalTrack("audio", track, track.getSettings().deviceId, true);
1536
- await this.room.media.audio.enable?.({
1537
- trackId: track.id,
1538
- deviceId: track.getSettings().deviceId,
1539
- providerSessionId
1540
- });
1541
- this.syncAllPeerSenders();
1542
- return track;
1543
- }
1544
- async enableVideo(constraints = true) {
1545
- const track = await this.createUserMediaTrack("video", constraints);
1546
- if (!track) {
1547
- throw new Error("P2P transport could not create a local video track.");
1548
- }
1549
- const providerSessionId = await this.ensureConnectedMemberId();
1550
- this.rememberLocalTrack("video", track, track.getSettings().deviceId, true);
1551
- await this.room.media.video.enable?.({
1552
- trackId: track.id,
1553
- deviceId: track.getSettings().deviceId,
1554
- providerSessionId
1555
- });
1556
- this.syncAllPeerSenders();
1557
- return track;
1558
- }
1559
- async startScreenShare(constraints = { video: true, audio: false }) {
1560
- const devices = await this.resolveMediaDevices();
1561
- if (!devices?.getDisplayMedia) {
1562
- throw new Error("Screen sharing is not available in this environment.");
1563
- }
1564
- const stream = await devices.getDisplayMedia(constraints);
1565
- const track = stream.getVideoTracks()[0] ?? null;
1566
- if (!track) {
1567
- throw new Error("P2P transport could not create a screen-share track.");
1568
- }
1569
- track.addEventListener("ended", () => {
1570
- void this.stopScreenShare();
1571
- }, { once: true });
1572
- const providerSessionId = await this.ensureConnectedMemberId();
1573
- this.rememberLocalTrack("screen", track, track.getSettings().deviceId, true);
1574
- await this.room.media.screen.start?.({
1575
- trackId: track.id,
1576
- deviceId: track.getSettings().deviceId,
1577
- providerSessionId
1578
- });
1579
- this.syncAllPeerSenders();
1580
- return track;
1581
- }
1582
- async disableAudio() {
1583
- this.releaseLocalTrack("audio");
1584
- this.syncAllPeerSenders();
1585
- await this.room.media.audio.disable();
1586
- }
1587
- async disableVideo() {
1588
- this.releaseLocalTrack("video");
1589
- this.syncAllPeerSenders();
1590
- await this.room.media.video.disable();
1591
- }
1592
- async stopScreenShare() {
1593
- this.releaseLocalTrack("screen");
1594
- this.syncAllPeerSenders();
1595
- await this.room.media.screen.stop();
1596
- }
1597
- async setMuted(kind, muted) {
1598
- const localTrack = this.localTracks.get(kind)?.track;
1599
- if (localTrack) {
1600
- localTrack.enabled = !muted;
1601
- }
1602
- if (kind === "audio") {
1603
- await this.room.media.audio.setMuted?.(muted);
1604
- } else {
1605
- await this.room.media.video.setMuted?.(muted);
1606
- }
1607
- }
1608
- async switchDevices(payload) {
1609
- if (payload.audioInputId && this.localTracks.has("audio")) {
1610
- const nextAudioTrack = await this.createUserMediaTrack("audio", buildExactDeviceConstraint(payload.audioInputId));
1611
- if (nextAudioTrack) {
1612
- this.rememberLocalTrack("audio", nextAudioTrack, payload.audioInputId, true);
1613
- }
1614
- }
1615
- if (payload.videoInputId && this.localTracks.has("video")) {
1616
- const nextVideoTrack = await this.createUserMediaTrack("video", buildExactDeviceConstraint(payload.videoInputId));
1617
- if (nextVideoTrack) {
1618
- this.rememberLocalTrack("video", nextVideoTrack, payload.videoInputId, true);
1619
- }
1620
- }
1621
- this.syncAllPeerSenders();
1622
- await this.room.media.devices.switch(payload);
1623
- }
1624
- onRemoteTrack(handler) {
1625
- this.remoteTrackHandlers.push(handler);
1626
- return createSubscription(() => {
1627
- const index = this.remoteTrackHandlers.indexOf(handler);
1628
- if (index >= 0) {
1629
- this.remoteTrackHandlers.splice(index, 1);
1630
- }
1631
- });
1632
- }
1633
- destroy() {
1634
- this.connected = false;
1635
- this.localMemberId = null;
1636
- for (const subscription of this.subscriptions.splice(0)) {
1637
- subscription.unsubscribe();
1638
- }
1639
- for (const peer of this.peers.values()) {
1640
- this.destroyPeer(peer);
1641
- }
1642
- this.peers.clear();
1643
- for (const kind of Array.from(this.localTracks.keys())) {
1644
- this.releaseLocalTrack(kind);
1645
- }
1646
- this.remoteTrackKinds.clear();
1647
- this.emittedRemoteTracks.clear();
1648
- this.pendingRemoteTracks.clear();
1649
- }
1650
- attachRoomSubscriptions() {
1651
- if (this.subscriptions.length > 0) {
1652
- return;
1653
- }
1654
- this.subscriptions.push(
1655
- this.room.members.onJoin((member) => {
1656
- if (member.memberId !== this.localMemberId) {
1657
- this.ensurePeer(member.memberId);
1658
- }
1659
- }),
1660
- this.room.members.onSync((members) => {
1661
- const activeMemberIds = /* @__PURE__ */ new Set();
1662
- for (const member of members) {
1663
- if (member.memberId !== this.localMemberId) {
1664
- activeMemberIds.add(member.memberId);
1665
- this.ensurePeer(member.memberId);
1666
- }
1667
- }
1668
- for (const memberId of Array.from(this.peers.keys())) {
1669
- if (!activeMemberIds.has(memberId)) {
1670
- this.removeRemoteMember(memberId);
1671
- }
1672
- }
1673
- }),
1674
- this.room.members.onLeave((member) => {
1675
- this.removeRemoteMember(member.memberId);
1676
- }),
1677
- this.room.signals.on(this.offerEvent, (payload, meta) => {
1678
- void this.handleDescriptionSignal("offer", payload, meta);
1679
- }),
1680
- this.room.signals.on(this.answerEvent, (payload, meta) => {
1681
- void this.handleDescriptionSignal("answer", payload, meta);
1682
- }),
1683
- this.room.signals.on(this.iceEvent, (payload, meta) => {
1684
- void this.handleIceSignal(payload, meta);
1685
- }),
1686
- this.room.media.onTrack((track, member) => {
1687
- if (member.memberId !== this.localMemberId) {
1688
- this.ensurePeer(member.memberId);
1689
- }
1690
- this.rememberRemoteTrackKind(track, member);
1691
- }),
1692
- this.room.media.onTrackRemoved((track, member) => {
1693
- if (!track.trackId) return;
1694
- const key = buildTrackKey(member.memberId, track.trackId);
1695
- this.remoteTrackKinds.delete(key);
1696
- this.emittedRemoteTracks.delete(key);
1697
- this.pendingRemoteTracks.delete(key);
1698
- })
1699
- );
1700
- }
1701
- hydrateRemoteTrackKinds() {
1702
- this.remoteTrackKinds.clear();
1703
- this.emittedRemoteTracks.clear();
1704
- this.pendingRemoteTracks.clear();
1705
- for (const mediaMember of this.room.media.list()) {
1706
- for (const track of mediaMember.tracks) {
1707
- this.rememberRemoteTrackKind(track, mediaMember.member);
1708
- }
1709
- }
1710
- }
1711
- rememberRemoteTrackKind(track, member) {
1712
- if (!track.trackId || member.memberId === this.localMemberId) {
1713
- return;
1714
- }
1715
- const key = buildTrackKey(member.memberId, track.trackId);
1716
- this.remoteTrackKinds.set(key, track.kind);
1717
- const pending = this.pendingRemoteTracks.get(key);
1718
- if (pending) {
1719
- this.pendingRemoteTracks.delete(key);
1720
- this.emitRemoteTrack(member.memberId, pending.track, pending.stream, track.kind);
1721
- return;
1722
- }
1723
- this.flushPendingRemoteTracks(member.memberId, track.kind);
1724
- }
1725
- ensurePeer(memberId) {
1726
- const existing = this.peers.get(memberId);
1727
- if (existing) {
1728
- this.syncPeerSenders(existing);
1729
- return existing;
1730
- }
1731
- const factory = this.options.peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
1732
- const pc = factory(this.options.rtcConfiguration);
1733
- const peer = {
1734
- memberId,
1735
- pc,
1736
- polite: !!this.localMemberId && this.localMemberId.localeCompare(memberId) > 0,
1737
- makingOffer: false,
1738
- ignoreOffer: false,
1739
- isSettingRemoteAnswerPending: false,
1740
- pendingCandidates: [],
1741
- senders: /* @__PURE__ */ new Map()
1742
- };
1743
- pc.onicecandidate = (event) => {
1744
- if (!event.candidate) return;
1745
- void this.room.signals.sendTo(memberId, this.iceEvent, {
1746
- candidate: serializeCandidate(event.candidate)
1747
- });
1748
- };
1749
- pc.onnegotiationneeded = () => {
1750
- void this.negotiatePeer(peer);
1751
- };
1752
- pc.ontrack = (event) => {
1753
- const stream = event.streams[0] ?? new MediaStream([event.track]);
1754
- const key = buildTrackKey(memberId, event.track.id);
1755
- const exactKind = this.remoteTrackKinds.get(key);
1756
- const fallbackKind = exactKind ? null : this.resolveFallbackRemoteTrackKind(memberId, event.track);
1757
- const kind = exactKind ?? fallbackKind ?? normalizeTrackKind(event.track);
1758
- if (!kind || !exactKind && !fallbackKind && kind === "video" && event.track.kind === "video") {
1759
- this.pendingRemoteTracks.set(key, { memberId, track: event.track, stream });
1760
- return;
1761
- }
1762
- this.emitRemoteTrack(memberId, event.track, stream, kind);
1763
- };
1764
- this.peers.set(memberId, peer);
1765
- this.syncPeerSenders(peer);
1766
- return peer;
1767
- }
1768
- async negotiatePeer(peer) {
1769
- if (!this.connected || peer.pc.connectionState === "closed" || peer.makingOffer || peer.isSettingRemoteAnswerPending || peer.pc.signalingState !== "stable") {
1770
- return;
1771
- }
1772
- try {
1773
- peer.makingOffer = true;
1774
- await peer.pc.setLocalDescription();
1775
- if (!peer.pc.localDescription) {
1776
- return;
1777
- }
1778
- await this.room.signals.sendTo(peer.memberId, this.offerEvent, {
1779
- description: serializeDescription(peer.pc.localDescription)
1780
- });
1781
- } catch (error) {
1782
- console.warn("[RoomP2PMediaTransport] Failed to negotiate peer offer.", {
1783
- memberId: peer.memberId,
1784
- signalingState: peer.pc.signalingState,
1785
- error
1786
- });
1787
- } finally {
1788
- peer.makingOffer = false;
1789
- }
1790
- }
1791
- async handleDescriptionSignal(expectedType, payload, meta) {
1792
- const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
1793
- if (!senderId || senderId === this.localMemberId) {
1794
- return;
1795
- }
1796
- const description = this.normalizeDescription(payload);
1797
- if (!description || description.type !== expectedType) {
1798
- return;
1799
- }
1800
- const peer = this.ensurePeer(senderId);
1801
- const readyForOffer = !peer.makingOffer && (peer.pc.signalingState === "stable" || peer.isSettingRemoteAnswerPending);
1802
- const offerCollision = description.type === "offer" && !readyForOffer;
1803
- peer.ignoreOffer = !peer.polite && offerCollision;
1804
- if (peer.ignoreOffer) {
1805
- return;
1806
- }
1807
- try {
1808
- peer.isSettingRemoteAnswerPending = description.type === "answer";
1809
- await peer.pc.setRemoteDescription(description);
1810
- peer.isSettingRemoteAnswerPending = false;
1811
- await this.flushPendingCandidates(peer);
1812
- if (description.type === "offer") {
1813
- this.syncPeerSenders(peer);
1814
- await peer.pc.setLocalDescription();
1815
- if (!peer.pc.localDescription) {
1816
- return;
1817
- }
1818
- await this.room.signals.sendTo(senderId, this.answerEvent, {
1819
- description: serializeDescription(peer.pc.localDescription)
1820
- });
1821
- }
1822
- } catch (error) {
1823
- console.warn("[RoomP2PMediaTransport] Failed to apply remote session description.", {
1824
- memberId: senderId,
1825
- expectedType,
1826
- signalingState: peer.pc.signalingState,
1827
- error
1828
- });
1829
- peer.isSettingRemoteAnswerPending = false;
1830
- }
1831
- }
1832
- async handleIceSignal(payload, meta) {
1833
- const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
1834
- if (!senderId || senderId === this.localMemberId) {
1835
- return;
1836
- }
1837
- const candidate = this.normalizeCandidate(payload);
1838
- if (!candidate) {
1839
- return;
1840
- }
1841
- const peer = this.ensurePeer(senderId);
1842
- if (!peer.pc.remoteDescription) {
1843
- peer.pendingCandidates.push(candidate);
1844
- return;
1845
- }
1846
- try {
1847
- await peer.pc.addIceCandidate(candidate);
1848
- } catch (error) {
1849
- console.warn("[RoomP2PMediaTransport] Failed to add ICE candidate.", {
1850
- memberId: senderId,
1851
- error
1852
- });
1853
- if (!peer.ignoreOffer) {
1854
- peer.pendingCandidates.push(candidate);
1855
- }
1856
- }
1857
- }
1858
- async flushPendingCandidates(peer) {
1859
- if (!peer.pc.remoteDescription || peer.pendingCandidates.length === 0) {
1860
- return;
1861
- }
1862
- const pending = [...peer.pendingCandidates];
1863
- peer.pendingCandidates.length = 0;
1864
- for (const candidate of pending) {
1865
- try {
1866
- await peer.pc.addIceCandidate(candidate);
1867
- } catch (error) {
1868
- console.warn("[RoomP2PMediaTransport] Failed to flush pending ICE candidate.", {
1869
- memberId: peer.memberId,
1870
- error
1871
- });
1872
- if (!peer.ignoreOffer) {
1873
- peer.pendingCandidates.push(candidate);
1874
- }
1875
- }
1876
- }
1877
- }
1878
- syncAllPeerSenders() {
1879
- for (const peer of this.peers.values()) {
1880
- this.syncPeerSenders(peer);
1881
- }
1882
- }
1883
- syncPeerSenders(peer) {
1884
- const activeKinds = /* @__PURE__ */ new Set();
1885
- let changed = false;
1886
- for (const [kind, localTrack] of this.localTracks.entries()) {
1887
- activeKinds.add(kind);
1888
- const sender = peer.senders.get(kind);
1889
- if (sender) {
1890
- if (sender.track !== localTrack.track) {
1891
- void sender.replaceTrack(localTrack.track);
1892
- changed = true;
1893
- }
1894
- continue;
1895
- }
1896
- const addedSender = peer.pc.addTrack(localTrack.track, new MediaStream([localTrack.track]));
1897
- peer.senders.set(kind, addedSender);
1898
- changed = true;
1899
- }
1900
- for (const [kind, sender] of Array.from(peer.senders.entries())) {
1901
- if (activeKinds.has(kind)) {
1902
- continue;
1903
- }
1904
- try {
1905
- peer.pc.removeTrack(sender);
1906
- } catch {
1907
- }
1908
- peer.senders.delete(kind);
1909
- changed = true;
1910
- }
1911
- if (changed) {
1912
- void this.negotiatePeer(peer);
1913
- }
1914
- }
1915
- emitRemoteTrack(memberId, track, stream, kind) {
1916
- const key = buildTrackKey(memberId, track.id);
1917
- if (this.emittedRemoteTracks.has(key)) {
1918
- return;
1919
- }
1920
- this.emittedRemoteTracks.add(key);
1921
- this.remoteTrackKinds.set(key, kind);
1922
- const participant = this.findMember(memberId);
1923
- const payload = {
1924
- kind,
1925
- track,
1926
- stream,
1927
- trackName: track.id,
1928
- providerSessionId: memberId,
1929
- participantId: memberId,
1930
- userId: participant?.userId
1931
- };
1932
- for (const handler of this.remoteTrackHandlers) {
1933
- handler(payload);
1934
- }
1935
- }
1936
- resolveFallbackRemoteTrackKind(memberId, track) {
1937
- const normalizedKind = normalizeTrackKind(track);
1938
- if (!normalizedKind) {
1939
- return null;
1940
- }
1941
- if (normalizedKind === "audio") {
1942
- return "audio";
1943
- }
1944
- return this.getNextUnassignedPublishedVideoLikeKind(memberId);
1945
- }
1946
- flushPendingRemoteTracks(memberId, roomKind) {
1947
- const expectedTrackKind = roomKind === "audio" ? "audio" : "video";
1948
- for (const [key, pending] of this.pendingRemoteTracks.entries()) {
1949
- if (pending.memberId !== memberId || pending.track.kind !== expectedTrackKind) {
1950
- continue;
1951
- }
1952
- this.pendingRemoteTracks.delete(key);
1953
- this.emitRemoteTrack(memberId, pending.track, pending.stream, roomKind);
1954
- return;
1955
- }
1956
- }
1957
- getPublishedVideoLikeKinds(memberId) {
1958
- const mediaMember = this.room.media.list().find((entry) => entry.member.memberId === memberId);
1959
- if (!mediaMember) {
1960
- return [];
1961
- }
1962
- const publishedKinds = /* @__PURE__ */ new Set();
1963
- for (const track of mediaMember.tracks) {
1964
- if ((track.kind === "video" || track.kind === "screen") && track.trackId) {
1965
- publishedKinds.add(track.kind);
1966
- }
1967
- }
1968
- return Array.from(publishedKinds);
1969
- }
1970
- getNextUnassignedPublishedVideoLikeKind(memberId) {
1971
- const publishedKinds = this.getPublishedVideoLikeKinds(memberId);
1972
- if (publishedKinds.length === 0) {
1973
- return null;
1974
- }
1975
- const assignedKinds = /* @__PURE__ */ new Set();
1976
- for (const key of this.emittedRemoteTracks) {
1977
- if (!key.startsWith(`${memberId}:`)) {
1978
- continue;
1979
- }
1980
- const kind = this.remoteTrackKinds.get(key);
1981
- if (kind === "video" || kind === "screen") {
1982
- assignedKinds.add(kind);
1983
- }
1984
- }
1985
- return publishedKinds.find((kind) => !assignedKinds.has(kind)) ?? null;
1986
- }
1987
- closePeer(memberId) {
1988
- const peer = this.peers.get(memberId);
1989
- if (!peer) return;
1990
- this.destroyPeer(peer);
1991
- this.peers.delete(memberId);
1992
- }
1993
- removeRemoteMember(memberId) {
1994
- this.remoteTrackKinds.forEach((_kind, key) => {
1995
- if (key.startsWith(`${memberId}:`)) {
1996
- this.remoteTrackKinds.delete(key);
1997
- }
1998
- });
1999
- this.emittedRemoteTracks.forEach((key) => {
2000
- if (key.startsWith(`${memberId}:`)) {
2001
- this.emittedRemoteTracks.delete(key);
2002
- }
2003
- });
2004
- this.pendingRemoteTracks.forEach((_pending, key) => {
2005
- if (key.startsWith(`${memberId}:`)) {
2006
- this.pendingRemoteTracks.delete(key);
2007
- }
2008
- });
2009
- this.closePeer(memberId);
2010
- }
2011
- findMember(memberId) {
2012
- return this.room.members.list().find((member) => member.memberId === memberId);
2013
- }
2014
- rollbackConnectedState() {
2015
- this.connected = false;
2016
- this.localMemberId = null;
2017
- for (const subscription of this.subscriptions.splice(0)) {
2018
- subscription.unsubscribe();
2019
- }
2020
- for (const peer of this.peers.values()) {
2021
- this.destroyPeer(peer);
2022
- }
2023
- this.peers.clear();
2024
- this.remoteTrackKinds.clear();
2025
- this.emittedRemoteTracks.clear();
2026
- this.pendingRemoteTracks.clear();
2027
- }
2028
- destroyPeer(peer) {
2029
- peer.pc.onicecandidate = null;
2030
- peer.pc.onnegotiationneeded = null;
2031
- peer.pc.ontrack = null;
2032
- try {
2033
- peer.pc.close();
2034
- } catch {
2035
- }
2036
- }
2037
- async createUserMediaTrack(kind, constraints) {
2038
- const devices = await this.resolveMediaDevices();
2039
- if (!devices?.getUserMedia || constraints === false) {
2040
- return null;
2041
- }
2042
- const stream = await devices.getUserMedia(
2043
- kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
2044
- );
2045
- return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
2046
- }
2047
- rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
2048
- this.releaseLocalTrack(kind);
2049
- this.localTracks.set(kind, {
2050
- kind,
2051
- track,
2052
- deviceId,
2053
- stopOnCleanup
2054
- });
2055
- }
2056
- releaseLocalTrack(kind) {
2057
- const local = this.localTracks.get(kind);
2058
- if (!local) return;
2059
- if (local.stopOnCleanup) {
2060
- local.track.stop();
2061
- }
2062
- this.localTracks.delete(kind);
2063
- }
2064
- async ensureConnectedMemberId() {
2065
- if (this.localMemberId) {
2066
- return this.localMemberId;
2067
- }
2068
- return this.connect();
2069
- }
2070
- normalizeDescription(payload) {
2071
- if (!payload || typeof payload !== "object") {
2072
- return null;
2073
- }
2074
- const raw = payload.description;
2075
- if (!raw || typeof raw.type !== "string") {
2076
- return null;
2077
- }
2078
- return {
2079
- type: raw.type,
2080
- sdp: typeof raw.sdp === "string" ? raw.sdp : void 0
2081
- };
2082
- }
2083
- normalizeCandidate(payload) {
2084
- if (!payload || typeof payload !== "object") {
2085
- return null;
2086
- }
2087
- const raw = payload.candidate;
2088
- if (!raw || typeof raw.candidate !== "string") {
2089
- return null;
2090
- }
2091
- return raw;
2092
- }
2093
- async ensureRuntime() {
2094
- this.runtimePromise ??= import('@cloudflare/react-native-webrtc').then((mod) => {
2095
- const runtime = mod;
2096
- runtime.registerGlobals?.();
2097
- return runtime;
2098
- }).catch((error) => {
2099
- throw new Error(`${installMessage2("@cloudflare/react-native-webrtc")}
2100
- ${String(error)}`);
2101
- });
2102
- return this.runtimePromise;
2103
- }
2104
- async resolveMediaDevices() {
2105
- if (this.options.mediaDevices) {
2106
- return this.options.mediaDevices;
2107
- }
2108
- const runtime = await this.ensureRuntime();
2109
- return runtime.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0);
2110
- }
2111
- get offerEvent() {
2112
- return `${this.options.signalPrefix}.offer`;
2113
- }
2114
- get answerEvent() {
2115
- return `${this.options.signalPrefix}.answer`;
2116
- }
2117
- get iceEvent() {
2118
- return `${this.options.signalPrefix}.ice`;
2119
- }
2120
- };
2121
-
2122
- // src/room.ts
2123
1019
  var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2124
1020
  function deepSet(obj, path, value) {
2125
1021
  const parts = path.split(".");
@@ -2157,18 +1053,21 @@ function cloneRecord(value) {
2157
1053
  var WS_CONNECTING = 0;
2158
1054
  var WS_OPEN = 1;
2159
1055
  var ROOM_EXPLICIT_LEAVE_CLOSE_CODE = 4005;
1056
+ var ROOM_AUTH_STATE_LOST_CLOSE_CODE = 4006;
2160
1057
  var ROOM_EXPLICIT_LEAVE_REASON = "Client left room";
2161
- var ROOM_EXPLICIT_LEAVE_CLOSE_DELAY_MS = 40;
1058
+ var ROOM_HEARTBEAT_INTERVAL_MS = 8e3;
1059
+ var ROOM_HEARTBEAT_STALE_TIMEOUT_MS = 2e4;
2162
1060
  function isSocketOpenOrConnecting(socket) {
2163
1061
  return !!socket && (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING);
2164
1062
  }
2165
1063
  function closeSocketAfterLeave(socket, reason) {
2166
- globalThis.setTimeout(() => {
2167
- try {
2168
- socket.close(ROOM_EXPLICIT_LEAVE_CLOSE_CODE, reason);
2169
- } catch {
2170
- }
2171
- }, ROOM_EXPLICIT_LEAVE_CLOSE_DELAY_MS);
1064
+ try {
1065
+ socket.close(ROOM_EXPLICIT_LEAVE_CLOSE_CODE, reason);
1066
+ } catch {
1067
+ }
1068
+ }
1069
+ function getDefaultHeartbeatStaleTimeoutMs(heartbeatIntervalMs) {
1070
+ return Math.max(Math.floor(heartbeatIntervalMs * 2.5), ROOM_HEARTBEAT_STALE_TIMEOUT_MS);
2172
1071
  }
2173
1072
  var RoomClient = class _RoomClient {
2174
1073
  baseUrl;
@@ -2184,7 +1083,7 @@ var RoomClient = class _RoomClient {
2184
1083
  _playerState = {};
2185
1084
  _playerVersion = 0;
2186
1085
  _members = [];
2187
- _mediaMembers = [];
1086
+ lastLocalMemberState = null;
2188
1087
  // ─── Connection ───
2189
1088
  ws = null;
2190
1089
  reconnectAttempts = 0;
@@ -2197,16 +1096,41 @@ var RoomClient = class _RoomClient {
2197
1096
  reconnectInfo = null;
2198
1097
  connectingPromise = null;
2199
1098
  heartbeatTimer = null;
1099
+ lastHeartbeatAckAt = Date.now();
1100
+ disconnectResetTimer = null;
2200
1101
  intentionallyLeft = false;
2201
1102
  waitingForAuth = false;
2202
1103
  joinRequested = false;
2203
1104
  unsubAuthState = null;
1105
+ browserNetworkListenersAttached = false;
1106
+ browserOfflineHandler = () => {
1107
+ if (this.intentionallyLeft || !this.joinRequested) {
1108
+ return;
1109
+ }
1110
+ if (this.connectionState === "connected") {
1111
+ this.setConnectionState("reconnecting");
1112
+ }
1113
+ if (isSocketOpenOrConnecting(this.ws)) {
1114
+ try {
1115
+ this.ws?.close();
1116
+ } catch {
1117
+ }
1118
+ }
1119
+ };
1120
+ browserOnlineHandler = () => {
1121
+ if (this.intentionallyLeft || !this.joinRequested || this.connectingPromise || isSocketOpenOrConnecting(this.ws)) {
1122
+ return;
1123
+ }
1124
+ if (this.connectionState === "reconnecting" || this.connectionState === "disconnected") {
1125
+ this.ensureConnection().catch(() => {
1126
+ });
1127
+ }
1128
+ };
2204
1129
  // ─── Pending send() requests (requestId → { resolve, reject, timeout }) ───
2205
1130
  pendingRequests = /* @__PURE__ */ new Map();
2206
1131
  pendingSignalRequests = /* @__PURE__ */ new Map();
2207
1132
  pendingAdminRequests = /* @__PURE__ */ new Map();
2208
1133
  pendingMemberStateRequests = /* @__PURE__ */ new Map();
2209
- pendingMediaRequests = /* @__PURE__ */ new Map();
2210
1134
  // ─── Subscriptions ───
2211
1135
  sharedStateHandlers = [];
2212
1136
  playerStateHandlers = [];
@@ -2216,16 +1140,14 @@ var RoomClient = class _RoomClient {
2216
1140
  errorHandlers = [];
2217
1141
  kickedHandlers = [];
2218
1142
  memberSyncHandlers = [];
1143
+ memberSnapshotHandlers = [];
2219
1144
  memberJoinHandlers = [];
2220
1145
  memberLeaveHandlers = [];
2221
1146
  memberStateHandlers = [];
2222
1147
  signalHandlers = /* @__PURE__ */ new Map();
2223
1148
  anySignalHandlers = [];
2224
- mediaTrackHandlers = [];
2225
- mediaTrackRemovedHandlers = [];
2226
- mediaStateHandlers = [];
2227
- mediaDeviceHandlers = [];
2228
1149
  reconnectHandlers = [];
1150
+ recoveryFailureHandlers = [];
2229
1151
  connectionStateHandlers = [];
2230
1152
  state = {
2231
1153
  getShared: () => this.getSharedState(),
@@ -2235,7 +1157,8 @@ var RoomClient = class _RoomClient {
2235
1157
  send: (actionType, payload) => this.send(actionType, payload)
2236
1158
  };
2237
1159
  meta = {
2238
- get: () => this.getMetadata()
1160
+ get: () => this.getMetadata(),
1161
+ summary: () => this.getSummary()
2239
1162
  };
2240
1163
  signals = {
2241
1164
  send: (event, payload, options) => this.sendSignal(event, payload, options),
@@ -2244,7 +1167,7 @@ var RoomClient = class _RoomClient {
2244
1167
  onAny: (handler) => this.onAnySignal(handler)
2245
1168
  };
2246
1169
  members = {
2247
- list: () => cloneValue(this._members),
1170
+ list: () => this._members.map((member) => cloneValue(member)),
2248
1171
  current: () => {
2249
1172
  const connectionId = this.currentConnectionId;
2250
1173
  if (connectionId) {
@@ -2260,7 +1183,9 @@ var RoomClient = class _RoomClient {
2260
1183
  const member = this._members.find((entry) => entry.userId === userId) ?? null;
2261
1184
  return member ? cloneValue(member) : null;
2262
1185
  },
1186
+ awaitCurrent: (timeoutMs = 1e4) => this.waitForCurrentMember(timeoutMs),
2263
1187
  onSync: (handler) => this.onMembersSync(handler),
1188
+ onSnapshot: (handler) => this.onMemberSnapshot(handler),
2264
1189
  onJoin: (handler) => this.onMemberJoin(handler),
2265
1190
  onLeave: (handler) => this.onMemberLeave(handler),
2266
1191
  setState: (state) => this.sendMemberState(state),
@@ -2269,53 +1194,16 @@ var RoomClient = class _RoomClient {
2269
1194
  };
2270
1195
  admin = {
2271
1196
  kick: (memberId) => this.sendAdmin("kick", memberId),
2272
- mute: (memberId) => this.sendAdmin("mute", memberId),
2273
1197
  block: (memberId) => this.sendAdmin("block", memberId),
2274
- setRole: (memberId, role) => this.sendAdmin("setRole", memberId, { role }),
2275
- disableVideo: (memberId) => this.sendAdmin("disableVideo", memberId),
2276
- stopScreenShare: (memberId) => this.sendAdmin("stopScreenShare", memberId)
2277
- };
2278
- media = {
2279
- list: () => cloneValue(this._mediaMembers),
2280
- audio: {
2281
- enable: (payload) => this.sendMedia("publish", "audio", payload),
2282
- disable: () => this.sendMedia("unpublish", "audio"),
2283
- setMuted: (muted) => this.sendMedia("mute", "audio", { muted })
2284
- },
2285
- video: {
2286
- enable: (payload) => this.sendMedia("publish", "video", payload),
2287
- disable: () => this.sendMedia("unpublish", "video"),
2288
- setMuted: (muted) => this.sendMedia("mute", "video", { muted })
2289
- },
2290
- screen: {
2291
- start: (payload) => this.sendMedia("publish", "screen", payload),
2292
- stop: () => this.sendMedia("unpublish", "screen")
2293
- },
2294
- devices: {
2295
- switch: (payload) => this.switchMediaDevices(payload)
2296
- },
2297
- cloudflareRealtimeKit: {
2298
- createSession: (payload) => this.requestCloudflareRealtimeKitMedia("session", "POST", payload)
2299
- },
2300
- transport: (options) => {
2301
- const provider = options?.provider ?? "cloudflare_realtimekit";
2302
- if (provider === "p2p") {
2303
- const p2pOptions = options?.p2p;
2304
- return new RoomP2PMediaTransport(this, p2pOptions);
2305
- }
2306
- const cloudflareOptions = options?.cloudflareRealtimeKit;
2307
- return new RoomCloudflareMediaTransport(this, cloudflareOptions);
2308
- },
2309
- onTrack: (handler) => this.onMediaTrack(handler),
2310
- onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
2311
- onStateChange: (handler) => this.onMediaStateChange(handler),
2312
- onDeviceChange: (handler) => this.onMediaDeviceChange(handler)
1198
+ setRole: (memberId, role) => this.sendAdmin("setRole", memberId, { role })
2313
1199
  };
2314
1200
  session = {
2315
1201
  onError: (handler) => this.onError(handler),
2316
1202
  onKicked: (handler) => this.onKicked(handler),
2317
1203
  onReconnect: (handler) => this.onReconnect(handler),
2318
- onConnectionStateChange: (handler) => this.onConnectionStateChange(handler)
1204
+ onConnectionStateChange: (handler) => this.onConnectionStateChange(handler),
1205
+ onRecoveryFailure: (handler) => this.onRecoveryFailure(handler),
1206
+ getDebugSnapshot: () => this.getDebugSnapshot()
2319
1207
  };
2320
1208
  constructor(baseUrl, namespace, roomId, tokenManager, options) {
2321
1209
  this.baseUrl = baseUrl;
@@ -2327,11 +1215,16 @@ var RoomClient = class _RoomClient {
2327
1215
  maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
2328
1216
  reconnectBaseDelay: options?.reconnectBaseDelay ?? 1e3,
2329
1217
  sendTimeout: options?.sendTimeout ?? 1e4,
2330
- connectionTimeout: options?.connectionTimeout ?? 15e3
1218
+ connectionTimeout: options?.connectionTimeout ?? 15e3,
1219
+ heartbeatIntervalMs: options?.heartbeatIntervalMs ?? ROOM_HEARTBEAT_INTERVAL_MS,
1220
+ heartbeatStaleTimeoutMs: options?.heartbeatStaleTimeoutMs ?? getDefaultHeartbeatStaleTimeoutMs(options?.heartbeatIntervalMs ?? ROOM_HEARTBEAT_INTERVAL_MS),
1221
+ networkRecoveryGraceMs: options?.networkRecoveryGraceMs ?? 3500,
1222
+ disconnectResetTimeoutMs: options?.disconnectResetTimeoutMs ?? 8e3
2331
1223
  };
2332
1224
  this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
2333
1225
  this.handleAuthStateChange(user);
2334
1226
  });
1227
+ this.attachBrowserNetworkListeners();
2335
1228
  }
2336
1229
  // ─── State Accessors ───
2337
1230
  /** Get current shared state (read-only snapshot) */
@@ -2342,71 +1235,128 @@ var RoomClient = class _RoomClient {
2342
1235
  getPlayerState() {
2343
1236
  return cloneRecord(this._playerState);
2344
1237
  }
1238
+ async waitForCurrentMember(timeoutMs = 1e4) {
1239
+ const startedAt = Date.now();
1240
+ while (Date.now() - startedAt < timeoutMs) {
1241
+ const member = this.members.current();
1242
+ if (member) {
1243
+ return member;
1244
+ }
1245
+ await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
1246
+ }
1247
+ return this.members.current();
1248
+ }
2345
1249
  // ─── Metadata (HTTP, no WebSocket needed) ───
2346
1250
  /**
2347
1251
  * Get room metadata without joining (HTTP GET).
2348
1252
  * Returns developer-defined metadata set by room.setMetadata() on the server.
2349
- */
2350
- async getMetadata() {
2351
- return _RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
2352
- }
2353
- async requestCloudflareRealtimeKitMedia(path, method, payload) {
2354
- return this.requestRoomMedia("cloudflare_realtimekit", path, method, payload);
2355
- }
2356
- async requestRoomMedia(providerPath, path, method, payload) {
2357
- const token = await this.tokenManager.getAccessToken(
2358
- (refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
2359
- );
2360
- if (!token) {
2361
- throw new EdgeBaseError(401, "Authentication required before calling room media APIs. Sign in and join the room first.");
2362
- }
2363
- const url = new URL(`${this.baseUrl.replace(/\/$/, "")}/api/room/media/${providerPath}/${path}`);
2364
- url.searchParams.set("namespace", this.namespace);
2365
- url.searchParams.set("id", this.roomId);
2366
- const response = await fetch(url.toString(), {
2367
- method,
2368
- headers: {
2369
- Authorization: `Bearer ${token}`,
2370
- "Content-Type": "application/json"
2371
- },
2372
- body: method === "GET" ? void 0 : JSON.stringify(payload ?? {})
2373
- });
2374
- const data = await response.json().catch(() => ({}));
2375
- if (!response.ok) {
2376
- throw new EdgeBaseError(
2377
- response.status,
2378
- typeof data.message === "string" && data.message || `Room media request failed: ${response.statusText}`
2379
- );
2380
- }
2381
- return data;
1253
+ */
1254
+ async getMetadata() {
1255
+ return _RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
1256
+ }
1257
+ async getSummary() {
1258
+ return _RoomClient.getSummary(this.baseUrl, this.namespace, this.roomId);
1259
+ }
1260
+ async checkConnection() {
1261
+ return _RoomClient.checkConnection(this.baseUrl, this.namespace, this.roomId);
2382
1262
  }
2383
1263
  /**
2384
1264
  * Static: Get room metadata without creating a RoomClient instance.
2385
1265
  * Useful for lobby screens where you need room info before joining.
2386
1266
  */
2387
1267
  static async getMetadata(baseUrl, namespace, roomId) {
2388
- const url = `${baseUrl.replace(/\/$/, "")}/api/room/metadata?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
1268
+ return _RoomClient.requestPublicRoomResource(
1269
+ baseUrl,
1270
+ "metadata",
1271
+ namespace,
1272
+ roomId,
1273
+ "Failed to get room metadata"
1274
+ );
1275
+ }
1276
+ static async getSummary(baseUrl, namespace, roomId) {
1277
+ return _RoomClient.requestPublicRoomResource(
1278
+ baseUrl,
1279
+ "summary",
1280
+ namespace,
1281
+ roomId,
1282
+ "Failed to get room summary"
1283
+ );
1284
+ }
1285
+ static async getSummaries(baseUrl, namespace, roomIds) {
1286
+ const url = `${baseUrl.replace(/\/$/, "")}/api/room/summaries`;
2389
1287
  let res;
2390
1288
  try {
2391
- res = await fetch(url);
1289
+ res = await fetch(url, {
1290
+ method: "POST",
1291
+ headers: { "Content-Type": "application/json" },
1292
+ body: JSON.stringify({ namespace, ids: roomIds })
1293
+ });
2392
1294
  } catch (error) {
2393
1295
  throw networkError(
2394
- `Room metadata request could not reach ${url}. Make sure the EdgeBase server is running and reachable.`,
1296
+ `Failed to get room summaries. Could not reach ${baseUrl.replace(/\/$/, "")}. Make sure the EdgeBase server is running and reachable.`,
2395
1297
  { cause: error }
2396
1298
  );
2397
1299
  }
2398
1300
  const data = await res.json().catch(() => null);
2399
1301
  if (!res.ok) {
2400
1302
  const parsed = parseErrorResponse(res.status, data);
2401
- if (data && typeof data === "object" && typeof data.message === "string") {
2402
- throw parsed;
2403
- }
1303
+ parsed.message = `Failed to get room summaries: ${parsed.message}`;
1304
+ throw parsed;
1305
+ }
1306
+ return data;
1307
+ }
1308
+ static async checkConnection(baseUrl, namespace, roomId) {
1309
+ const url = `${baseUrl.replace(/\/$/, "")}/api/room/connect-check?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
1310
+ let response;
1311
+ try {
1312
+ response = await fetch(url);
1313
+ } catch (error) {
1314
+ throw networkError(
1315
+ `Room connect-check could not reach ${baseUrl.replace(/\/$/, "")}. Make sure the EdgeBase server is running and reachable.`,
1316
+ { cause: error }
1317
+ );
1318
+ }
1319
+ const data = await response.json().catch(() => null);
1320
+ if (!response.ok) {
1321
+ throw parseErrorResponse(response.status, data);
1322
+ }
1323
+ if (typeof data?.ok !== "boolean" || typeof data?.type !== "string" || typeof data?.category !== "string" || typeof data?.message !== "string") {
2404
1324
  throw new EdgeBaseError(
2405
- res.status,
2406
- `Failed to load room metadata for '${roomId}' in namespace '${namespace}'. ${parsed.message}`
1325
+ response.status || 500,
1326
+ "Room connect-check returned an unexpected response. The EdgeBase server and SDK may be out of sync."
1327
+ );
1328
+ }
1329
+ const diagnostic = data;
1330
+ return {
1331
+ ok: diagnostic.ok,
1332
+ type: diagnostic.type,
1333
+ category: diagnostic.category,
1334
+ message: diagnostic.message,
1335
+ namespace: typeof diagnostic.namespace === "string" ? diagnostic.namespace : void 0,
1336
+ roomId: typeof diagnostic.roomId === "string" ? diagnostic.roomId : void 0,
1337
+ runtime: typeof diagnostic.runtime === "string" ? diagnostic.runtime : void 0,
1338
+ pendingCount: typeof diagnostic.pendingCount === "number" ? diagnostic.pendingCount : void 0,
1339
+ maxPending: typeof diagnostic.maxPending === "number" ? diagnostic.maxPending : void 0
1340
+ };
1341
+ }
1342
+ static async requestPublicRoomResource(baseUrl, resource, namespace, roomId, failureMessage) {
1343
+ const url = `${baseUrl.replace(/\/$/, "")}/api/room/${resource}?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
1344
+ let res;
1345
+ try {
1346
+ res = await fetch(url);
1347
+ } catch (error) {
1348
+ throw networkError(
1349
+ `${failureMessage}. Could not reach ${baseUrl.replace(/\/$/, "")}. Make sure the EdgeBase server is running and reachable.`,
1350
+ { cause: error }
2407
1351
  );
2408
1352
  }
2409
- return data ?? {};
1353
+ if (!res.ok) {
1354
+ const data = await res.json().catch(() => null);
1355
+ const parsed = parseErrorResponse(res.status, data);
1356
+ parsed.message = `${failureMessage}: ${parsed.message}`;
1357
+ throw parsed;
1358
+ }
1359
+ return res.json();
2410
1360
  }
2411
1361
  // ─── Connection Lifecycle ───
2412
1362
  /** Connect to the room, authenticate, and join */
@@ -2425,15 +1375,8 @@ var RoomClient = class _RoomClient {
2425
1375
  this.joinRequested = false;
2426
1376
  this.waitingForAuth = false;
2427
1377
  this.stopHeartbeat();
2428
- for (const [, pending] of this.pendingRequests) {
2429
- clearTimeout(pending.timeout);
2430
- pending.reject(new EdgeBaseError(499, "Room left"));
2431
- }
2432
- this.pendingRequests.clear();
2433
- this.rejectPendingVoidRequests(this.pendingSignalRequests, new EdgeBaseError(499, "Room left"));
2434
- this.rejectPendingVoidRequests(this.pendingAdminRequests, new EdgeBaseError(499, "Room left"));
2435
- this.rejectPendingVoidRequests(this.pendingMemberStateRequests, new EdgeBaseError(499, "Room left"));
2436
- this.rejectPendingVoidRequests(this.pendingMediaRequests, new EdgeBaseError(499, "Room left"));
1378
+ this.clearDisconnectResetTimer();
1379
+ this.rejectAllPendingRequests(new EdgeBaseError(499, "Room left"));
2437
1380
  if (this.ws) {
2438
1381
  const socket = this.ws;
2439
1382
  this.sendRaw({ type: "leave" });
@@ -2449,15 +1392,28 @@ var RoomClient = class _RoomClient {
2449
1392
  this._playerState = {};
2450
1393
  this._playerVersion = 0;
2451
1394
  this._members = [];
2452
- this._mediaMembers = [];
1395
+ this.lastLocalMemberState = null;
2453
1396
  this.currentUserId = null;
2454
1397
  this.currentConnectionId = null;
2455
1398
  this.reconnectInfo = null;
2456
1399
  this.setConnectionState("disconnected");
2457
1400
  }
2458
- /** Destroy the room client, cleaning up all listeners and the auth subscription. */
1401
+ assertConnected(action) {
1402
+ if (!this.ws || !this.connected || !this.authenticated) {
1403
+ throw new EdgeBaseError(
1404
+ 400,
1405
+ `Room connection required before ${action}. Call room.join() and wait for the connection to finish.`
1406
+ );
1407
+ }
1408
+ }
1409
+ /**
1410
+ * Destroy the RoomClient and release all resources.
1411
+ * Calls leave() if still connected, unsubscribes from auth state changes,
1412
+ * and clears all handler arrays to allow garbage collection.
1413
+ */
2459
1414
  destroy() {
2460
1415
  this.leave();
1416
+ this.detachBrowserNetworkListeners();
2461
1417
  this.unsubAuthState?.();
2462
1418
  this.unsubAuthState = null;
2463
1419
  this.sharedStateHandlers.length = 0;
@@ -2472,10 +1428,6 @@ var RoomClient = class _RoomClient {
2472
1428
  this.memberStateHandlers.length = 0;
2473
1429
  this.signalHandlers.clear();
2474
1430
  this.anySignalHandlers.length = 0;
2475
- this.mediaTrackHandlers.length = 0;
2476
- this.mediaTrackRemovedHandlers.length = 0;
2477
- this.mediaStateHandlers.length = 0;
2478
- this.mediaDeviceHandlers.length = 0;
2479
1431
  this.reconnectHandlers.length = 0;
2480
1432
  this.connectionStateHandlers.length = 0;
2481
1433
  }
@@ -2488,9 +1440,7 @@ var RoomClient = class _RoomClient {
2488
1440
  * const result = await room.send('SET_SCORE', { score: 42 });
2489
1441
  */
2490
1442
  async send(actionType, payload) {
2491
- if (!this.ws || !this.connected || !this.authenticated) {
2492
- throw new EdgeBaseError(400, "Not connected to room. Call room.join() and wait for the room to connect before sending actions, signals, or media.");
2493
- }
1443
+ this.assertConnected(`sending action '${actionType}'`);
2494
1444
  const requestId = generateRequestId();
2495
1445
  return new Promise((resolve, reject) => {
2496
1446
  const timeout = setTimeout(() => {
@@ -2608,6 +1558,13 @@ var RoomClient = class _RoomClient {
2608
1558
  if (index >= 0) this.memberSyncHandlers.splice(index, 1);
2609
1559
  });
2610
1560
  }
1561
+ onMemberSnapshot(handler) {
1562
+ this.memberSnapshotHandlers.push(handler);
1563
+ return createSubscription(() => {
1564
+ const index = this.memberSnapshotHandlers.indexOf(handler);
1565
+ if (index >= 0) this.memberSnapshotHandlers.splice(index, 1);
1566
+ });
1567
+ }
2611
1568
  onMemberJoin(handler) {
2612
1569
  this.memberJoinHandlers.push(handler);
2613
1570
  return createSubscription(() => {
@@ -2636,6 +1593,13 @@ var RoomClient = class _RoomClient {
2636
1593
  if (index >= 0) this.reconnectHandlers.splice(index, 1);
2637
1594
  });
2638
1595
  }
1596
+ onRecoveryFailure(handler) {
1597
+ this.recoveryFailureHandlers.push(handler);
1598
+ return createSubscription(() => {
1599
+ const index = this.recoveryFailureHandlers.indexOf(handler);
1600
+ if (index >= 0) this.recoveryFailureHandlers.splice(index, 1);
1601
+ });
1602
+ }
2639
1603
  onConnectionStateChange(handler) {
2640
1604
  this.connectionStateHandlers.push(handler);
2641
1605
  return createSubscription(() => {
@@ -2643,38 +1607,8 @@ var RoomClient = class _RoomClient {
2643
1607
  if (index >= 0) this.connectionStateHandlers.splice(index, 1);
2644
1608
  });
2645
1609
  }
2646
- onMediaTrack(handler) {
2647
- this.mediaTrackHandlers.push(handler);
2648
- return createSubscription(() => {
2649
- const index = this.mediaTrackHandlers.indexOf(handler);
2650
- if (index >= 0) this.mediaTrackHandlers.splice(index, 1);
2651
- });
2652
- }
2653
- onMediaTrackRemoved(handler) {
2654
- this.mediaTrackRemovedHandlers.push(handler);
2655
- return createSubscription(() => {
2656
- const index = this.mediaTrackRemovedHandlers.indexOf(handler);
2657
- if (index >= 0) this.mediaTrackRemovedHandlers.splice(index, 1);
2658
- });
2659
- }
2660
- onMediaStateChange(handler) {
2661
- this.mediaStateHandlers.push(handler);
2662
- return createSubscription(() => {
2663
- const index = this.mediaStateHandlers.indexOf(handler);
2664
- if (index >= 0) this.mediaStateHandlers.splice(index, 1);
2665
- });
2666
- }
2667
- onMediaDeviceChange(handler) {
2668
- this.mediaDeviceHandlers.push(handler);
2669
- return createSubscription(() => {
2670
- const index = this.mediaDeviceHandlers.indexOf(handler);
2671
- if (index >= 0) this.mediaDeviceHandlers.splice(index, 1);
2672
- });
2673
- }
2674
1610
  async sendSignal(event, payload, options) {
2675
- if (!this.ws || !this.connected || !this.authenticated) {
2676
- throw new EdgeBaseError(400, "Not connected to room. Call room.join() and wait for the room to connect before sending actions, signals, or media.");
2677
- }
1611
+ this.assertConnected(`sending signal '${event}'`);
2678
1612
  const requestId = generateRequestId();
2679
1613
  return new Promise((resolve, reject) => {
2680
1614
  const timeout = setTimeout(() => {
@@ -2693,34 +1627,39 @@ var RoomClient = class _RoomClient {
2693
1627
  });
2694
1628
  }
2695
1629
  async sendMemberState(state) {
1630
+ const nextState = {
1631
+ ...this.lastLocalMemberState ?? {},
1632
+ ...cloneRecord(state)
1633
+ };
2696
1634
  return this.sendMemberStateRequest({
2697
1635
  type: "member_state",
2698
1636
  state
1637
+ }, () => {
1638
+ this.lastLocalMemberState = nextState;
2699
1639
  });
2700
1640
  }
2701
1641
  async clearMemberState() {
1642
+ const clearedState = {};
2702
1643
  return this.sendMemberStateRequest({
2703
1644
  type: "member_state_clear"
1645
+ }, () => {
1646
+ this.lastLocalMemberState = clearedState;
2704
1647
  });
2705
1648
  }
2706
- async sendMemberStateRequest(payload) {
2707
- if (!this.ws || !this.connected || !this.authenticated) {
2708
- throw new EdgeBaseError(400, "Not connected to room. Call room.join() and wait for the room to connect before sending actions, signals, or media.");
2709
- }
1649
+ async sendMemberStateRequest(payload, onSuccess) {
1650
+ this.assertConnected("updating member state");
2710
1651
  const requestId = generateRequestId();
2711
1652
  return new Promise((resolve, reject) => {
2712
1653
  const timeout = setTimeout(() => {
2713
1654
  this.pendingMemberStateRequests.delete(requestId);
2714
1655
  reject(new EdgeBaseError(408, "Member state update timed out"));
2715
1656
  }, this.options.sendTimeout);
2716
- this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout });
1657
+ this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout, onSuccess });
2717
1658
  this.sendRaw({ ...payload, requestId });
2718
1659
  });
2719
1660
  }
2720
1661
  async sendAdmin(operation, memberId, payload) {
2721
- if (!this.ws || !this.connected || !this.authenticated) {
2722
- throw new EdgeBaseError(400, "Not connected to room. Call room.join() and wait for the room to connect before sending actions, signals, or media.");
2723
- }
1662
+ this.assertConnected(`running admin operation '${operation}'`);
2724
1663
  const requestId = generateRequestId();
2725
1664
  return new Promise((resolve, reject) => {
2726
1665
  const timeout = setTimeout(() => {
@@ -2737,39 +1676,6 @@ var RoomClient = class _RoomClient {
2737
1676
  });
2738
1677
  });
2739
1678
  }
2740
- async sendMedia(operation, kind, payload) {
2741
- if (!this.ws || !this.connected || !this.authenticated) {
2742
- throw new EdgeBaseError(400, "Not connected to room. Call room.join() and wait for the room to connect before sending actions, signals, or media.");
2743
- }
2744
- const requestId = generateRequestId();
2745
- return new Promise((resolve, reject) => {
2746
- const timeout = setTimeout(() => {
2747
- this.pendingMediaRequests.delete(requestId);
2748
- reject(new EdgeBaseError(408, `Media operation '${operation}' timed out`));
2749
- }, this.options.sendTimeout);
2750
- this.pendingMediaRequests.set(requestId, { resolve, reject, timeout });
2751
- this.sendRaw({
2752
- type: "media",
2753
- operation,
2754
- kind,
2755
- payload: payload ?? {},
2756
- requestId
2757
- });
2758
- });
2759
- }
2760
- async switchMediaDevices(payload) {
2761
- const operations = [];
2762
- if (payload.audioInputId) {
2763
- operations.push(this.sendMedia("device", "audio", { deviceId: payload.audioInputId }));
2764
- }
2765
- if (payload.videoInputId) {
2766
- operations.push(this.sendMedia("device", "video", { deviceId: payload.videoInputId }));
2767
- }
2768
- if (payload.screenInputId) {
2769
- operations.push(this.sendMedia("device", "screen", { deviceId: payload.screenInputId }));
2770
- }
2771
- await Promise.all(operations);
2772
- }
2773
1679
  // ─── Private: Connection ───
2774
1680
  async establishConnection() {
2775
1681
  return new Promise((resolve, reject) => {
@@ -2817,11 +1723,16 @@ var RoomClient = class _RoomClient {
2817
1723
  this.joined = false;
2818
1724
  this.ws = null;
2819
1725
  this.stopHeartbeat();
2820
- if (event.code === 4004 && this.connectionState !== "kicked") {
2821
- this.handleKicked();
1726
+ const closeMessage = event.reason?.trim() ? `Room authentication lost: ${event.reason}` : "Room authentication lost";
1727
+ const closeError = event.code === ROOM_AUTH_STATE_LOST_CLOSE_CODE ? new EdgeBaseError(401, closeMessage) : new EdgeBaseError(499, "WebSocket connection lost");
1728
+ if (event.code === ROOM_AUTH_STATE_LOST_CLOSE_CODE && this.connectionState !== "auth_lost") {
1729
+ this.setConnectionState("auth_lost");
2822
1730
  }
2823
1731
  if (!this.intentionallyLeft) {
2824
- this.rejectAllPendingRequests(new EdgeBaseError(499, "WebSocket connection lost"));
1732
+ this.rejectAllPendingRequests(closeError);
1733
+ }
1734
+ if (event.code === 4004 && this.connectionState !== "kicked") {
1735
+ this.handleKicked();
2825
1736
  }
2826
1737
  if (!this.intentionallyLeft && !this.waitingForAuth && this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
2827
1738
  this.scheduleReconnect();
@@ -2855,11 +1766,19 @@ var RoomClient = class _RoomClient {
2855
1766
  (refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
2856
1767
  );
2857
1768
  if (!token) {
2858
- throw new EdgeBaseError(401, "No access token available. Sign in first.");
1769
+ throw new EdgeBaseError(
1770
+ 401,
1771
+ "Room authentication requires a signed-in session. Sign in before joining the room."
1772
+ );
2859
1773
  }
2860
1774
  return new Promise((resolve, reject) => {
2861
1775
  const timeout = setTimeout(() => {
2862
- reject(new EdgeBaseError(401, "Room auth timeout"));
1776
+ reject(
1777
+ new EdgeBaseError(
1778
+ 401,
1779
+ "Room authentication timed out. Check the server auth response and room connectivity."
1780
+ )
1781
+ );
2863
1782
  }, 1e4);
2864
1783
  const originalOnMessage = this.ws?.onmessage;
2865
1784
  if (this.ws) {
@@ -2876,7 +1795,8 @@ var RoomClient = class _RoomClient {
2876
1795
  lastSharedState: this._sharedState,
2877
1796
  lastSharedVersion: this._sharedVersion,
2878
1797
  lastPlayerState: this._playerState,
2879
- lastPlayerVersion: this._playerVersion
1798
+ lastPlayerVersion: this._playerVersion,
1799
+ lastMemberState: this.getReconnectMemberState()
2880
1800
  });
2881
1801
  this.joined = true;
2882
1802
  resolve();
@@ -2933,9 +1853,6 @@ var RoomClient = class _RoomClient {
2933
1853
  case "members_sync":
2934
1854
  this.handleMembersSync(msg);
2935
1855
  break;
2936
- case "media_sync":
2937
- this.handleMediaSync(msg);
2938
- break;
2939
1856
  case "member_join":
2940
1857
  this.handleMemberJoinFrame(msg);
2941
1858
  break;
@@ -2948,24 +1865,6 @@ var RoomClient = class _RoomClient {
2948
1865
  case "member_state_error":
2949
1866
  this.handleMemberStateError(msg);
2950
1867
  break;
2951
- case "media_track":
2952
- this.handleMediaTrackFrame(msg);
2953
- break;
2954
- case "media_track_removed":
2955
- this.handleMediaTrackRemovedFrame(msg);
2956
- break;
2957
- case "media_state":
2958
- this.handleMediaStateFrame(msg);
2959
- break;
2960
- case "media_device":
2961
- this.handleMediaDeviceFrame(msg);
2962
- break;
2963
- case "media_result":
2964
- this.handleMediaResult(msg);
2965
- break;
2966
- case "media_error":
2967
- this.handleMediaError(msg);
2968
- break;
2969
1868
  case "admin_result":
2970
1869
  this.handleAdminResult(msg);
2971
1870
  break;
@@ -2978,6 +1877,9 @@ var RoomClient = class _RoomClient {
2978
1877
  case "error":
2979
1878
  this.handleError(msg);
2980
1879
  break;
1880
+ case "pong":
1881
+ this.lastHeartbeatAckAt = Date.now();
1882
+ break;
2981
1883
  }
2982
1884
  }
2983
1885
  handleSync(msg) {
@@ -3093,22 +1995,18 @@ var RoomClient = class _RoomClient {
3093
1995
  handleMembersSync(msg) {
3094
1996
  const members = this.normalizeMembers(msg.members);
3095
1997
  this._members = members;
3096
- for (const member of members) {
3097
- this.syncMediaMemberInfo(member);
3098
- }
3099
- const snapshot = cloneValue(this._members);
1998
+ const snapshot = this._members.map((member) => cloneValue(member));
3100
1999
  for (const handler of this.memberSyncHandlers) {
3101
2000
  handler(snapshot);
3102
2001
  }
3103
- }
3104
- handleMediaSync(msg) {
3105
- this._mediaMembers = this.normalizeMediaMembers(msg.members);
2002
+ for (const handler of this.memberSnapshotHandlers) {
2003
+ handler(snapshot);
2004
+ }
3106
2005
  }
3107
2006
  handleMemberJoinFrame(msg) {
3108
2007
  const member = this.normalizeMember(msg.member);
3109
2008
  if (!member) return;
3110
2009
  this.upsertMember(member);
3111
- this.syncMediaMemberInfo(member);
3112
2010
  const snapshot = cloneValue(member);
3113
2011
  for (const handler of this.memberJoinHandlers) {
3114
2012
  handler(snapshot);
@@ -3118,7 +2016,6 @@ var RoomClient = class _RoomClient {
3118
2016
  const member = this.normalizeMember(msg.member);
3119
2017
  if (!member) return;
3120
2018
  this.removeMember(member.memberId);
3121
- this.removeMediaMember(member.memberId);
3122
2019
  const reason = this.normalizeLeaveReason(msg.reason);
3123
2020
  const snapshot = cloneValue(member);
3124
2021
  for (const handler of this.memberLeaveHandlers) {
@@ -3131,13 +2028,13 @@ var RoomClient = class _RoomClient {
3131
2028
  if (!member) return;
3132
2029
  member.state = state;
3133
2030
  this.upsertMember(member);
3134
- this.syncMediaMemberInfo(member);
3135
2031
  const requestId = msg.requestId;
3136
- if (requestId && member.memberId === this.currentUserId) {
2032
+ if (requestId) {
3137
2033
  const pending = this.pendingMemberStateRequests.get(requestId);
3138
2034
  if (pending) {
3139
2035
  clearTimeout(pending.timeout);
3140
2036
  this.pendingMemberStateRequests.delete(requestId);
2037
+ pending.onSuccess?.();
3141
2038
  pending.resolve();
3142
2039
  }
3143
2040
  }
@@ -3156,93 +2053,6 @@ var RoomClient = class _RoomClient {
3156
2053
  this.pendingMemberStateRequests.delete(requestId);
3157
2054
  pending.reject(new EdgeBaseError(400, msg.message || "Member state update failed"));
3158
2055
  }
3159
- handleMediaTrackFrame(msg) {
3160
- const member = this.normalizeMember(msg.member);
3161
- const track = this.normalizeMediaTrack(msg.track);
3162
- if (!member || !track) return;
3163
- const mediaMember = this.ensureMediaMember(member);
3164
- this.upsertMediaTrack(mediaMember, track);
3165
- this.mergeMediaState(mediaMember, track.kind, {
3166
- published: true,
3167
- muted: track.muted,
3168
- trackId: track.trackId,
3169
- deviceId: track.deviceId,
3170
- publishedAt: track.publishedAt,
3171
- adminDisabled: track.adminDisabled
3172
- });
3173
- const memberSnapshot = cloneValue(mediaMember.member);
3174
- const trackSnapshot = cloneValue(track);
3175
- for (const handler of this.mediaTrackHandlers) {
3176
- handler(trackSnapshot, memberSnapshot);
3177
- }
3178
- }
3179
- handleMediaTrackRemovedFrame(msg) {
3180
- const member = this.normalizeMember(msg.member);
3181
- const track = this.normalizeMediaTrack(msg.track);
3182
- if (!member || !track) return;
3183
- const mediaMember = this.ensureMediaMember(member);
3184
- this.removeMediaTrack(mediaMember, track);
3185
- mediaMember.state = {
3186
- ...mediaMember.state,
3187
- [track.kind]: {
3188
- published: false,
3189
- muted: false,
3190
- adminDisabled: false
3191
- }
3192
- };
3193
- const memberSnapshot = cloneValue(mediaMember.member);
3194
- const trackSnapshot = cloneValue(track);
3195
- for (const handler of this.mediaTrackRemovedHandlers) {
3196
- handler(trackSnapshot, memberSnapshot);
3197
- }
3198
- }
3199
- handleMediaStateFrame(msg) {
3200
- const member = this.normalizeMember(msg.member);
3201
- if (!member) return;
3202
- const mediaMember = this.ensureMediaMember(member);
3203
- mediaMember.state = this.normalizeMediaState(msg.state);
3204
- const memberSnapshot = cloneValue(mediaMember.member);
3205
- const stateSnapshot = cloneValue(mediaMember.state);
3206
- for (const handler of this.mediaStateHandlers) {
3207
- handler(memberSnapshot, stateSnapshot);
3208
- }
3209
- }
3210
- handleMediaDeviceFrame(msg) {
3211
- const member = this.normalizeMember(msg.member);
3212
- const kind = this.normalizeMediaKind(msg.kind);
3213
- const deviceId = typeof msg.deviceId === "string" ? msg.deviceId : "";
3214
- if (!member || !kind || !deviceId) return;
3215
- const mediaMember = this.ensureMediaMember(member);
3216
- this.mergeMediaState(mediaMember, kind, { deviceId });
3217
- for (const track of mediaMember.tracks) {
3218
- if (track.kind === kind) {
3219
- track.deviceId = deviceId;
3220
- }
3221
- }
3222
- const memberSnapshot = cloneValue(mediaMember.member);
3223
- const change = { kind, deviceId };
3224
- for (const handler of this.mediaDeviceHandlers) {
3225
- handler(memberSnapshot, change);
3226
- }
3227
- }
3228
- handleMediaResult(msg) {
3229
- const requestId = msg.requestId;
3230
- if (!requestId) return;
3231
- const pending = this.pendingMediaRequests.get(requestId);
3232
- if (!pending) return;
3233
- clearTimeout(pending.timeout);
3234
- this.pendingMediaRequests.delete(requestId);
3235
- pending.resolve();
3236
- }
3237
- handleMediaError(msg) {
3238
- const requestId = msg.requestId;
3239
- if (!requestId) return;
3240
- const pending = this.pendingMediaRequests.get(requestId);
3241
- if (!pending) return;
3242
- clearTimeout(pending.timeout);
3243
- this.pendingMediaRequests.delete(requestId);
3244
- pending.reject(new EdgeBaseError(400, msg.message || "Media operation failed"));
3245
- }
3246
2056
  handleAdminResult(msg) {
3247
2057
  const requestId = msg.requestId;
3248
2058
  if (!requestId) return;
@@ -3268,8 +2078,13 @@ var RoomClient = class _RoomClient {
3268
2078
  this.setConnectionState("kicked");
3269
2079
  }
3270
2080
  handleError(msg) {
2081
+ const code = typeof msg.code === "string" ? msg.code : "";
2082
+ const message = typeof msg.message === "string" ? msg.message : "";
2083
+ if (this.shouldTreatErrorAsAuthLoss(code)) {
2084
+ this.handleRoomAuthStateLoss(message);
2085
+ }
3271
2086
  for (const handler of this.errorHandlers) {
3272
- handler({ code: msg.code, message: msg.message });
2087
+ handler({ code, message });
3273
2088
  }
3274
2089
  }
3275
2090
  refreshAuth() {
@@ -3278,9 +2093,6 @@ var RoomClient = class _RoomClient {
3278
2093
  this.sendRaw({ type: "auth", token });
3279
2094
  }
3280
2095
  handleAuthStateChange(user) {
3281
- if (user === null) {
3282
- this.rejectAllPendingRequests(new EdgeBaseError(401, "Auth state lost"));
3283
- }
3284
2096
  if (user) {
3285
2097
  if (this.ws && this.connected && this.authenticated) {
3286
2098
  this.refreshAuth();
@@ -3297,6 +2109,7 @@ var RoomClient = class _RoomClient {
3297
2109
  this.waitingForAuth = this.joinRequested;
3298
2110
  this.reconnectInfo = null;
3299
2111
  this.setConnectionState("auth_lost");
2112
+ this.rejectAllPendingRequests(new EdgeBaseError(401, "Auth state lost"));
3300
2113
  if (this.ws) {
3301
2114
  const socket = this.ws;
3302
2115
  this.sendRaw({ type: "leave" });
@@ -3305,7 +2118,6 @@ var RoomClient = class _RoomClient {
3305
2118
  this.connected = false;
3306
2119
  this.authenticated = false;
3307
2120
  this.joined = false;
3308
- this._mediaMembers = [];
3309
2121
  this.currentUserId = null;
3310
2122
  this.currentConnectionId = null;
3311
2123
  try {
@@ -3317,7 +2129,6 @@ var RoomClient = class _RoomClient {
3317
2129
  this.connected = false;
3318
2130
  this.authenticated = false;
3319
2131
  this.joined = false;
3320
- this._mediaMembers = [];
3321
2132
  }
3322
2133
  handleAuthenticationFailure(error) {
3323
2134
  const authError = error instanceof EdgeBaseError ? error : new EdgeBaseError(500, "Room authentication failed.");
@@ -3336,17 +2147,35 @@ var RoomClient = class _RoomClient {
3336
2147
  }
3337
2148
  }
3338
2149
  }
3339
- normalizeMembers(value) {
3340
- if (!Array.isArray(value)) {
3341
- return [];
2150
+ shouldTreatErrorAsAuthLoss(code) {
2151
+ if (code === "AUTH_STATE_LOST") {
2152
+ return true;
3342
2153
  }
3343
- return value.map((member) => this.normalizeMember(member)).filter((member) => !!member);
2154
+ if (code !== "NOT_AUTHENTICATED") {
2155
+ return false;
2156
+ }
2157
+ return this.authenticated || this.hasPendingRequests();
2158
+ }
2159
+ hasPendingRequests() {
2160
+ return this.pendingRequests.size > 0 || this.pendingSignalRequests.size > 0 || this.pendingAdminRequests.size > 0 || this.pendingMemberStateRequests.size > 0;
3344
2161
  }
3345
- normalizeMediaMembers(value) {
2162
+ handleRoomAuthStateLoss(message) {
2163
+ const detail = message?.trim();
2164
+ const authLossMessage = detail ? `Room authentication lost: ${detail}` : "Room authentication lost";
2165
+ this.setConnectionState("auth_lost");
2166
+ this.rejectAllPendingRequests(new EdgeBaseError(401, authLossMessage));
2167
+ if (this.ws && this.ws.readyState === WS_OPEN) {
2168
+ try {
2169
+ this.ws.close(ROOM_AUTH_STATE_LOST_CLOSE_CODE, authLossMessage);
2170
+ } catch {
2171
+ }
2172
+ }
2173
+ }
2174
+ normalizeMembers(value) {
3346
2175
  if (!Array.isArray(value)) {
3347
2176
  return [];
3348
2177
  }
3349
- return value.map((member) => this.normalizeMediaMember(member)).filter((member) => !!member);
2178
+ return value.map((member) => this.normalizeMember(member)).filter((member) => !!member);
3350
2179
  }
3351
2180
  normalizeMember(value) {
3352
2181
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -3371,80 +2200,6 @@ var RoomClient = class _RoomClient {
3371
2200
  }
3372
2201
  return cloneRecord(value);
3373
2202
  }
3374
- normalizeMediaMember(value) {
3375
- if (!value || typeof value !== "object" || Array.isArray(value)) {
3376
- return null;
3377
- }
3378
- const entry = value;
3379
- const member = this.normalizeMember(entry.member);
3380
- if (!member) {
3381
- return null;
3382
- }
3383
- return {
3384
- member,
3385
- state: this.normalizeMediaState(entry.state),
3386
- tracks: this.normalizeMediaTracks(entry.tracks)
3387
- };
3388
- }
3389
- normalizeMediaState(value) {
3390
- if (!value || typeof value !== "object" || Array.isArray(value)) {
3391
- return {};
3392
- }
3393
- const state = value;
3394
- return {
3395
- audio: this.normalizeMediaKindState(state.audio),
3396
- video: this.normalizeMediaKindState(state.video),
3397
- screen: this.normalizeMediaKindState(state.screen)
3398
- };
3399
- }
3400
- normalizeMediaKindState(value) {
3401
- if (!value || typeof value !== "object" || Array.isArray(value)) {
3402
- return void 0;
3403
- }
3404
- const state = value;
3405
- return {
3406
- published: state.published === true,
3407
- muted: state.muted === true,
3408
- trackId: typeof state.trackId === "string" ? state.trackId : void 0,
3409
- deviceId: typeof state.deviceId === "string" ? state.deviceId : void 0,
3410
- publishedAt: typeof state.publishedAt === "number" ? state.publishedAt : void 0,
3411
- adminDisabled: state.adminDisabled === true
3412
- };
3413
- }
3414
- normalizeMediaTracks(value) {
3415
- if (!Array.isArray(value)) {
3416
- return [];
3417
- }
3418
- return value.map((track) => this.normalizeMediaTrack(track)).filter((track) => !!track);
3419
- }
3420
- normalizeMediaTrack(value) {
3421
- if (!value || typeof value !== "object" || Array.isArray(value)) {
3422
- return null;
3423
- }
3424
- const track = value;
3425
- const kind = this.normalizeMediaKind(track.kind);
3426
- if (!kind) {
3427
- return null;
3428
- }
3429
- return {
3430
- kind,
3431
- trackId: typeof track.trackId === "string" ? track.trackId : void 0,
3432
- deviceId: typeof track.deviceId === "string" ? track.deviceId : void 0,
3433
- muted: track.muted === true,
3434
- publishedAt: typeof track.publishedAt === "number" ? track.publishedAt : void 0,
3435
- adminDisabled: track.adminDisabled === true
3436
- };
3437
- }
3438
- normalizeMediaKind(value) {
3439
- switch (value) {
3440
- case "audio":
3441
- case "video":
3442
- case "screen":
3443
- return value;
3444
- default:
3445
- return null;
3446
- }
3447
- }
3448
2203
  normalizeSignalMeta(value) {
3449
2204
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3450
2205
  return {};
@@ -3468,6 +2223,20 @@ var RoomClient = class _RoomClient {
3468
2223
  return "leave";
3469
2224
  }
3470
2225
  }
2226
+ getDebugSnapshot() {
2227
+ return {
2228
+ connectionState: this.connectionState,
2229
+ connected: this.connected,
2230
+ authenticated: this.authenticated,
2231
+ joined: this.joined,
2232
+ currentUserId: this.currentUserId,
2233
+ currentConnectionId: this.currentConnectionId,
2234
+ membersCount: this._members.length,
2235
+ reconnectAttempts: this.reconnectAttempts,
2236
+ joinRequested: this.joinRequested,
2237
+ waitingForAuth: this.waitingForAuth
2238
+ };
2239
+ }
3471
2240
  upsertMember(member) {
3472
2241
  const index = this._members.findIndex((entry) => entry.memberId === member.memberId);
3473
2242
  if (index >= 0) {
@@ -3479,62 +2248,7 @@ var RoomClient = class _RoomClient {
3479
2248
  removeMember(memberId) {
3480
2249
  this._members = this._members.filter((member) => member.memberId !== memberId);
3481
2250
  }
3482
- syncMediaMemberInfo(member) {
3483
- const mediaMember = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
3484
- if (!mediaMember) {
3485
- return;
3486
- }
3487
- mediaMember.member = cloneValue(member);
3488
- }
3489
- ensureMediaMember(member) {
3490
- const existing = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
3491
- if (existing) {
3492
- existing.member = cloneValue(member);
3493
- return existing;
3494
- }
3495
- const created = {
3496
- member: cloneValue(member),
3497
- state: {},
3498
- tracks: []
3499
- };
3500
- this._mediaMembers.push(created);
3501
- return created;
3502
- }
3503
- removeMediaMember(memberId) {
3504
- this._mediaMembers = this._mediaMembers.filter((member) => member.member.memberId !== memberId);
3505
- }
3506
- upsertMediaTrack(mediaMember, track) {
3507
- const index = mediaMember.tracks.findIndex(
3508
- (entry) => entry.kind === track.kind && entry.trackId === track.trackId
3509
- );
3510
- if (index >= 0) {
3511
- mediaMember.tracks[index] = cloneValue(track);
3512
- return;
3513
- }
3514
- mediaMember.tracks = mediaMember.tracks.filter((entry) => !(entry.kind === track.kind && !track.trackId)).concat(cloneValue(track));
3515
- }
3516
- removeMediaTrack(mediaMember, track) {
3517
- mediaMember.tracks = mediaMember.tracks.filter((entry) => {
3518
- if (track.trackId) {
3519
- return !(entry.kind === track.kind && entry.trackId === track.trackId);
3520
- }
3521
- return entry.kind !== track.kind;
3522
- });
3523
- }
3524
- mergeMediaState(mediaMember, kind, partial) {
3525
- const next = {
3526
- published: partial.published ?? mediaMember.state[kind]?.published ?? false,
3527
- muted: partial.muted ?? mediaMember.state[kind]?.muted ?? false,
3528
- trackId: partial.trackId ?? mediaMember.state[kind]?.trackId,
3529
- deviceId: partial.deviceId ?? mediaMember.state[kind]?.deviceId,
3530
- publishedAt: partial.publishedAt ?? mediaMember.state[kind]?.publishedAt,
3531
- adminDisabled: partial.adminDisabled ?? mediaMember.state[kind]?.adminDisabled
3532
- };
3533
- mediaMember.state = {
3534
- ...mediaMember.state,
3535
- [kind]: next
3536
- };
3537
- }
2251
+ /** Reject all 5 pending request maps at once. */
3538
2252
  rejectAllPendingRequests(error) {
3539
2253
  for (const [, pending] of this.pendingRequests) {
3540
2254
  clearTimeout(pending.timeout);
@@ -3544,7 +2258,6 @@ var RoomClient = class _RoomClient {
3544
2258
  this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
3545
2259
  this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
3546
2260
  this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
3547
- this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
3548
2261
  }
3549
2262
  rejectPendingVoidRequests(pendingRequests, error) {
3550
2263
  for (const [, pending] of pendingRequests) {
@@ -3553,11 +2266,56 @@ var RoomClient = class _RoomClient {
3553
2266
  }
3554
2267
  pendingRequests.clear();
3555
2268
  }
2269
+ shouldScheduleDisconnectReset(next) {
2270
+ if (this.intentionallyLeft || !this.joinRequested) {
2271
+ return false;
2272
+ }
2273
+ return next === "disconnected";
2274
+ }
2275
+ clearDisconnectResetTimer() {
2276
+ if (this.disconnectResetTimer) {
2277
+ clearTimeout(this.disconnectResetTimer);
2278
+ this.disconnectResetTimer = null;
2279
+ }
2280
+ }
2281
+ scheduleDisconnectReset(stateAtSchedule) {
2282
+ this.clearDisconnectResetTimer();
2283
+ const timeoutMs = this.options.disconnectResetTimeoutMs;
2284
+ if (!(timeoutMs > 0)) {
2285
+ return;
2286
+ }
2287
+ this.disconnectResetTimer = setTimeout(() => {
2288
+ this.disconnectResetTimer = null;
2289
+ if (this.intentionallyLeft || !this.joinRequested) {
2290
+ return;
2291
+ }
2292
+ if (this.connectionState !== stateAtSchedule) {
2293
+ return;
2294
+ }
2295
+ if (this.connectionState === "connected") {
2296
+ return;
2297
+ }
2298
+ for (const handler of this.recoveryFailureHandlers) {
2299
+ try {
2300
+ handler({
2301
+ state: this.connectionState,
2302
+ timeoutMs
2303
+ });
2304
+ } catch {
2305
+ }
2306
+ }
2307
+ }, timeoutMs);
2308
+ }
3556
2309
  setConnectionState(next) {
3557
2310
  if (this.connectionState === next) {
3558
2311
  return;
3559
2312
  }
3560
2313
  this.connectionState = next;
2314
+ if (this.shouldScheduleDisconnectReset(next)) {
2315
+ this.scheduleDisconnectReset(next);
2316
+ } else {
2317
+ this.clearDisconnectResetTimer();
2318
+ }
3561
2319
  for (const handler of this.connectionStateHandlers) {
3562
2320
  handler(next);
3563
2321
  }
@@ -3574,6 +2332,30 @@ var RoomClient = class _RoomClient {
3574
2332
  const wsUrl = httpUrl.replace(/^http/, "ws");
3575
2333
  return `${wsUrl}/api/room?namespace=${encodeURIComponent(this.namespace)}&id=${encodeURIComponent(this.roomId)}`;
3576
2334
  }
2335
+ attachBrowserNetworkListeners() {
2336
+ if (this.browserNetworkListenersAttached) {
2337
+ return;
2338
+ }
2339
+ const eventTarget = typeof globalThis !== "undefined" && typeof globalThis.addEventListener === "function" ? globalThis : null;
2340
+ if (!eventTarget) {
2341
+ return;
2342
+ }
2343
+ eventTarget.addEventListener("offline", this.browserOfflineHandler);
2344
+ eventTarget.addEventListener("online", this.browserOnlineHandler);
2345
+ this.browserNetworkListenersAttached = true;
2346
+ }
2347
+ detachBrowserNetworkListeners() {
2348
+ if (!this.browserNetworkListenersAttached) {
2349
+ return;
2350
+ }
2351
+ const eventTarget = typeof globalThis !== "undefined" && typeof globalThis.removeEventListener === "function" ? globalThis : null;
2352
+ if (!eventTarget) {
2353
+ return;
2354
+ }
2355
+ eventTarget.removeEventListener("offline", this.browserOfflineHandler);
2356
+ eventTarget.removeEventListener("online", this.browserOnlineHandler);
2357
+ this.browserNetworkListenersAttached = false;
2358
+ }
3577
2359
  scheduleReconnect() {
3578
2360
  const attempt = this.reconnectAttempts + 1;
3579
2361
  const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
@@ -3592,11 +2374,19 @@ var RoomClient = class _RoomClient {
3592
2374
  }
3593
2375
  startHeartbeat() {
3594
2376
  this.stopHeartbeat();
2377
+ this.lastHeartbeatAckAt = Date.now();
3595
2378
  this.heartbeatTimer = setInterval(() => {
3596
2379
  if (this.ws && this.connected) {
2380
+ if (Date.now() - this.lastHeartbeatAckAt > this.options.heartbeatStaleTimeoutMs) {
2381
+ try {
2382
+ this.ws.close();
2383
+ } catch {
2384
+ }
2385
+ return;
2386
+ }
3597
2387
  this.ws.send(JSON.stringify({ type: "ping" }));
3598
2388
  }
3599
- }, 3e4);
2389
+ }, this.options.heartbeatIntervalMs);
3600
2390
  }
3601
2391
  stopHeartbeat() {
3602
2392
  if (this.heartbeatTimer) {
@@ -3604,6 +2394,12 @@ var RoomClient = class _RoomClient {
3604
2394
  this.heartbeatTimer = null;
3605
2395
  }
3606
2396
  }
2397
+ getReconnectMemberState() {
2398
+ if (!this.lastLocalMemberState) {
2399
+ return void 0;
2400
+ }
2401
+ return cloneRecord(this.lastLocalMemberState);
2402
+ }
3607
2403
  };
3608
2404
  var PushClient = class {
3609
2405
  constructor(http, storage, core) {