@poolse/react 2.18.0 → 2.20.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.d.cts CHANGED
@@ -682,6 +682,33 @@ interface UseCalls {
682
682
  error: Error | null;
683
683
  /** Ring every other member of a conversation. */
684
684
  call: (conversationId: string, media?: CallMedia) => Promise<void>;
685
+ /**
686
+ * Register a ring that arrived by some route other than the socket —
687
+ * in practice a VoIP push.
688
+ *
689
+ * Without this the push path shows a ring the state machine knows
690
+ * nothing about, and every signal that ends a call is guarded on
691
+ * state the machine never had: the caller hanging up sent
692
+ * `call:cancelled`, the guard found no matching `incoming`, and the
693
+ * callee's phone rang on until its own timeout.
694
+ *
695
+ * Idempotent per call id, because the socket almost always delivers
696
+ * the same call a moment later — that is one ring arriving twice, not
697
+ * a second caller.
698
+ */
699
+ ring: (call: IncomingCall) => void;
700
+ /**
701
+ * A call was torn down from outside this state machine.
702
+ *
703
+ * The counterpart to {@link ring}: the caller withdrew, or the call
704
+ * ended, and the news reached this device as a VoIP push rather than
705
+ * over the socket — which is the normal case for a phone that was
706
+ * asleep when the ring arrived and whose socket may not be up yet.
707
+ *
708
+ * Ignores call ids this client is not in, so a teardown for a
709
+ * previous call cannot end the current one.
710
+ */
711
+ dropped: (callId: string) => void;
685
712
  /**
686
713
  * Answer the inbound ring.
687
714
  *
package/dist/index.d.ts CHANGED
@@ -682,6 +682,33 @@ interface UseCalls {
682
682
  error: Error | null;
683
683
  /** Ring every other member of a conversation. */
684
684
  call: (conversationId: string, media?: CallMedia) => Promise<void>;
685
+ /**
686
+ * Register a ring that arrived by some route other than the socket —
687
+ * in practice a VoIP push.
688
+ *
689
+ * Without this the push path shows a ring the state machine knows
690
+ * nothing about, and every signal that ends a call is guarded on
691
+ * state the machine never had: the caller hanging up sent
692
+ * `call:cancelled`, the guard found no matching `incoming`, and the
693
+ * callee's phone rang on until its own timeout.
694
+ *
695
+ * Idempotent per call id, because the socket almost always delivers
696
+ * the same call a moment later — that is one ring arriving twice, not
697
+ * a second caller.
698
+ */
699
+ ring: (call: IncomingCall) => void;
700
+ /**
701
+ * A call was torn down from outside this state machine.
702
+ *
703
+ * The counterpart to {@link ring}: the caller withdrew, or the call
704
+ * ended, and the news reached this device as a VoIP push rather than
705
+ * over the socket — which is the normal case for a phone that was
706
+ * asleep when the ring arrived and whose socket may not be up yet.
707
+ *
708
+ * Ignores call ids this client is not in, so a teardown for a
709
+ * previous call cannot end the current one.
710
+ */
711
+ dropped: (callId: string) => void;
685
712
  /**
686
713
  * Answer the inbound ring.
687
714
  *
package/dist/index.js CHANGED
@@ -1341,6 +1341,8 @@ function useCalls(userId, opts = {}) {
1341
1341
  const isCurrent = useCallback((callId) => {
1342
1342
  return callId === activeCallIdRef.current || callId === incomingRef.current?.callId || callId === outgoingRef.current?.callId;
1343
1343
  }, []);
1344
+ const rejectedRef = useRef(/* @__PURE__ */ new Map());
1345
+ const pendingRef = useRef(null);
1344
1346
  const actionsRef = useRef({
1345
1347
  cancel: async () => {
1346
1348
  },
@@ -1348,9 +1350,24 @@ function useCalls(userId, opts = {}) {
1348
1350
  }
1349
1351
  });
1350
1352
  const calls = useMemo(() => userId ? poolse.realtime.calls(userId) : null, [poolse, userId]);
