@edge-base/react-native 0.2.5 → 0.2.7

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