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