1353
+ const reject = useCallback((callId, userId2, kind) => {
1354
+ if (phaseRef.current !== "ringing-out") return;
1355
+ const out = outgoingRef.current;
1356
+ if (!out || out.callId !== callId) return;
1357
+ rejectedRef.current.set(userId2, kind);
1358
+ const outstanding = out.calleeUserIds.filter((id) => !rejectedRef.current.has(id));
1359
+ if (out.calleeUserIds.length > 0 && outstanding.length > 0) return;
1360
+ const allBusy = [...rejectedRef.current.values()].every((k) => k === "busy");
1361
+ setPhase(allBusy ? "busy" : "declined");
1362
+ setOutgoing(null);
1363
+ setActiveCallId(null);
1364
+ setActiveMedia(null);
1365
+ }, []);
1351
1366
  useEffect(() => {
1352
1367
  if (!calls) return;
1353
1368
  const offIncoming = calls.onIncoming((call2) => {
1369
+ if (call2.callId === incomingRef.current?.callId) return;
1370
+ if (call2.callId === activeCallIdRef.current) return;
1354
1371
  if (phaseRef.current !== "idle") {
1355
1372
  void calls.markBusy(call2).catch(() => {
1356
1373
  });
@@ -1367,17 +1384,11 @@ function useCalls(userId, opts = {}) {
1367
1384
  });
1368
1385
  const offDeclined = calls.onDeclined((call2) => {
1369
1386
  if (!isCurrent(call2.callId)) return;
1370
- setPhase("declined");
1371
- setOutgoing(null);
1372
- setActiveCallId(null);
1373
- setActiveMedia(null);
1387
+ reject(call2.callId, call2.userId, "declined");
1374
1388
  });
1375
1389
  const offBusy = calls.onBusy((call2) => {
1376
1390
  if (!isCurrent(call2.callId)) return;
1377
- setPhase("busy");
1378
- setOutgoing(null);
1379
- setActiveCallId(null);
1380
- setActiveMedia(null);
1391
+ reject(call2.callId, call2.userId, "busy");
1381
1392
  });
1382
1393
  const offEnded = calls.onEnded((call2) => {
1383
1394
  if (!isCurrent(call2.callId)) return;
@@ -1403,11 +1414,22 @@ function useCalls(userId, opts = {}) {
1403
1414
  offEnded();
1404
1415
  offCancelled();
1405
1416
  };
1406
- }, [calls, isCurrent]);
1417
+ }, [calls, isCurrent, reject]);
1418
+ useEffect(() => {
1419
+ if (!calls) return;
1420
+ const queued = pendingRef.current;
1421
+ if (!queued) return;
1422
+ pendingRef.current = null;
1423
+ const sent = queued.kind === "accept" ? calls.accept(queued.call) : calls.decline(queued.call);
1424
+ void sent.catch((err) => {
1425
+ setError(err instanceof Error ? err : new Error(String(err)));
1426
+ });
1427
+ }, [calls]);
1407
1428
  const call = useCallback(
1408
1429
  async (conversationId, media2 = "audio") => {
1409
1430
  if (!calls) return;
1410
1431
  setError(null);
1432
+ rejectedRef.current = /* @__PURE__ */ new Map();
1411
1433
  try {
1412
1434
  const placed = await calls.invite(conversationId, media2);
1413
1435
  setOutgoing(placed);
@@ -1421,10 +1443,40 @@ function useCalls(userId, opts = {}) {
1421
1443
  },
1422
1444
  [calls]
1423
1445
  );
1446
+ const ring = useCallback((call2) => {
1447
+ if (incomingRef.current?.callId === call2.callId) return;
1448
+ if (activeCallIdRef.current === call2.callId) return;
1449
+ if (phaseRef.current !== "idle") return;
1450
+ setIncoming(call2);
1451
+ setPhase("ringing-in");
1452
+ }, []);
1453
+ const dropped = useCallback(
1454
+ (callId) => {
1455
+ if (pendingRef.current?.call.callId === callId) pendingRef.current = null;
1456
+ if (!isCurrent(callId)) return;
1457
+ rejectedRef.current = /* @__PURE__ */ new Map();
1458
+ setPhase("idle");
1459
+ setIncoming(null);
1460
+ setOutgoing(null);
1461
+ setActiveConversationId(null);
1462
+ setActiveCallId(null);
1463
+ setActiveMedia(null);
1464
+ },
1465
+ [isCurrent]
1466
+ );
1424
1467
  const accept = useCallback(
1425
1468
  async (call2) => {
1426
1469
  const target = call2 ?? incoming;
1427
- if (!calls || !target) return;
1470
+ if (!target) return;
1471
+ if (!calls) {
1472
+ pendingRef.current = { kind: "accept", call: target };
1473
+ setActiveConversationId(target.conversationId);
1474
+ setActiveCallId(target.callId);
1475
+ setActiveMedia(target.media);
1476
+ setPhase("active");
1477
+ setIncoming(null);
1478
+ return;
1479
+ }
1428
1480
  try {
1429
1481
  await calls.accept(target);
1430
1482
  setActiveConversationId(target.conversationId);
@@ -1441,7 +1493,15 @@ function useCalls(userId, opts = {}) {
1441
1493
  const decline = useCallback(
1442
1494
  async (call2) => {
1443
1495
  const target = call2 ?? incoming;
1444
- if (!calls || !target) return;
1496
+ if (!target) return;
1497
+ if (!calls) {
1498
+ pendingRef.current = { kind: "decline", call: target };
1499
+ setIncoming(null);
1500
+ setActiveCallId(null);
1501
+ setActiveMedia(null);
1502
+ setPhase("idle");
1503
+ return;
1504
+ }
1445
1505
  try {
1446
1506
  await calls.decline(target);
1447
1507
  } catch (err) {
@@ -1504,6 +1564,8 @@ function useCalls(userId, opts = {}) {
1504
1564
  }
1505
1565
  }, [calls, phase, incoming, outgoing, activeConversationId, activeCallId]);
1506
1566
  const reset = useCallback(() => {
1567
+ rejectedRef.current = /* @__PURE__ */ new Map();
1568
+ pendingRef.current = null;
1507
1569
  setPhase("idle");
1508
1570
  setIncoming(null);
1509
1571
  setOutgoing(null);
@@ -1521,6 +1583,8 @@ function useCalls(userId, opts = {}) {
1521
1583
  media,
1522
1584
  error,
1523
1585
  call,
1586
+ ring,
1587
+ dropped,
1524
1588
  accept,
1525
1589
  decline,
1526
1590
  cancel,