@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/CHANGELOG.md +26 -0
- package/dist/index.cjs +65 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +65 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to `@poolse/react` are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[semver](https://semver.org).
|
|
6
6
|
|
|
7
|
+
## [2.15.0] — 2026-08-23
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Answering a call then hanging up left the other side in it.**
|
|
12
|
+
`accept()` clears `incoming`, and a callee never had an `outgoing`, so
|
|
13
|
+
`hangUp()` resolved no call id and returned before signalling. The
|
|
14
|
+
call id of the call in progress is now tracked in its own right.
|
|
15
|
+
- **A late `call:cancelled` tore down a live call.** The guard fell
|
|
16
|
+
through whenever `incoming` was null — exactly the state `accept()`
|
|
17
|
+
leaves behind — so a caller's ring timeout firing as the callee
|
|
18
|
+
answered dropped the callee straight out of the call.
|
|
19
|
+
- **Every call signal is now matched to the call it belongs to.**
|
|
20
|
+
`accepted`, `declined`, `busy`, `ended` and `cancelled` are ignored
|
|
21
|
+
unless they name the call this client is actually in. A signal about a
|
|
22
|
+
previous call could previously end the current one.
|
|
23
|
+
- **A room could be left connected with nothing holding it.** If a call
|
|
24
|
+
ended while its media was still connecting, the cleanup ran before
|
|
25
|
+
`join()` resolved, found nothing to release, and the connection
|
|
26
|
+
completed into a room no component held — audio still flowing, no UI,
|
|
27
|
+
and no way to hang up. Both `useCallRoom` and `useVoiceRoom` now
|
|
28
|
+
release a room that was superseded mid-connect.
|
|
29
|
+
- **`markBusy` could reject a call the user was being shown.** The
|
|
30
|
+
decision ran inside a `setPhase` updater; updaters must be pure, and
|
|
31
|
+
React may run them more than once.
|
|
32
|
+
|
|
7
33
|
## [2.14.1] — 2026-08-23
|
|
8
34
|
|
|
9
35
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -1082,6 +1082,7 @@ function useCallRoom(conversationId, opts) {
|
|
|
1082
1082
|
const optsRef = react.useRef(opts);
|
|
1083
1083
|
optsRef.current = opts;
|
|
1084
1084
|
const joined = react.useRef(false);
|
|
1085
|
+
const held = react.useRef(null);
|
|
1085
1086
|
const room = react.useMemo(() => {
|
|
1086
1087
|
if (!conversationId) return null;
|
|
1087
1088
|
return poolse.call(conversationId, {
|
|
@@ -1121,6 +1122,7 @@ function useCallRoom(conversationId, opts) {
|
|
|
1121
1122
|
}
|
|
1122
1123
|
setStatus(room.getStatus());
|
|
1123
1124
|
setParticipants(room.getParticipants());
|
|
1125
|
+
held.current = room;
|
|
1124
1126
|
setAlone(false);
|
|
1125
1127
|
const offStatus = room.onStatus(setStatus);
|
|
1126
1128
|
const offRoster = room.onParticipants((next) => {
|
|
@@ -1136,6 +1138,7 @@ function useCallRoom(conversationId, opts) {
|
|
|
1136
1138
|
offAlone();
|
|
1137
1139
|
offDevices();
|
|
1138
1140
|
offError();
|
|
1141
|
+
held.current = null;
|
|
1139
1142
|
if (joined.current) {
|
|
1140
1143
|
joined.current = false;
|
|
1141
1144
|
room.leave();
|
|
@@ -1143,14 +1146,19 @@ function useCallRoom(conversationId, opts) {
|
|
|
1143
1146
|
};
|
|
1144
1147
|
}, [room, sync, refreshDevices]);
|
|
1145
1148
|
const join = react.useCallback(async () => {
|
|
1146
|
-
|
|
1149
|
+
const target = room;
|
|
1150
|
+
if (!target) return false;
|
|
1147
1151
|
setError(null);
|
|
1148
1152
|
try {
|
|
1149
|
-
const connected = await
|
|
1153
|
+
const connected = await target.join();
|
|
1154
|
+
if (held.current !== target) {
|
|
1155
|
+
if (connected) target.leave();
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1150
1158
|
joined.current = connected;
|
|
1151
1159
|
setSfuAvailable(connected);
|
|
1152
|
-
sync(
|
|
1153
|
-
if (connected) refreshDevices(
|
|
1160
|
+
sync(target);
|
|
1161
|
+
if (connected) refreshDevices(target);
|
|
1154
1162
|
return connected;
|
|
1155
1163
|
} catch (err) {
|
|
1156
1164
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1257,12 +1265,14 @@ function useVoiceRoom(conversationId, opts = {}) {
|
|
|
1257
1265
|
const [error, setError] = react.useState(null);
|
|
1258
1266
|
const optsRef = react.useRef(opts);
|
|
1259
1267
|
const joined = react.useRef(false);
|
|
1268
|
+
const held = react.useRef(null);
|
|
1260
1269
|
const room = react.useMemo(
|
|
1261
1270
|
() => conversationId ? poolse.realtime.voice(conversationId, optsRef.current) : null,
|
|
1262
1271
|
[poolse, conversationId]
|
|
1263
1272
|
);
|
|
1264
1273
|
react.useEffect(() => {
|
|
1265
1274
|
if (!room) return;
|
|
1275
|
+
held.current = room;
|
|
1266
1276
|
setStatus(room.getStatus());
|
|
1267
1277
|
setParticipants(room.getParticipants());
|
|
1268
1278
|
setMuted(room.isMuted());
|
|
@@ -1273,6 +1283,7 @@ function useVoiceRoom(conversationId, opts = {}) {
|
|
|
1273
1283
|
offStatus();
|
|
1274
1284
|
offRoster();
|
|
1275
1285
|
offError();
|
|
1286
|
+
held.current = null;
|
|
1276
1287
|
if (joined.current) {
|
|
1277
1288
|
joined.current = false;
|
|
1278
1289
|
room.leave();
|
|
@@ -1280,10 +1291,15 @@ function useVoiceRoom(conversationId, opts = {}) {
|
|
|
1280
1291
|
};
|
|
1281
1292
|
}, [room]);
|
|
1282
1293
|
const join = react.useCallback(async () => {
|
|
1283
|
-
|
|
1294
|
+
const target = room;
|
|
1295
|
+
if (!target) return;
|
|
1284
1296
|
setError(null);
|
|
1285
1297
|
try {
|
|
1286
|
-
await
|
|
1298
|
+
await target.join();
|
|
1299
|
+
if (held.current !== target) {
|
|
1300
|
+
target.leave();
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1287
1303
|
joined.current = true;
|
|
1288
1304
|
} catch (err) {
|
|
1289
1305
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1309,10 +1325,22 @@ function useCalls(userId, opts = {}) {
|
|
|
1309
1325
|
const ringTimeoutMs = opts.ringTimeoutMs ?? 45e3;
|
|
1310
1326
|
const poolse = usePoolse();
|
|
1311
1327
|
const [phase, setPhase] = react.useState("idle");
|
|
1328
|
+
const phaseRef = react.useRef("idle");
|
|
1329
|
+
phaseRef.current = phase;
|
|
1312
1330
|
const [incoming, setIncoming] = react.useState(null);
|
|
1313
1331
|
const [outgoing, setOutgoing] = react.useState(null);
|
|
1314
1332
|
const [activeConversationId, setActiveConversationId] = react.useState(null);
|
|
1333
|
+
const [activeCallId, setActiveCallId] = react.useState(null);
|
|
1315
1334
|
const [error, setError] = react.useState(null);
|
|
1335
|
+
const incomingRef = react.useRef(null);
|
|
1336
|
+
incomingRef.current = incoming;
|
|
1337
|
+
const outgoingRef = react.useRef(null);
|
|
1338
|
+
outgoingRef.current = outgoing;
|
|
1339
|
+
const activeCallIdRef = react.useRef(null);
|
|
1340
|
+
activeCallIdRef.current = activeCallId;
|
|
1341
|
+
const isCurrent = react.useCallback((callId) => {
|
|
1342
|
+
return callId === activeCallIdRef.current || callId === incomingRef.current?.callId || callId === outgoingRef.current?.callId;
|
|
1343
|
+
}, []);
|
|
1316
1344
|
const actionsRef = react.useRef({
|
|
1317
1345
|
cancel: async () => {
|
|
1318
1346
|
},
|
|
@@ -1323,40 +1351,45 @@ function useCalls(userId, opts = {}) {
|
|
|
1323
1351
|
react.useEffect(() => {
|
|
1324
1352
|
if (!calls) return;
|
|
1325
1353
|
const offIncoming = calls.onIncoming((call2) => {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
return "ringing-in";
|
|
1334
|
-
});
|
|
1354
|
+
if (phaseRef.current !== "idle") {
|
|
1355
|
+
void calls.markBusy(call2).catch(() => {
|
|
1356
|
+
});
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
setIncoming(call2);
|
|
1360
|
+
setPhase("ringing-in");
|
|
1335
1361
|
});
|
|
1336
1362
|
const offAccepted = calls.onAccepted((call2) => {
|
|
1363
|
+
if (outgoingRef.current && call2.callId !== outgoingRef.current.callId) return;
|
|
1337
1364
|
setPhase("active");
|
|
1338
1365
|
setActiveConversationId(call2.conversationId);
|
|
1366
|
+
setActiveCallId(call2.callId);
|
|
1339
1367
|
});
|
|
1340
|
-
const offDeclined = calls.onDeclined(() => {
|
|
1368
|
+
const offDeclined = calls.onDeclined((call2) => {
|
|
1369
|
+
if (!isCurrent(call2.callId)) return;
|
|
1341
1370
|
setPhase("declined");
|
|
1342
1371
|
setOutgoing(null);
|
|
1372
|
+
setActiveCallId(null);
|
|
1343
1373
|
});
|
|
1344
|
-
const offBusy = calls.onBusy(() => {
|
|
1374
|
+
const offBusy = calls.onBusy((call2) => {
|
|
1375
|
+
if (!isCurrent(call2.callId)) return;
|
|
1345
1376
|
setPhase("busy");
|
|
1346
1377
|
setOutgoing(null);
|
|
1378
|
+
setActiveCallId(null);
|
|
1347
1379
|
});
|
|
1348
|
-
const offEnded = calls.onEnded(() => {
|
|
1380
|
+
const offEnded = calls.onEnded((call2) => {
|
|
1381
|
+
if (!isCurrent(call2.callId)) return;
|
|
1349
1382
|
setPhase("idle");
|
|
1350
1383
|
setIncoming(null);
|
|
1351
1384
|
setOutgoing(null);
|
|
1352
1385
|
setActiveConversationId(null);
|
|
1386
|
+
setActiveCallId(null);
|
|
1353
1387
|
});
|
|
1354
1388
|
const offCancelled = calls.onCancelled((call2) => {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
});
|
|
1389
|
+
if (call2.callId !== incomingRef.current?.callId) return;
|
|
1390
|
+
setIncoming(null);
|
|
1391
|
+
setPhase("idle");
|
|
1392
|
+
setActiveCallId(null);
|
|
1360
1393
|
});
|
|
1361
1394
|
return () => {
|
|
1362
1395
|
offIncoming();
|
|
@@ -1366,7 +1399,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1366
1399
|
offEnded();
|
|
1367
1400
|
offCancelled();
|
|
1368
1401
|
};
|
|
1369
|
-
}, [calls]);
|
|
1402
|
+
}, [calls, isCurrent]);
|
|
1370
1403
|
const call = react.useCallback(
|
|
1371
1404
|
async (conversationId, media2 = "audio") => {
|
|
1372
1405
|
if (!calls) return;
|
|
@@ -1374,6 +1407,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1374
1407
|
try {
|
|
1375
1408
|
const placed = await calls.invite(conversationId, media2);
|
|
1376
1409
|
setOutgoing(placed);
|
|
1410
|
+
setActiveCallId(placed.callId);
|
|
1377
1411
|
setPhase("ringing-out");
|
|
1378
1412
|
} catch (err) {
|
|
1379
1413
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1389,6 +1423,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1389
1423
|
try {
|
|
1390
1424
|
await calls.accept(target);
|
|
1391
1425
|
setActiveConversationId(target.conversationId);
|
|
1426
|
+
setActiveCallId(target.callId);
|
|
1392
1427
|
setPhase("active");
|
|
1393
1428
|
setIncoming(null);
|
|
1394
1429
|
} catch (err) {
|
|
@@ -1407,6 +1442,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1407
1442
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1408
1443
|
} finally {
|
|
1409
1444
|
setIncoming(null);
|
|
1445
|
+
setActiveCallId(null);
|
|
1410
1446
|
setPhase("idle");
|
|
1411
1447
|
}
|
|
1412
1448
|
},
|
|
@@ -1420,6 +1456,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1420
1456
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1421
1457
|
} finally {
|
|
1422
1458
|
setOutgoing(null);
|
|
1459
|
+
setActiveCallId(null);
|
|
1423
1460
|
setPhase("idle");
|
|
1424
1461
|
}
|
|
1425
1462
|
}, [calls, outgoing]);
|
|
@@ -1437,7 +1474,7 @@ function useCalls(userId, opts = {}) {
|
|
|
1437
1474
|
}, [phase, ringTimeoutMs]);
|
|
1438
1475
|
const hangUp = react.useCallback(async () => {
|
|
1439
1476
|
const conversationId = activeConversationId ?? incoming?.conversationId ?? outgoing?.conversationId;
|
|
1440
|
-
const callId = incoming?.callId ?? outgoing?.callId;
|
|
1477
|
+
const callId = activeCallId ?? incoming?.callId ?? outgoing?.callId;
|
|
1441
1478
|
try {
|
|
1442
1479
|
if (!calls || !callId || !conversationId) return;
|
|
1443
1480
|
if (phase === "ringing-out" && outgoing) {
|
|
@@ -1454,13 +1491,15 @@ function useCalls(userId, opts = {}) {
|
|
|
1454
1491
|
setIncoming(null);
|
|
1455
1492
|
setOutgoing(null);
|
|
1456
1493
|
setActiveConversationId(null);
|
|
1494
|
+
setActiveCallId(null);
|
|
1457
1495
|
}
|
|
1458
|
-
}, [calls, phase, incoming, outgoing, activeConversationId]);
|
|
1496
|
+
}, [calls, phase, incoming, outgoing, activeConversationId, activeCallId]);
|
|
1459
1497
|
const reset = react.useCallback(() => {
|
|
1460
1498
|
setPhase("idle");
|
|
1461
1499
|
setIncoming(null);
|
|
1462
1500
|
setOutgoing(null);
|
|
1463
1501
|
setActiveConversationId(null);
|
|
1502
|
+
setActiveCallId(null);
|
|
1464
1503
|
setError(null);
|
|
1465
1504
|
}, []);
|
|
1466
1505
|
const media = incoming?.media ?? outgoing?.media ?? "audio";
|