@poolse/react 2.14.1 → 2.15.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.js CHANGED
@@ -1081,6 +1081,7 @@ function useCallRoom(conversationId, opts) {
1081
1081
  const optsRef = useRef(opts);
1082
1082
  optsRef.current = opts;
1083
1083
  const joined = useRef(false);
1084
+ const held = useRef(null);
1084
1085
  const room = useMemo(() => {
1085
1086
  if (!conversationId) return null;
1086
1087
  return poolse.call(conversationId, {
@@ -1120,6 +1121,7 @@ function useCallRoom(conversationId, opts) {
1120
1121
  }
1121
1122
  setStatus(room.getStatus());
1122
1123
  setParticipants(room.getParticipants());
1124
+ held.current = room;
1123
1125
  setAlone(false);
1124
1126
  const offStatus = room.onStatus(setStatus);
1125
1127
  const offRoster = room.onParticipants((next) => {
@@ -1135,6 +1137,7 @@ function useCallRoom(conversationId, opts) {
1135
1137
  offAlone();
1136
1138
  offDevices();
1137
1139
  offError();
1140
+ held.current = null;
1138
1141
  if (joined.current) {
1139
1142
  joined.current = false;
1140
1143
  room.leave();
@@ -1142,14 +1145,19 @@ function useCallRoom(conversationId, opts) {
1142
1145
  };
1143
1146
  }, [room, sync, refreshDevices]);
1144
1147
  const join = useCallback(async () => {
1145
- if (!room) return false;
1148
+ const target = room;
1149
+ if (!target) return false;
1146
1150
  setError(null);
1147
1151
  try {
1148
- const connected = await room.join();
1152
+ const connected = await target.join();
1153
+ if (held.current !== target) {
1154
+ if (connected) target.leave();
1155
+ return false;
1156
+ }
1149
1157
  joined.current = connected;
1150
1158
  setSfuAvailable(connected);
1151
- sync(room);
1152
- if (connected) refreshDevices(room);
1159
+ sync(target);
1160
+ if (connected) refreshDevices(target);
1153
1161
  return connected;
1154
1162
  } catch (err) {
1155
1163
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1256,12 +1264,14 @@ function useVoiceRoom(conversationId, opts = {}) {
1256
1264
  const [error, setError] = useState(null);
1257
1265
  const optsRef = useRef(opts);
1258
1266
  const joined = useRef(false);
1267
+ const held = useRef(null);
1259
1268
  const room = useMemo(
1260
1269
  () => conversationId ? poolse.realtime.voice(conversationId, optsRef.current) : null,
1261
1270
  [poolse, conversationId]
1262
1271
  );
1263
1272
  useEffect(() => {
1264
1273
  if (!room) return;
1274
+ held.current = room;
1265
1275
  setStatus(room.getStatus());
1266
1276
  setParticipants(room.getParticipants());
1267
1277
  setMuted(room.isMuted());
@@ -1272,6 +1282,7 @@ function useVoiceRoom(conversationId, opts = {}) {
1272
1282
  offStatus();
1273
1283
  offRoster();
1274
1284
  offError();
1285
+ held.current = null;
1275
1286
  if (joined.current) {
1276
1287
  joined.current = false;
1277
1288
  room.leave();
@@ -1279,10 +1290,15 @@ function useVoiceRoom(conversationId, opts = {}) {
1279
1290
  };
1280
1291
  }, [room]);
1281
1292
  const join = useCallback(async () => {
1282
- if (!room) return;
1293
+ const target = room;
1294
+ if (!target) return;
1283
1295
  setError(null);
1284
1296
  try {
1285
- await room.join();
1297
+ await target.join();
1298
+ if (held.current !== target) {
1299
+ target.leave();
1300
+ return;
1301
+ }
1286
1302
  joined.current = true;
1287
1303
  } catch (err) {
1288
1304
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1308,10 +1324,22 @@ function useCalls(userId, opts = {}) {
1308
1324
  const ringTimeoutMs = opts.ringTimeoutMs ?? 45e3;
1309
1325
  const poolse = usePoolse();
1310
1326
  const [phase, setPhase] = useState("idle");
1327
+ const phaseRef = useRef("idle");
1328
+ phaseRef.current = phase;
1311
1329
  const [incoming, setIncoming] = useState(null);
1312
1330
  const [outgoing, setOutgoing] = useState(null);
1313
1331
  const [activeConversationId, setActiveConversationId] = useState(null);
1332
+ const [activeCallId, setActiveCallId] = useState(null);
1314
1333
  const [error, setError] = useState(null);
1334
+ const incomingRef = useRef(null);
1335
+ incomingRef.current = incoming;
1336
+ const outgoingRef = useRef(null);
1337
+ outgoingRef.current = outgoing;
1338
+ const activeCallIdRef = useRef(null);
1339
+ activeCallIdRef.current = activeCallId;
1340
+ const isCurrent = useCallback((callId) => {
1341
+ return callId === activeCallIdRef.current || callId === incomingRef.current?.callId || callId === outgoingRef.current?.callId;
1342
+ }, []);
1315
1343
  const actionsRef = useRef({
1316
1344
  cancel: async () => {
1317
1345
  },
@@ -1322,40 +1350,45 @@ function useCalls(userId, opts = {}) {
1322
1350
  useEffect(() => {
1323
1351
  if (!calls) return;
1324
1352
  const offIncoming = calls.onIncoming((call2) => {
1325
- setPhase((current) => {
1326
- if (current !== "idle") {
1327
- void calls.markBusy(call2).catch(() => {
1328
- });
1329
- return current;
1330
- }
1331
- setIncoming(call2);
1332
- return "ringing-in";
1333
- });
1353
+ if (phaseRef.current !== "idle") {
1354
+ void calls.markBusy(call2).catch(() => {
1355
+ });
1356
+ return;
1357
+ }
1358
+ setIncoming(call2);
1359
+ setPhase("ringing-in");
1334
1360
  });
1335
1361
  const offAccepted = calls.onAccepted((call2) => {
1362
+ if (outgoingRef.current && call2.callId !== outgoingRef.current.callId) return;
1336
1363
  setPhase("active");
1337
1364
  setActiveConversationId(call2.conversationId);
1365
+ setActiveCallId(call2.callId);
1338
1366
  });
1339
- const offDeclined = calls.onDeclined(() => {
1367
+ const offDeclined = calls.onDeclined((call2) => {
1368
+ if (!isCurrent(call2.callId)) return;
1340
1369
  setPhase("declined");
1341
1370
  setOutgoing(null);
1371
+ setActiveCallId(null);
1342
1372
  });
1343
- const offBusy = calls.onBusy(() => {
1373
+ const offBusy = calls.onBusy((call2) => {
1374
+ if (!isCurrent(call2.callId)) return;
1344
1375
  setPhase("busy");
1345
1376
  setOutgoing(null);
1377
+ setActiveCallId(null);
1346
1378
  });
1347
- const offEnded = calls.onEnded(() => {
1379
+ const offEnded = calls.onEnded((call2) => {
1380
+ if (!isCurrent(call2.callId)) return;
1348
1381
  setPhase("idle");
1349
1382
  setIncoming(null);
1350
1383
  setOutgoing(null);
1351
1384
  setActiveConversationId(null);
1385
+ setActiveCallId(null);
1352
1386
  });
1353
1387
  const offCancelled = calls.onCancelled((call2) => {
1354
- setIncoming((current) => {
1355
- if (current && current.callId !== call2.callId) return current;
1356
- setPhase("idle");
1357
- return null;
1358
- });
1388
+ if (call2.callId !== incomingRef.current?.callId) return;
1389
+ setIncoming(null);
1390
+ setPhase("idle");
1391
+ setActiveCallId(null);
1359
1392
  });
1360
1393
  return () => {
1361
1394
  offIncoming();
@@ -1365,7 +1398,7 @@ function useCalls(userId, opts = {}) {
1365
1398
  offEnded();
1366
1399
  offCancelled();
1367
1400
  };
1368
- }, [calls]);
1401
+ }, [calls, isCurrent]);
1369
1402
  const call = useCallback(
1370
1403
  async (conversationId, media2 = "audio") => {
1371
1404
  if (!calls) return;
@@ -1373,6 +1406,7 @@ function useCalls(userId, opts = {}) {
1373
1406
  try {
1374
1407
  const placed = await calls.invite(conversationId, media2);
1375
1408
  setOutgoing(placed);
1409
+ setActiveCallId(placed.callId);
1376
1410
  setPhase("ringing-out");
1377
1411
  } catch (err) {
1378
1412
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1388,6 +1422,7 @@ function useCalls(userId, opts = {}) {
1388
1422
  try {
1389
1423
  await calls.accept(target);
1390
1424
  setActiveConversationId(target.conversationId);
1425
+ setActiveCallId(target.callId);
1391
1426
  setPhase("active");
1392
1427
  setIncoming(null);
1393
1428
  } catch (err) {
@@ -1406,6 +1441,7 @@ function useCalls(userId, opts = {}) {
1406
1441
  setError(err instanceof Error ? err : new Error(String(err)));
1407
1442
  } finally {
1408
1443
  setIncoming(null);
1444
+ setActiveCallId(null);
1409
1445
  setPhase("idle");
1410
1446
  }
1411
1447
  },
@@ -1419,6 +1455,7 @@ function useCalls(userId, opts = {}) {
1419
1455
  setError(err instanceof Error ? err : new Error(String(err)));
1420
1456
  } finally {
1421
1457
  setOutgoing(null);
1458
+ setActiveCallId(null);
1422
1459
  setPhase("idle");
1423
1460
  }
1424
1461
  }, [calls, outgoing]);
@@ -1436,7 +1473,7 @@ function useCalls(userId, opts = {}) {
1436
1473
  }, [phase, ringTimeoutMs]);
1437
1474
  const hangUp = useCallback(async () => {
1438
1475
  const conversationId = activeConversationId ?? incoming?.conversationId ?? outgoing?.conversationId;
1439
- const callId = incoming?.callId ?? outgoing?.callId;
1476
+ const callId = activeCallId ?? incoming?.callId ?? outgoing?.callId;
1440
1477
  try {
1441
1478
  if (!calls || !callId || !conversationId) return;
1442
1479
  if (phase === "ringing-out" && outgoing) {
@@ -1453,13 +1490,15 @@ function useCalls(userId, opts = {}) {
1453
1490
  setIncoming(null);
1454
1491
  setOutgoing(null);
1455
1492
  setActiveConversationId(null);
1493
+ setActiveCallId(null);
1456
1494
  }
1457
- }, [calls, phase, incoming, outgoing, activeConversationId]);
1495
+ }, [calls, phase, incoming, outgoing, activeConversationId, activeCallId]);
1458
1496
  const reset = useCallback(() => {
1459
1497
  setPhase("idle");
1460
1498
  setIncoming(null);
1461
1499
  setOutgoing(null);
1462
1500
  setActiveConversationId(null);
1501
+ setActiveCallId(null);
1463
1502
  setError(null);
1464
1503
  }, []);
1465
1504
  const media = incoming?.media ?? outgoing?.media ?? "audio";