@edge-base/react-native 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -758,7 +758,7 @@ var DatabaseLiveClient = class {
758
758
  (refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
759
759
  );
760
760
  if (!token) throw new core.EdgeBaseError(401, "No access token available. Sign in first.");
761
- this.sendRaw({ type: "auth", token, sdkVersion: "0.1.5" });
761
+ this.sendRaw({ type: "auth", token, sdkVersion: "0.2.0" });
762
762
  return new Promise((resolve, reject) => {
763
763
  const timeout = setTimeout(() => reject(new core.EdgeBaseError(401, "Auth timeout")), 1e4);
764
764
  const original = this.ws?.onmessage;
@@ -878,7 +878,7 @@ var DatabaseLiveClient = class {
878
878
  refreshAuth() {
879
879
  const token = this.tokenManager.currentAccessToken;
880
880
  if (!token || !this.ws || !this.connected) return;
881
- this.sendRaw({ type: "auth", token, sdkVersion: "0.1.5" });
881
+ this.sendRaw({ type: "auth", token, sdkVersion: "0.2.0" });
882
882
  }
883
883
  handleAuthStateChange(user) {
884
884
  if (user) {
@@ -941,7 +941,9 @@ var DatabaseLiveClient = class {
941
941
  }
942
942
  }
943
943
  scheduleReconnect(channel) {
944
- const delay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
944
+ const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
945
+ const jitter = Math.random() * baseDelay * 0.25;
946
+ const delay = baseDelay + jitter;
945
947
  this.reconnectAttempts++;
946
948
  setTimeout(() => {
947
949
  this.connect(channel).catch(() => {
@@ -1017,11 +1019,1119 @@ function matchesDatabaseLiveChannel(channel, change, messageChannel) {
1017
1019
  if (parts.length === 2) return parts[1] === change.table;
1018
1020
  if (parts.length === 3) return parts[2] === change.table;
1019
1021
  if (parts.length === 4) {
1020
- if (parts[2] === change.table) return change.docId === parts[3];
1022
+ if (parts[2] === change.table && change.docId === parts[3]) return true;
1021
1023
  return parts[3] === change.table;
1022
1024
  }
1023
1025
  return parts[3] === change.table && change.docId === parts[4];
1024
1026
  }
1027
+
1028
+ // src/room-cloudflare-media.ts
1029
+ function buildRemoteTrackKey(participantId, kind) {
1030
+ return `${participantId}:${kind}`;
1031
+ }
1032
+ function installMessage(packageName) {
1033
+ return `Install ${packageName} to use React Native room media transport. See https://edgebase.fun/docs/room/media`;
1034
+ }
1035
+ var RoomCloudflareMediaTransport = class {
1036
+ room;
1037
+ options;
1038
+ localTracks = /* @__PURE__ */ new Map();
1039
+ remoteTrackHandlers = [];
1040
+ participantListeners = /* @__PURE__ */ new Map();
1041
+ remoteTrackIds = /* @__PURE__ */ new Map();
1042
+ clientFactoryPromise = null;
1043
+ connectPromise = null;
1044
+ lifecycleVersion = 0;
1045
+ meeting = null;
1046
+ sessionId = null;
1047
+ joinedMapSubscriptionsAttached = false;
1048
+ onParticipantJoined = (participant) => {
1049
+ this.attachParticipant(participant);
1050
+ };
1051
+ onParticipantLeft = (participant) => {
1052
+ this.detachParticipant(participant.id);
1053
+ };
1054
+ onParticipantsCleared = () => {
1055
+ this.clearParticipantListeners();
1056
+ };
1057
+ onParticipantsUpdate = () => {
1058
+ this.syncAllParticipants();
1059
+ };
1060
+ constructor(room, options) {
1061
+ this.room = room;
1062
+ this.options = {
1063
+ autoSubscribe: options?.autoSubscribe ?? true,
1064
+ mediaDevices: options?.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0),
1065
+ clientFactory: options?.clientFactory
1066
+ };
1067
+ }
1068
+ getSessionId() {
1069
+ return this.sessionId;
1070
+ }
1071
+ getPeerConnection() {
1072
+ return null;
1073
+ }
1074
+ async connect(payload) {
1075
+ if (this.meeting && this.sessionId) {
1076
+ return this.sessionId;
1077
+ }
1078
+ if (this.connectPromise) {
1079
+ return this.connectPromise;
1080
+ }
1081
+ const connectPromise = (async () => {
1082
+ const lifecycleVersion = this.lifecycleVersion;
1083
+ const session = await this.room.media.cloudflareRealtimeKit.createSession(payload);
1084
+ this.assertConnectStillActive(lifecycleVersion);
1085
+ const factory = await this.resolveClientFactory();
1086
+ this.assertConnectStillActive(lifecycleVersion);
1087
+ const meeting = await factory.init({
1088
+ authToken: session.authToken,
1089
+ defaults: {
1090
+ audio: false,
1091
+ video: false
1092
+ }
1093
+ });
1094
+ this.assertConnectStillActive(lifecycleVersion, meeting);
1095
+ this.meeting = meeting;
1096
+ this.sessionId = session.sessionId;
1097
+ this.attachParticipantMapListeners();
1098
+ try {
1099
+ if (meeting.join) {
1100
+ await meeting.join();
1101
+ } else if (meeting.joinRoom) {
1102
+ await meeting.joinRoom();
1103
+ } else {
1104
+ throw new Error("RealtimeKit client does not expose join()/joinRoom().");
1105
+ }
1106
+ this.assertConnectStillActive(lifecycleVersion, meeting);
1107
+ this.syncAllParticipants();
1108
+ return session.sessionId;
1109
+ } catch (error) {
1110
+ if (this.meeting === meeting) {
1111
+ this.cleanupMeeting();
1112
+ } else {
1113
+ this.leaveMeetingSilently(meeting);
1114
+ }
1115
+ throw error;
1116
+ }
1117
+ })();
1118
+ this.connectPromise = connectPromise;
1119
+ try {
1120
+ return await connectPromise;
1121
+ } finally {
1122
+ if (this.connectPromise === connectPromise) {
1123
+ this.connectPromise = null;
1124
+ }
1125
+ }
1126
+ }
1127
+ async enableAudio(constraints = true) {
1128
+ const meeting = await this.ensureMeeting();
1129
+ const customTrack = await this.createUserMediaTrack("audio", constraints);
1130
+ await meeting.self.enableAudio(customTrack ?? void 0);
1131
+ const track = meeting.self.audioTrack ?? customTrack;
1132
+ if (!track) {
1133
+ throw new Error("RealtimeKit did not expose a local audio track after enabling audio.");
1134
+ }
1135
+ this.rememberLocalTrack("audio", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
1136
+ await this.room.media.audio.enable?.({
1137
+ trackId: track.id,
1138
+ deviceId: this.localTracks.get("audio")?.deviceId,
1139
+ providerSessionId: meeting.self.id
1140
+ });
1141
+ return track;
1142
+ }
1143
+ async enableVideo(constraints = true) {
1144
+ const meeting = await this.ensureMeeting();
1145
+ const customTrack = await this.createUserMediaTrack("video", constraints);
1146
+ await meeting.self.enableVideo(customTrack ?? void 0);
1147
+ const track = meeting.self.videoTrack ?? customTrack;
1148
+ if (!track) {
1149
+ throw new Error("RealtimeKit did not expose a local video track after enabling video.");
1150
+ }
1151
+ this.rememberLocalTrack("video", track, track.getSettings().deviceId ?? customTrack?.getSettings().deviceId, !!customTrack);
1152
+ await this.room.media.video.enable?.({
1153
+ trackId: track.id,
1154
+ deviceId: this.localTracks.get("video")?.deviceId,
1155
+ providerSessionId: meeting.self.id
1156
+ });
1157
+ return track;
1158
+ }
1159
+ async startScreenShare(_constraints = { video: true, audio: false }) {
1160
+ const meeting = await this.ensureMeeting();
1161
+ await meeting.self.enableScreenShare();
1162
+ const track = meeting.self.screenShareTracks?.video;
1163
+ if (!track) {
1164
+ throw new Error("RealtimeKit did not expose a screen-share video track.");
1165
+ }
1166
+ track.addEventListener("ended", () => {
1167
+ void this.stopScreenShare();
1168
+ }, { once: true });
1169
+ this.rememberLocalTrack("screen", track, track.getSettings().deviceId, false);
1170
+ await this.room.media.screen.start?.({
1171
+ trackId: track.id,
1172
+ deviceId: track.getSettings().deviceId,
1173
+ providerSessionId: meeting.self.id
1174
+ });
1175
+ return track;
1176
+ }
1177
+ async disableAudio() {
1178
+ if (!this.meeting) return;
1179
+ await this.meeting.self.disableAudio();
1180
+ this.releaseLocalTrack("audio");
1181
+ await this.room.media.audio.disable();
1182
+ }
1183
+ async disableVideo() {
1184
+ if (!this.meeting) return;
1185
+ await this.meeting.self.disableVideo();
1186
+ this.releaseLocalTrack("video");
1187
+ await this.room.media.video.disable();
1188
+ }
1189
+ async stopScreenShare() {
1190
+ if (!this.meeting) return;
1191
+ await this.meeting.self.disableScreenShare();
1192
+ this.releaseLocalTrack("screen");
1193
+ await this.room.media.screen.stop();
1194
+ }
1195
+ async setMuted(kind, muted) {
1196
+ const localTrack = this.localTracks.get(kind)?.track ?? (kind === "audio" ? this.meeting?.self.audioTrack : this.meeting?.self.videoTrack);
1197
+ if (localTrack) {
1198
+ localTrack.enabled = !muted;
1199
+ }
1200
+ if (kind === "audio") {
1201
+ await this.room.media.audio.setMuted?.(muted);
1202
+ } else {
1203
+ await this.room.media.video.setMuted?.(muted);
1204
+ }
1205
+ }
1206
+ async switchDevices(payload) {
1207
+ const meeting = await this.ensureMeeting();
1208
+ if (payload.audioInputId) {
1209
+ const audioDevice = await meeting.self.getDeviceById(payload.audioInputId, "audio");
1210
+ await meeting.self.setDevice(audioDevice);
1211
+ const audioTrack = meeting.self.audioTrack;
1212
+ if (audioTrack) {
1213
+ this.rememberLocalTrack("audio", audioTrack, payload.audioInputId, false);
1214
+ }
1215
+ }
1216
+ if (payload.videoInputId) {
1217
+ const videoDevice = await meeting.self.getDeviceById(payload.videoInputId, "video");
1218
+ await meeting.self.setDevice(videoDevice);
1219
+ const videoTrack = meeting.self.videoTrack;
1220
+ if (videoTrack) {
1221
+ this.rememberLocalTrack("video", videoTrack, payload.videoInputId, false);
1222
+ }
1223
+ }
1224
+ await this.room.media.devices.switch(payload);
1225
+ }
1226
+ onRemoteTrack(handler) {
1227
+ this.remoteTrackHandlers.push(handler);
1228
+ return {
1229
+ unsubscribe: () => {
1230
+ const index = this.remoteTrackHandlers.indexOf(handler);
1231
+ if (index >= 0) {
1232
+ this.remoteTrackHandlers.splice(index, 1);
1233
+ }
1234
+ }
1235
+ };
1236
+ }
1237
+ destroy() {
1238
+ this.lifecycleVersion += 1;
1239
+ this.connectPromise = null;
1240
+ for (const kind of this.localTracks.keys()) {
1241
+ this.releaseLocalTrack(kind);
1242
+ }
1243
+ this.clearParticipantListeners();
1244
+ this.detachParticipantMapListeners();
1245
+ this.cleanupMeeting();
1246
+ }
1247
+ async ensureMeeting() {
1248
+ if (!this.meeting) {
1249
+ await this.connect();
1250
+ }
1251
+ if (!this.meeting) {
1252
+ throw new Error("Cloudflare media transport is not connected");
1253
+ }
1254
+ return this.meeting;
1255
+ }
1256
+ async resolveClientFactory() {
1257
+ if (this.options.clientFactory) {
1258
+ return this.options.clientFactory;
1259
+ }
1260
+ this.clientFactoryPromise ??= import('@cloudflare/realtimekit-react-native').then((mod) => mod.default ?? mod).catch((error) => {
1261
+ throw new Error(`${installMessage("@cloudflare/realtimekit-react-native")}
1262
+ ${String(error)}`);
1263
+ });
1264
+ return this.clientFactoryPromise;
1265
+ }
1266
+ async createUserMediaTrack(kind, constraints) {
1267
+ const devices = this.options.mediaDevices;
1268
+ if (!devices?.getUserMedia || constraints === false) {
1269
+ return null;
1270
+ }
1271
+ const stream = await devices.getUserMedia(
1272
+ kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
1273
+ );
1274
+ return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
1275
+ }
1276
+ rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
1277
+ this.releaseLocalTrack(kind);
1278
+ this.localTracks.set(kind, {
1279
+ kind,
1280
+ track,
1281
+ deviceId,
1282
+ stopOnCleanup
1283
+ });
1284
+ }
1285
+ releaseLocalTrack(kind) {
1286
+ const local = this.localTracks.get(kind);
1287
+ if (!local) return;
1288
+ if (local.stopOnCleanup) {
1289
+ local.track.stop();
1290
+ }
1291
+ this.localTracks.delete(kind);
1292
+ }
1293
+ attachParticipantMapListeners() {
1294
+ const participantMap = this.getParticipantMap();
1295
+ if (!participantMap || !this.meeting || this.joinedMapSubscriptionsAttached) {
1296
+ return;
1297
+ }
1298
+ participantMap.on("participantJoined", this.onParticipantJoined);
1299
+ participantMap.on("participantLeft", this.onParticipantLeft);
1300
+ participantMap.on("participantsCleared", this.onParticipantsCleared);
1301
+ participantMap.on("participantsUpdate", this.onParticipantsUpdate);
1302
+ this.joinedMapSubscriptionsAttached = true;
1303
+ }
1304
+ detachParticipantMapListeners() {
1305
+ const participantMap = this.getParticipantMap();
1306
+ if (!participantMap || !this.meeting || !this.joinedMapSubscriptionsAttached) {
1307
+ return;
1308
+ }
1309
+ participantMap.off("participantJoined", this.onParticipantJoined);
1310
+ participantMap.off("participantLeft", this.onParticipantLeft);
1311
+ participantMap.off("participantsCleared", this.onParticipantsCleared);
1312
+ participantMap.off("participantsUpdate", this.onParticipantsUpdate);
1313
+ this.joinedMapSubscriptionsAttached = false;
1314
+ }
1315
+ syncAllParticipants() {
1316
+ const participantMap = this.getParticipantMap();
1317
+ if (!participantMap || !this.meeting || !this.options.autoSubscribe) {
1318
+ return;
1319
+ }
1320
+ for (const participant of participantMap.values()) {
1321
+ this.attachParticipant(participant);
1322
+ }
1323
+ }
1324
+ getParticipantMap() {
1325
+ if (!this.meeting) {
1326
+ return null;
1327
+ }
1328
+ return this.meeting.participants.active ?? this.meeting.participants.joined ?? null;
1329
+ }
1330
+ attachParticipant(participant) {
1331
+ if (!this.options.autoSubscribe || !this.meeting) {
1332
+ return;
1333
+ }
1334
+ if (participant.id === this.meeting.self.id || this.participantListeners.has(participant.id)) {
1335
+ this.syncParticipantTracks(participant);
1336
+ return;
1337
+ }
1338
+ const listenerSet = {
1339
+ participant,
1340
+ onAudioUpdate: ({ audioEnabled, audioTrack }) => {
1341
+ this.handleRemoteTrackUpdate("audio", participant, audioTrack, audioEnabled);
1342
+ },
1343
+ onVideoUpdate: ({ videoEnabled, videoTrack }) => {
1344
+ this.handleRemoteTrackUpdate("video", participant, videoTrack, videoEnabled);
1345
+ },
1346
+ onScreenShareUpdate: ({ screenShareEnabled, screenShareTracks }) => {
1347
+ this.handleRemoteTrackUpdate("screen", participant, screenShareTracks.video, screenShareEnabled);
1348
+ }
1349
+ };
1350
+ participant.on("audioUpdate", listenerSet.onAudioUpdate);
1351
+ participant.on("videoUpdate", listenerSet.onVideoUpdate);
1352
+ participant.on("screenShareUpdate", listenerSet.onScreenShareUpdate);
1353
+ this.participantListeners.set(participant.id, listenerSet);
1354
+ this.syncParticipantTracks(participant);
1355
+ }
1356
+ detachParticipant(participantId) {
1357
+ const listenerSet = this.participantListeners.get(participantId);
1358
+ if (!listenerSet) return;
1359
+ listenerSet.participant.off("audioUpdate", listenerSet.onAudioUpdate);
1360
+ listenerSet.participant.off("videoUpdate", listenerSet.onVideoUpdate);
1361
+ listenerSet.participant.off("screenShareUpdate", listenerSet.onScreenShareUpdate);
1362
+ this.participantListeners.delete(participantId);
1363
+ for (const kind of ["audio", "video", "screen"]) {
1364
+ this.remoteTrackIds.delete(buildRemoteTrackKey(participantId, kind));
1365
+ }
1366
+ }
1367
+ clearParticipantListeners() {
1368
+ for (const participantId of Array.from(this.participantListeners.keys())) {
1369
+ this.detachParticipant(participantId);
1370
+ }
1371
+ this.remoteTrackIds.clear();
1372
+ }
1373
+ syncParticipantTracks(participant) {
1374
+ this.handleRemoteTrackUpdate("audio", participant, participant.audioTrack, participant.audioEnabled === true);
1375
+ this.handleRemoteTrackUpdate("video", participant, participant.videoTrack, participant.videoEnabled === true);
1376
+ this.handleRemoteTrackUpdate("screen", participant, participant.screenShareTracks?.video, participant.screenShareEnabled === true);
1377
+ }
1378
+ handleRemoteTrackUpdate(kind, participant, track, enabled) {
1379
+ const key = buildRemoteTrackKey(participant.id, kind);
1380
+ if (!enabled || !track) {
1381
+ this.remoteTrackIds.delete(key);
1382
+ return;
1383
+ }
1384
+ const previousTrackId = this.remoteTrackIds.get(key);
1385
+ if (previousTrackId === track.id) {
1386
+ return;
1387
+ }
1388
+ this.remoteTrackIds.set(key, track.id);
1389
+ const payload = {
1390
+ kind,
1391
+ track,
1392
+ stream: new MediaStream([track]),
1393
+ trackName: track.id,
1394
+ providerSessionId: participant.id,
1395
+ participantId: participant.id,
1396
+ customParticipantId: participant.customParticipantId,
1397
+ userId: participant.userId
1398
+ };
1399
+ for (const handler of this.remoteTrackHandlers) {
1400
+ handler(payload);
1401
+ }
1402
+ }
1403
+ cleanupMeeting() {
1404
+ const meeting = this.meeting;
1405
+ this.detachParticipantMapListeners();
1406
+ this.clearParticipantListeners();
1407
+ this.meeting = null;
1408
+ this.sessionId = null;
1409
+ this.leaveMeetingSilently(meeting);
1410
+ }
1411
+ assertConnectStillActive(lifecycleVersion, meeting) {
1412
+ if (lifecycleVersion === this.lifecycleVersion) {
1413
+ return;
1414
+ }
1415
+ if (meeting) {
1416
+ this.leaveMeetingSilently(meeting);
1417
+ }
1418
+ throw new Error("Cloudflare media transport was destroyed during connect.");
1419
+ }
1420
+ leaveMeetingSilently(meeting) {
1421
+ if (!meeting) {
1422
+ return;
1423
+ }
1424
+ const leavePromise = meeting.leave?.() ?? meeting.leaveRoom?.();
1425
+ if (leavePromise) {
1426
+ void leavePromise.catch(() => {
1427
+ });
1428
+ }
1429
+ }
1430
+ };
1431
+
1432
+ // src/room-p2p-media.ts
1433
+ var DEFAULT_SIGNAL_PREFIX = "edgebase.media.p2p";
1434
+ var DEFAULT_ICE_SERVERS = [
1435
+ { urls: "stun:stun.l.google.com:19302" }
1436
+ ];
1437
+ var DEFAULT_MEMBER_READY_TIMEOUT_MS = 1e4;
1438
+ function buildTrackKey(memberId, trackId) {
1439
+ return `${memberId}:${trackId}`;
1440
+ }
1441
+ function buildExactDeviceConstraint(deviceId) {
1442
+ return { deviceId: { exact: deviceId } };
1443
+ }
1444
+ function normalizeTrackKind(track) {
1445
+ if (track.kind === "audio") return "audio";
1446
+ if (track.kind === "video") return "video";
1447
+ return null;
1448
+ }
1449
+ function serializeDescription(description) {
1450
+ return {
1451
+ type: description.type,
1452
+ sdp: description.sdp ?? void 0
1453
+ };
1454
+ }
1455
+ function serializeCandidate(candidate) {
1456
+ if ("toJSON" in candidate && typeof candidate.toJSON === "function") {
1457
+ return candidate.toJSON();
1458
+ }
1459
+ return candidate;
1460
+ }
1461
+ function installMessage2(packageName) {
1462
+ return `Install ${packageName} to use React Native P2P media transport. See https://edgebase.fun/docs/room/media`;
1463
+ }
1464
+ var RoomP2PMediaTransport = class {
1465
+ room;
1466
+ options;
1467
+ localTracks = /* @__PURE__ */ new Map();
1468
+ peers = /* @__PURE__ */ new Map();
1469
+ remoteTrackHandlers = [];
1470
+ remoteTrackKinds = /* @__PURE__ */ new Map();
1471
+ emittedRemoteTracks = /* @__PURE__ */ new Set();
1472
+ pendingRemoteTracks = /* @__PURE__ */ new Map();
1473
+ subscriptions = [];
1474
+ localMemberId = null;
1475
+ connected = false;
1476
+ runtimePromise = null;
1477
+ constructor(room, options) {
1478
+ this.room = room;
1479
+ this.options = {
1480
+ rtcConfiguration: {
1481
+ ...options?.rtcConfiguration,
1482
+ iceServers: options?.rtcConfiguration?.iceServers && options.rtcConfiguration.iceServers.length > 0 ? options.rtcConfiguration.iceServers : DEFAULT_ICE_SERVERS
1483
+ },
1484
+ peerConnectionFactory: options?.peerConnectionFactory,
1485
+ mediaDevices: options?.mediaDevices,
1486
+ signalPrefix: options?.signalPrefix ?? DEFAULT_SIGNAL_PREFIX
1487
+ };
1488
+ }
1489
+ getSessionId() {
1490
+ return this.localMemberId;
1491
+ }
1492
+ getPeerConnection() {
1493
+ if (this.peers.size !== 1) {
1494
+ return null;
1495
+ }
1496
+ return this.peers.values().next().value?.pc ?? null;
1497
+ }
1498
+ async connect(payload) {
1499
+ if (this.connected && this.localMemberId) {
1500
+ return this.localMemberId;
1501
+ }
1502
+ if (payload && typeof payload === "object" && "sessionDescription" in payload) {
1503
+ throw new Error(
1504
+ "RoomP2PMediaTransport.connect() does not accept sessionDescription; use room.signals through the built-in transport instead."
1505
+ );
1506
+ }
1507
+ await this.ensureRuntime();
1508
+ const currentMember = await this.waitForCurrentMember();
1509
+ if (!currentMember) {
1510
+ throw new Error("Join the room before connecting a P2P media transport.");
1511
+ }
1512
+ this.localMemberId = currentMember.memberId;
1513
+ this.connected = true;
1514
+ this.hydrateRemoteTrackKinds();
1515
+ this.attachRoomSubscriptions();
1516
+ try {
1517
+ for (const member of this.room.members.list()) {
1518
+ if (member.memberId !== this.localMemberId) {
1519
+ this.ensurePeer(member.memberId);
1520
+ }
1521
+ }
1522
+ } catch (error) {
1523
+ this.rollbackConnectedState();
1524
+ throw error;
1525
+ }
1526
+ return this.localMemberId;
1527
+ }
1528
+ async waitForCurrentMember(timeoutMs = DEFAULT_MEMBER_READY_TIMEOUT_MS) {
1529
+ const startedAt = Date.now();
1530
+ while (Date.now() - startedAt < timeoutMs) {
1531
+ const member = this.room.members.current();
1532
+ if (member) {
1533
+ return member;
1534
+ }
1535
+ await new Promise((resolve) => globalThis.setTimeout(resolve, 50));
1536
+ }
1537
+ return this.room.members.current();
1538
+ }
1539
+ async enableAudio(constraints = true) {
1540
+ const track = await this.createUserMediaTrack("audio", constraints);
1541
+ if (!track) {
1542
+ throw new Error("P2P transport could not create a local audio track.");
1543
+ }
1544
+ const providerSessionId = await this.ensureConnectedMemberId();
1545
+ this.rememberLocalTrack("audio", track, track.getSettings().deviceId, true);
1546
+ await this.room.media.audio.enable?.({
1547
+ trackId: track.id,
1548
+ deviceId: track.getSettings().deviceId,
1549
+ providerSessionId
1550
+ });
1551
+ this.syncAllPeerSenders();
1552
+ return track;
1553
+ }
1554
+ async enableVideo(constraints = true) {
1555
+ const track = await this.createUserMediaTrack("video", constraints);
1556
+ if (!track) {
1557
+ throw new Error("P2P transport could not create a local video track.");
1558
+ }
1559
+ const providerSessionId = await this.ensureConnectedMemberId();
1560
+ this.rememberLocalTrack("video", track, track.getSettings().deviceId, true);
1561
+ await this.room.media.video.enable?.({
1562
+ trackId: track.id,
1563
+ deviceId: track.getSettings().deviceId,
1564
+ providerSessionId
1565
+ });
1566
+ this.syncAllPeerSenders();
1567
+ return track;
1568
+ }
1569
+ async startScreenShare(constraints = { video: true, audio: false }) {
1570
+ const devices = await this.resolveMediaDevices();
1571
+ if (!devices?.getDisplayMedia) {
1572
+ throw new Error("Screen sharing is not available in this environment.");
1573
+ }
1574
+ const stream = await devices.getDisplayMedia(constraints);
1575
+ const track = stream.getVideoTracks()[0] ?? null;
1576
+ if (!track) {
1577
+ throw new Error("P2P transport could not create a screen-share track.");
1578
+ }
1579
+ track.addEventListener("ended", () => {
1580
+ void this.stopScreenShare();
1581
+ }, { once: true });
1582
+ const providerSessionId = await this.ensureConnectedMemberId();
1583
+ this.rememberLocalTrack("screen", track, track.getSettings().deviceId, true);
1584
+ await this.room.media.screen.start?.({
1585
+ trackId: track.id,
1586
+ deviceId: track.getSettings().deviceId,
1587
+ providerSessionId
1588
+ });
1589
+ this.syncAllPeerSenders();
1590
+ return track;
1591
+ }
1592
+ async disableAudio() {
1593
+ this.releaseLocalTrack("audio");
1594
+ this.syncAllPeerSenders();
1595
+ await this.room.media.audio.disable();
1596
+ }
1597
+ async disableVideo() {
1598
+ this.releaseLocalTrack("video");
1599
+ this.syncAllPeerSenders();
1600
+ await this.room.media.video.disable();
1601
+ }
1602
+ async stopScreenShare() {
1603
+ this.releaseLocalTrack("screen");
1604
+ this.syncAllPeerSenders();
1605
+ await this.room.media.screen.stop();
1606
+ }
1607
+ async setMuted(kind, muted) {
1608
+ const localTrack = this.localTracks.get(kind)?.track;
1609
+ if (localTrack) {
1610
+ localTrack.enabled = !muted;
1611
+ }
1612
+ if (kind === "audio") {
1613
+ await this.room.media.audio.setMuted?.(muted);
1614
+ } else {
1615
+ await this.room.media.video.setMuted?.(muted);
1616
+ }
1617
+ }
1618
+ async switchDevices(payload) {
1619
+ if (payload.audioInputId && this.localTracks.has("audio")) {
1620
+ const nextAudioTrack = await this.createUserMediaTrack("audio", buildExactDeviceConstraint(payload.audioInputId));
1621
+ if (nextAudioTrack) {
1622
+ this.rememberLocalTrack("audio", nextAudioTrack, payload.audioInputId, true);
1623
+ }
1624
+ }
1625
+ if (payload.videoInputId && this.localTracks.has("video")) {
1626
+ const nextVideoTrack = await this.createUserMediaTrack("video", buildExactDeviceConstraint(payload.videoInputId));
1627
+ if (nextVideoTrack) {
1628
+ this.rememberLocalTrack("video", nextVideoTrack, payload.videoInputId, true);
1629
+ }
1630
+ }
1631
+ this.syncAllPeerSenders();
1632
+ await this.room.media.devices.switch(payload);
1633
+ }
1634
+ onRemoteTrack(handler) {
1635
+ this.remoteTrackHandlers.push(handler);
1636
+ return {
1637
+ unsubscribe: () => {
1638
+ const index = this.remoteTrackHandlers.indexOf(handler);
1639
+ if (index >= 0) {
1640
+ this.remoteTrackHandlers.splice(index, 1);
1641
+ }
1642
+ }
1643
+ };
1644
+ }
1645
+ destroy() {
1646
+ this.connected = false;
1647
+ this.localMemberId = null;
1648
+ for (const subscription of this.subscriptions.splice(0)) {
1649
+ subscription.unsubscribe();
1650
+ }
1651
+ for (const peer of this.peers.values()) {
1652
+ this.destroyPeer(peer);
1653
+ }
1654
+ this.peers.clear();
1655
+ for (const kind of Array.from(this.localTracks.keys())) {
1656
+ this.releaseLocalTrack(kind);
1657
+ }
1658
+ this.remoteTrackKinds.clear();
1659
+ this.emittedRemoteTracks.clear();
1660
+ this.pendingRemoteTracks.clear();
1661
+ }
1662
+ attachRoomSubscriptions() {
1663
+ if (this.subscriptions.length > 0) {
1664
+ return;
1665
+ }
1666
+ this.subscriptions.push(
1667
+ this.room.members.onJoin((member) => {
1668
+ if (member.memberId !== this.localMemberId) {
1669
+ this.ensurePeer(member.memberId);
1670
+ }
1671
+ }),
1672
+ this.room.members.onSync((members) => {
1673
+ const activeMemberIds = /* @__PURE__ */ new Set();
1674
+ for (const member of members) {
1675
+ if (member.memberId !== this.localMemberId) {
1676
+ activeMemberIds.add(member.memberId);
1677
+ this.ensurePeer(member.memberId);
1678
+ }
1679
+ }
1680
+ for (const memberId of Array.from(this.peers.keys())) {
1681
+ if (!activeMemberIds.has(memberId)) {
1682
+ this.removeRemoteMember(memberId);
1683
+ }
1684
+ }
1685
+ }),
1686
+ this.room.members.onLeave((member) => {
1687
+ this.removeRemoteMember(member.memberId);
1688
+ }),
1689
+ this.room.signals.on(this.offerEvent, (payload, meta) => {
1690
+ void this.handleDescriptionSignal("offer", payload, meta);
1691
+ }),
1692
+ this.room.signals.on(this.answerEvent, (payload, meta) => {
1693
+ void this.handleDescriptionSignal("answer", payload, meta);
1694
+ }),
1695
+ this.room.signals.on(this.iceEvent, (payload, meta) => {
1696
+ void this.handleIceSignal(payload, meta);
1697
+ }),
1698
+ this.room.media.onTrack((track, member) => {
1699
+ if (member.memberId !== this.localMemberId) {
1700
+ this.ensurePeer(member.memberId);
1701
+ }
1702
+ this.rememberRemoteTrackKind(track, member);
1703
+ }),
1704
+ this.room.media.onTrackRemoved((track, member) => {
1705
+ if (!track.trackId) return;
1706
+ const key = buildTrackKey(member.memberId, track.trackId);
1707
+ this.remoteTrackKinds.delete(key);
1708
+ this.emittedRemoteTracks.delete(key);
1709
+ this.pendingRemoteTracks.delete(key);
1710
+ })
1711
+ );
1712
+ }
1713
+ hydrateRemoteTrackKinds() {
1714
+ this.remoteTrackKinds.clear();
1715
+ this.emittedRemoteTracks.clear();
1716
+ this.pendingRemoteTracks.clear();
1717
+ for (const mediaMember of this.room.media.list()) {
1718
+ for (const track of mediaMember.tracks) {
1719
+ this.rememberRemoteTrackKind(track, mediaMember.member);
1720
+ }
1721
+ }
1722
+ }
1723
+ rememberRemoteTrackKind(track, member) {
1724
+ if (!track.trackId || member.memberId === this.localMemberId) {
1725
+ return;
1726
+ }
1727
+ const key = buildTrackKey(member.memberId, track.trackId);
1728
+ this.remoteTrackKinds.set(key, track.kind);
1729
+ const pending = this.pendingRemoteTracks.get(key);
1730
+ if (pending) {
1731
+ this.pendingRemoteTracks.delete(key);
1732
+ this.emitRemoteTrack(member.memberId, pending.track, pending.stream, track.kind);
1733
+ return;
1734
+ }
1735
+ this.flushPendingRemoteTracks(member.memberId, track.kind);
1736
+ }
1737
+ ensurePeer(memberId) {
1738
+ const existing = this.peers.get(memberId);
1739
+ if (existing) {
1740
+ this.syncPeerSenders(existing);
1741
+ return existing;
1742
+ }
1743
+ const factory = this.options.peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
1744
+ const pc = factory(this.options.rtcConfiguration);
1745
+ const peer = {
1746
+ memberId,
1747
+ pc,
1748
+ polite: !!this.localMemberId && this.localMemberId.localeCompare(memberId) > 0,
1749
+ makingOffer: false,
1750
+ ignoreOffer: false,
1751
+ isSettingRemoteAnswerPending: false,
1752
+ pendingCandidates: [],
1753
+ senders: /* @__PURE__ */ new Map()
1754
+ };
1755
+ pc.onicecandidate = (event) => {
1756
+ if (!event.candidate) return;
1757
+ void this.room.signals.sendTo(memberId, this.iceEvent, {
1758
+ candidate: serializeCandidate(event.candidate)
1759
+ });
1760
+ };
1761
+ pc.onnegotiationneeded = () => {
1762
+ void this.negotiatePeer(peer);
1763
+ };
1764
+ pc.ontrack = (event) => {
1765
+ const stream = event.streams[0] ?? new MediaStream([event.track]);
1766
+ const key = buildTrackKey(memberId, event.track.id);
1767
+ const exactKind = this.remoteTrackKinds.get(key);
1768
+ const fallbackKind = exactKind ? null : this.resolveFallbackRemoteTrackKind(memberId, event.track);
1769
+ const kind = exactKind ?? fallbackKind ?? normalizeTrackKind(event.track);
1770
+ if (!kind || !exactKind && !fallbackKind && kind === "video" && event.track.kind === "video") {
1771
+ this.pendingRemoteTracks.set(key, { memberId, track: event.track, stream });
1772
+ return;
1773
+ }
1774
+ this.emitRemoteTrack(memberId, event.track, stream, kind);
1775
+ };
1776
+ this.peers.set(memberId, peer);
1777
+ this.syncPeerSenders(peer);
1778
+ return peer;
1779
+ }
1780
+ async negotiatePeer(peer) {
1781
+ if (!this.connected || peer.pc.connectionState === "closed" || peer.makingOffer || peer.isSettingRemoteAnswerPending || peer.pc.signalingState !== "stable") {
1782
+ return;
1783
+ }
1784
+ try {
1785
+ peer.makingOffer = true;
1786
+ await peer.pc.setLocalDescription();
1787
+ if (!peer.pc.localDescription) {
1788
+ return;
1789
+ }
1790
+ await this.room.signals.sendTo(peer.memberId, this.offerEvent, {
1791
+ description: serializeDescription(peer.pc.localDescription)
1792
+ });
1793
+ } catch (error) {
1794
+ console.warn("[RoomP2PMediaTransport] Failed to negotiate peer offer.", {
1795
+ memberId: peer.memberId,
1796
+ signalingState: peer.pc.signalingState,
1797
+ error
1798
+ });
1799
+ } finally {
1800
+ peer.makingOffer = false;
1801
+ }
1802
+ }
1803
+ async handleDescriptionSignal(expectedType, payload, meta) {
1804
+ const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
1805
+ if (!senderId || senderId === this.localMemberId) {
1806
+ return;
1807
+ }
1808
+ const description = this.normalizeDescription(payload);
1809
+ if (!description || description.type !== expectedType) {
1810
+ return;
1811
+ }
1812
+ const peer = this.ensurePeer(senderId);
1813
+ const readyForOffer = !peer.makingOffer && (peer.pc.signalingState === "stable" || peer.isSettingRemoteAnswerPending);
1814
+ const offerCollision = description.type === "offer" && !readyForOffer;
1815
+ peer.ignoreOffer = !peer.polite && offerCollision;
1816
+ if (peer.ignoreOffer) {
1817
+ return;
1818
+ }
1819
+ try {
1820
+ peer.isSettingRemoteAnswerPending = description.type === "answer";
1821
+ await peer.pc.setRemoteDescription(description);
1822
+ peer.isSettingRemoteAnswerPending = false;
1823
+ await this.flushPendingCandidates(peer);
1824
+ if (description.type === "offer") {
1825
+ this.syncPeerSenders(peer);
1826
+ await peer.pc.setLocalDescription();
1827
+ if (!peer.pc.localDescription) {
1828
+ return;
1829
+ }
1830
+ await this.room.signals.sendTo(senderId, this.answerEvent, {
1831
+ description: serializeDescription(peer.pc.localDescription)
1832
+ });
1833
+ }
1834
+ } catch (error) {
1835
+ console.warn("[RoomP2PMediaTransport] Failed to apply remote session description.", {
1836
+ memberId: senderId,
1837
+ expectedType,
1838
+ signalingState: peer.pc.signalingState,
1839
+ error
1840
+ });
1841
+ peer.isSettingRemoteAnswerPending = false;
1842
+ }
1843
+ }
1844
+ async handleIceSignal(payload, meta) {
1845
+ const senderId = typeof meta.memberId === "string" && meta.memberId.trim() ? meta.memberId.trim() : "";
1846
+ if (!senderId || senderId === this.localMemberId) {
1847
+ return;
1848
+ }
1849
+ const candidate = this.normalizeCandidate(payload);
1850
+ if (!candidate) {
1851
+ return;
1852
+ }
1853
+ const peer = this.ensurePeer(senderId);
1854
+ if (!peer.pc.remoteDescription) {
1855
+ peer.pendingCandidates.push(candidate);
1856
+ return;
1857
+ }
1858
+ try {
1859
+ await peer.pc.addIceCandidate(candidate);
1860
+ } catch (error) {
1861
+ console.warn("[RoomP2PMediaTransport] Failed to add ICE candidate.", {
1862
+ memberId: senderId,
1863
+ error
1864
+ });
1865
+ if (!peer.ignoreOffer) {
1866
+ peer.pendingCandidates.push(candidate);
1867
+ }
1868
+ }
1869
+ }
1870
+ async flushPendingCandidates(peer) {
1871
+ if (!peer.pc.remoteDescription || peer.pendingCandidates.length === 0) {
1872
+ return;
1873
+ }
1874
+ const pending = [...peer.pendingCandidates];
1875
+ peer.pendingCandidates.length = 0;
1876
+ for (const candidate of pending) {
1877
+ try {
1878
+ await peer.pc.addIceCandidate(candidate);
1879
+ } catch (error) {
1880
+ console.warn("[RoomP2PMediaTransport] Failed to flush pending ICE candidate.", {
1881
+ memberId: peer.memberId,
1882
+ error
1883
+ });
1884
+ if (!peer.ignoreOffer) {
1885
+ peer.pendingCandidates.push(candidate);
1886
+ }
1887
+ }
1888
+ }
1889
+ }
1890
+ syncAllPeerSenders() {
1891
+ for (const peer of this.peers.values()) {
1892
+ this.syncPeerSenders(peer);
1893
+ }
1894
+ }
1895
+ syncPeerSenders(peer) {
1896
+ const activeKinds = /* @__PURE__ */ new Set();
1897
+ let changed = false;
1898
+ for (const [kind, localTrack] of this.localTracks.entries()) {
1899
+ activeKinds.add(kind);
1900
+ const sender = peer.senders.get(kind);
1901
+ if (sender) {
1902
+ if (sender.track !== localTrack.track) {
1903
+ void sender.replaceTrack(localTrack.track);
1904
+ changed = true;
1905
+ }
1906
+ continue;
1907
+ }
1908
+ const addedSender = peer.pc.addTrack(localTrack.track, new MediaStream([localTrack.track]));
1909
+ peer.senders.set(kind, addedSender);
1910
+ changed = true;
1911
+ }
1912
+ for (const [kind, sender] of Array.from(peer.senders.entries())) {
1913
+ if (activeKinds.has(kind)) {
1914
+ continue;
1915
+ }
1916
+ try {
1917
+ peer.pc.removeTrack(sender);
1918
+ } catch {
1919
+ }
1920
+ peer.senders.delete(kind);
1921
+ changed = true;
1922
+ }
1923
+ if (changed) {
1924
+ void this.negotiatePeer(peer);
1925
+ }
1926
+ }
1927
+ emitRemoteTrack(memberId, track, stream, kind) {
1928
+ const key = buildTrackKey(memberId, track.id);
1929
+ if (this.emittedRemoteTracks.has(key)) {
1930
+ return;
1931
+ }
1932
+ this.emittedRemoteTracks.add(key);
1933
+ this.remoteTrackKinds.set(key, kind);
1934
+ const participant = this.findMember(memberId);
1935
+ const payload = {
1936
+ kind,
1937
+ track,
1938
+ stream,
1939
+ trackName: track.id,
1940
+ providerSessionId: memberId,
1941
+ participantId: memberId,
1942
+ userId: participant?.userId
1943
+ };
1944
+ for (const handler of this.remoteTrackHandlers) {
1945
+ handler(payload);
1946
+ }
1947
+ }
1948
+ resolveFallbackRemoteTrackKind(memberId, track) {
1949
+ const normalizedKind = normalizeTrackKind(track);
1950
+ if (!normalizedKind) {
1951
+ return null;
1952
+ }
1953
+ if (normalizedKind === "audio") {
1954
+ return "audio";
1955
+ }
1956
+ return this.getNextUnassignedPublishedVideoLikeKind(memberId);
1957
+ }
1958
+ flushPendingRemoteTracks(memberId, roomKind) {
1959
+ const expectedTrackKind = roomKind === "audio" ? "audio" : "video";
1960
+ for (const [key, pending] of this.pendingRemoteTracks.entries()) {
1961
+ if (pending.memberId !== memberId || pending.track.kind !== expectedTrackKind) {
1962
+ continue;
1963
+ }
1964
+ this.pendingRemoteTracks.delete(key);
1965
+ this.emitRemoteTrack(memberId, pending.track, pending.stream, roomKind);
1966
+ return;
1967
+ }
1968
+ }
1969
+ getPublishedVideoLikeKinds(memberId) {
1970
+ const mediaMember = this.room.media.list().find((entry) => entry.member.memberId === memberId);
1971
+ if (!mediaMember) {
1972
+ return [];
1973
+ }
1974
+ const publishedKinds = /* @__PURE__ */ new Set();
1975
+ for (const track of mediaMember.tracks) {
1976
+ if ((track.kind === "video" || track.kind === "screen") && track.trackId) {
1977
+ publishedKinds.add(track.kind);
1978
+ }
1979
+ }
1980
+ return Array.from(publishedKinds);
1981
+ }
1982
+ getNextUnassignedPublishedVideoLikeKind(memberId) {
1983
+ const publishedKinds = this.getPublishedVideoLikeKinds(memberId);
1984
+ if (publishedKinds.length === 0) {
1985
+ return null;
1986
+ }
1987
+ const assignedKinds = /* @__PURE__ */ new Set();
1988
+ for (const key of this.emittedRemoteTracks) {
1989
+ if (!key.startsWith(`${memberId}:`)) {
1990
+ continue;
1991
+ }
1992
+ const kind = this.remoteTrackKinds.get(key);
1993
+ if (kind === "video" || kind === "screen") {
1994
+ assignedKinds.add(kind);
1995
+ }
1996
+ }
1997
+ return publishedKinds.find((kind) => !assignedKinds.has(kind)) ?? null;
1998
+ }
1999
+ closePeer(memberId) {
2000
+ const peer = this.peers.get(memberId);
2001
+ if (!peer) return;
2002
+ this.destroyPeer(peer);
2003
+ this.peers.delete(memberId);
2004
+ }
2005
+ removeRemoteMember(memberId) {
2006
+ this.remoteTrackKinds.forEach((_kind, key) => {
2007
+ if (key.startsWith(`${memberId}:`)) {
2008
+ this.remoteTrackKinds.delete(key);
2009
+ }
2010
+ });
2011
+ this.emittedRemoteTracks.forEach((key) => {
2012
+ if (key.startsWith(`${memberId}:`)) {
2013
+ this.emittedRemoteTracks.delete(key);
2014
+ }
2015
+ });
2016
+ this.pendingRemoteTracks.forEach((_pending, key) => {
2017
+ if (key.startsWith(`${memberId}:`)) {
2018
+ this.pendingRemoteTracks.delete(key);
2019
+ }
2020
+ });
2021
+ this.closePeer(memberId);
2022
+ }
2023
+ findMember(memberId) {
2024
+ return this.room.members.list().find((member) => member.memberId === memberId);
2025
+ }
2026
+ rollbackConnectedState() {
2027
+ this.connected = false;
2028
+ this.localMemberId = null;
2029
+ for (const subscription of this.subscriptions.splice(0)) {
2030
+ subscription.unsubscribe();
2031
+ }
2032
+ for (const peer of this.peers.values()) {
2033
+ this.destroyPeer(peer);
2034
+ }
2035
+ this.peers.clear();
2036
+ this.remoteTrackKinds.clear();
2037
+ this.emittedRemoteTracks.clear();
2038
+ this.pendingRemoteTracks.clear();
2039
+ }
2040
+ destroyPeer(peer) {
2041
+ peer.pc.onicecandidate = null;
2042
+ peer.pc.onnegotiationneeded = null;
2043
+ peer.pc.ontrack = null;
2044
+ try {
2045
+ peer.pc.close();
2046
+ } catch {
2047
+ }
2048
+ }
2049
+ async createUserMediaTrack(kind, constraints) {
2050
+ const devices = await this.resolveMediaDevices();
2051
+ if (!devices?.getUserMedia || constraints === false) {
2052
+ return null;
2053
+ }
2054
+ const stream = await devices.getUserMedia(
2055
+ kind === "audio" ? { audio: constraints, video: false } : { audio: false, video: constraints }
2056
+ );
2057
+ return kind === "audio" ? stream.getAudioTracks()[0] ?? null : stream.getVideoTracks()[0] ?? null;
2058
+ }
2059
+ rememberLocalTrack(kind, track, deviceId, stopOnCleanup) {
2060
+ this.releaseLocalTrack(kind);
2061
+ this.localTracks.set(kind, {
2062
+ kind,
2063
+ track,
2064
+ deviceId,
2065
+ stopOnCleanup
2066
+ });
2067
+ }
2068
+ releaseLocalTrack(kind) {
2069
+ const local = this.localTracks.get(kind);
2070
+ if (!local) return;
2071
+ if (local.stopOnCleanup) {
2072
+ local.track.stop();
2073
+ }
2074
+ this.localTracks.delete(kind);
2075
+ }
2076
+ async ensureConnectedMemberId() {
2077
+ if (this.localMemberId) {
2078
+ return this.localMemberId;
2079
+ }
2080
+ return this.connect();
2081
+ }
2082
+ normalizeDescription(payload) {
2083
+ if (!payload || typeof payload !== "object") {
2084
+ return null;
2085
+ }
2086
+ const raw = payload.description;
2087
+ if (!raw || typeof raw.type !== "string") {
2088
+ return null;
2089
+ }
2090
+ return {
2091
+ type: raw.type,
2092
+ sdp: typeof raw.sdp === "string" ? raw.sdp : void 0
2093
+ };
2094
+ }
2095
+ normalizeCandidate(payload) {
2096
+ if (!payload || typeof payload !== "object") {
2097
+ return null;
2098
+ }
2099
+ const raw = payload.candidate;
2100
+ if (!raw || typeof raw.candidate !== "string") {
2101
+ return null;
2102
+ }
2103
+ return raw;
2104
+ }
2105
+ async ensureRuntime() {
2106
+ this.runtimePromise ??= import('@cloudflare/react-native-webrtc').then((mod) => {
2107
+ const runtime = mod;
2108
+ runtime.registerGlobals?.();
2109
+ return runtime;
2110
+ }).catch((error) => {
2111
+ throw new Error(`${installMessage2("@cloudflare/react-native-webrtc")}
2112
+ ${String(error)}`);
2113
+ });
2114
+ return this.runtimePromise;
2115
+ }
2116
+ async resolveMediaDevices() {
2117
+ if (this.options.mediaDevices) {
2118
+ return this.options.mediaDevices;
2119
+ }
2120
+ const runtime = await this.ensureRuntime();
2121
+ return runtime.mediaDevices ?? (typeof navigator !== "undefined" ? navigator.mediaDevices : void 0);
2122
+ }
2123
+ get offerEvent() {
2124
+ return `${this.options.signalPrefix}.offer`;
2125
+ }
2126
+ get answerEvent() {
2127
+ return `${this.options.signalPrefix}.answer`;
2128
+ }
2129
+ get iceEvent() {
2130
+ return `${this.options.signalPrefix}.ice`;
2131
+ }
2132
+ };
2133
+
2134
+ // src/room.ts
1025
2135
  var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1026
2136
  function deepSet(obj, path, value) {
1027
2137
  const parts = path.split(".");
@@ -1147,6 +2257,21 @@ var RoomClient = class _RoomClient {
1147
2257
  };
1148
2258
  members = {
1149
2259
  list: () => cloneValue(this._members),
2260
+ current: () => {
2261
+ const connectionId = this.currentConnectionId;
2262
+ if (connectionId) {
2263
+ const byConnection = this._members.find((member2) => member2.connectionId === connectionId);
2264
+ if (byConnection) {
2265
+ return cloneValue(byConnection);
2266
+ }
2267
+ }
2268
+ const userId = this.currentUserId;
2269
+ if (!userId) {
2270
+ return null;
2271
+ }
2272
+ const member = this._members.find((entry) => entry.userId === userId) ?? null;
2273
+ return member ? cloneValue(member) : null;
2274
+ },
1150
2275
  onSync: (handler) => this.onMembersSync(handler),
1151
2276
  onJoin: (handler) => this.onMemberJoin(handler),
1152
2277
  onLeave: (handler) => this.onMemberLeave(handler),
@@ -1181,6 +2306,18 @@ var RoomClient = class _RoomClient {
1181
2306
  devices: {
1182
2307
  switch: (payload) => this.switchMediaDevices(payload)
1183
2308
  },
2309
+ cloudflareRealtimeKit: {
2310
+ createSession: (payload) => this.requestCloudflareRealtimeKitMedia("session", "POST", payload)
2311
+ },
2312
+ transport: (options) => {
2313
+ const provider = options?.provider ?? "cloudflare_realtimekit";
2314
+ if (provider === "p2p") {
2315
+ const p2pOptions = options?.p2p;
2316
+ return new RoomP2PMediaTransport(this, p2pOptions);
2317
+ }
2318
+ const cloudflareOptions = options?.cloudflareRealtimeKit;
2319
+ return new RoomCloudflareMediaTransport(this, cloudflareOptions);
2320
+ },
1184
2321
  onTrack: (handler) => this.onMediaTrack(handler),
1185
2322
  onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
1186
2323
  onStateChange: (handler) => this.onMediaStateChange(handler),
@@ -1201,7 +2338,8 @@ var RoomClient = class _RoomClient {
1201
2338
  autoReconnect: options?.autoReconnect ?? true,
1202
2339
  maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
1203
2340
  reconnectBaseDelay: options?.reconnectBaseDelay ?? 1e3,
1204
- sendTimeout: options?.sendTimeout ?? 1e4
2341
+ sendTimeout: options?.sendTimeout ?? 1e4,
2342
+ connectionTimeout: options?.connectionTimeout ?? 15e3
1205
2343
  };
1206
2344
  this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
1207
2345
  this.handleAuthStateChange(user);
@@ -1224,6 +2362,36 @@ var RoomClient = class _RoomClient {
1224
2362
  async getMetadata() {
1225
2363
  return _RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
1226
2364
  }
2365
+ async requestCloudflareRealtimeKitMedia(path, method, payload) {
2366
+ return this.requestRoomMedia("cloudflare_realtimekit", path, method, payload);
2367
+ }
2368
+ async requestRoomMedia(providerPath, path, method, payload) {
2369
+ const token = await this.tokenManager.getAccessToken(
2370
+ (refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
2371
+ );
2372
+ if (!token) {
2373
+ throw new core.EdgeBaseError(401, "Authentication required");
2374
+ }
2375
+ const url = new URL(`${this.baseUrl.replace(/\/$/, "")}/api/room/media/${providerPath}/${path}`);
2376
+ url.searchParams.set("namespace", this.namespace);
2377
+ url.searchParams.set("id", this.roomId);
2378
+ const response = await fetch(url.toString(), {
2379
+ method,
2380
+ headers: {
2381
+ Authorization: `Bearer ${token}`,
2382
+ "Content-Type": "application/json"
2383
+ },
2384
+ body: method === "GET" ? void 0 : JSON.stringify(payload ?? {})
2385
+ });
2386
+ const data = await response.json().catch(() => ({}));
2387
+ if (!response.ok) {
2388
+ throw new core.EdgeBaseError(
2389
+ response.status,
2390
+ typeof data.message === "string" && data.message || `Room media request failed: ${response.statusText}`
2391
+ );
2392
+ }
2393
+ return data;
2394
+ }
1227
2395
  /**
1228
2396
  * Static: Get room metadata without creating a RoomClient instance.
1229
2397
  * Useful for lobby screens where you need room info before joining.
@@ -1283,6 +2451,30 @@ var RoomClient = class _RoomClient {
1283
2451
  this.reconnectInfo = null;
1284
2452
  this.setConnectionState("disconnected");
1285
2453
  }
2454
+ /** Destroy the room client, cleaning up all listeners and the auth subscription. */
2455
+ destroy() {
2456
+ this.leave();
2457
+ this.unsubAuthState?.();
2458
+ this.unsubAuthState = null;
2459
+ this.sharedStateHandlers.length = 0;
2460
+ this.playerStateHandlers.length = 0;
2461
+ this.messageHandlers.clear();
2462
+ this.allMessageHandlers.length = 0;
2463
+ this.errorHandlers.length = 0;
2464
+ this.kickedHandlers.length = 0;
2465
+ this.memberSyncHandlers.length = 0;
2466
+ this.memberJoinHandlers.length = 0;
2467
+ this.memberLeaveHandlers.length = 0;
2468
+ this.memberStateHandlers.length = 0;
2469
+ this.signalHandlers.clear();
2470
+ this.anySignalHandlers.length = 0;
2471
+ this.mediaTrackHandlers.length = 0;
2472
+ this.mediaTrackRemovedHandlers.length = 0;
2473
+ this.mediaStateHandlers.length = 0;
2474
+ this.mediaDeviceHandlers.length = 0;
2475
+ this.reconnectHandlers.length = 0;
2476
+ this.connectionStateHandlers.length = 0;
2477
+ }
1286
2478
  // ─── Actions ───
1287
2479
  /**
1288
2480
  * Send an action to the server.
@@ -1616,22 +2808,42 @@ var RoomClient = class _RoomClient {
1616
2808
  const wsUrl = this.buildWsUrl();
1617
2809
  const ws = new WebSocket(wsUrl);
1618
2810
  this.ws = ws;
2811
+ let settled = false;
2812
+ const connectionTimer = setTimeout(() => {
2813
+ if (!settled) {
2814
+ settled = true;
2815
+ try {
2816
+ ws.close();
2817
+ } catch (_) {
2818
+ }
2819
+ this.ws = null;
2820
+ reject(new core.EdgeBaseError(408, `Room WebSocket connection timed out after ${this.options.connectionTimeout}ms. Is the server running?`));
2821
+ }
2822
+ }, this.options.connectionTimeout);
1619
2823
  ws.onopen = () => {
2824
+ clearTimeout(connectionTimer);
1620
2825
  this.connected = true;
1621
2826
  this.reconnectAttempts = 0;
1622
2827
  this.startHeartbeat();
1623
2828
  this.authenticate().then(() => {
1624
- this.waitingForAuth = false;
1625
- resolve();
2829
+ if (!settled) {
2830
+ settled = true;
2831
+ this.waitingForAuth = false;
2832
+ resolve();
2833
+ }
1626
2834
  }).catch((error) => {
1627
- this.handleAuthenticationFailure(error);
1628
- reject(error);
2835
+ if (!settled) {
2836
+ settled = true;
2837
+ this.handleAuthenticationFailure(error);
2838
+ reject(error);
2839
+ }
1629
2840
  });
1630
2841
  };
1631
2842
  ws.onmessage = (event) => {
1632
2843
  this.handleMessage(event.data);
1633
2844
  };
1634
2845
  ws.onclose = (event) => {
2846
+ clearTimeout(connectionTimer);
1635
2847
  this.connected = false;
1636
2848
  this.authenticated = false;
1637
2849
  this.joined = false;
@@ -1640,6 +2852,9 @@ var RoomClient = class _RoomClient {
1640
2852
  if (event.code === 4004 && this.connectionState !== "kicked") {
1641
2853
  this.handleKicked();
1642
2854
  }
2855
+ if (!this.intentionallyLeft) {
2856
+ this.rejectAllPendingRequests(new core.EdgeBaseError(499, "WebSocket connection lost"));
2857
+ }
1643
2858
  if (!this.intentionallyLeft && !this.waitingForAuth && this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
1644
2859
  this.scheduleReconnect();
1645
2860
  } else if (!this.intentionallyLeft && this.connectionState !== "kicked" && this.connectionState !== "auth_lost") {
@@ -1647,7 +2862,11 @@ var RoomClient = class _RoomClient {
1647
2862
  }
1648
2863
  };
1649
2864
  ws.onerror = () => {
1650
- reject(new core.EdgeBaseError(500, "Room WebSocket connection error"));
2865
+ clearTimeout(connectionTimer);
2866
+ if (!settled) {
2867
+ settled = true;
2868
+ reject(new core.EdgeBaseError(500, "Room WebSocket connection error"));
2869
+ }
1651
2870
  };
1652
2871
  });
1653
2872
  }
@@ -2091,6 +3310,9 @@ var RoomClient = class _RoomClient {
2091
3310
  this.sendRaw({ type: "auth", token });
2092
3311
  }
2093
3312
  handleAuthStateChange(user) {
3313
+ if (user === null) {
3314
+ this.rejectAllPendingRequests(new core.EdgeBaseError(401, "Auth state lost"));
3315
+ }
2094
3316
  if (user) {
2095
3317
  if (this.ws && this.connected && this.authenticated) {
2096
3318
  this.refreshAuth();
@@ -2345,6 +3567,17 @@ var RoomClient = class _RoomClient {
2345
3567
  [kind]: next
2346
3568
  };
2347
3569
  }
3570
+ rejectAllPendingRequests(error) {
3571
+ for (const [, pending] of this.pendingRequests) {
3572
+ clearTimeout(pending.timeout);
3573
+ pending.reject(error);
3574
+ }
3575
+ this.pendingRequests.clear();
3576
+ this.rejectPendingVoidRequests(this.pendingSignalRequests, error);
3577
+ this.rejectPendingVoidRequests(this.pendingAdminRequests, error);
3578
+ this.rejectPendingVoidRequests(this.pendingMemberStateRequests, error);
3579
+ this.rejectPendingVoidRequests(this.pendingMediaRequests, error);
3580
+ }
2348
3581
  rejectPendingVoidRequests(pendingRequests, error) {
2349
3582
  for (const [, pending] of pendingRequests) {
2350
3583
  clearTimeout(pending.timeout);
@@ -2375,7 +3608,9 @@ var RoomClient = class _RoomClient {
2375
3608
  }
2376
3609
  scheduleReconnect() {
2377
3610
  const attempt = this.reconnectAttempts + 1;
2378
- const delay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
3611
+ const baseDelay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
3612
+ const jitter = Math.random() * baseDelay * 0.25;
3613
+ const delay = baseDelay + jitter;
2379
3614
  this.reconnectAttempts++;
2380
3615
  this.reconnectInfo = { attempt };
2381
3616
  this.setConnectionState("reconnecting");