@edge-base/react-native 0.2.6 → 0.2.8

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