@coventia/softphone 0.1.0 → 0.2.1
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/{call-BTYlT2SU.d.cts → call-VKq1I8yP.d.cts} +158 -5
- package/dist/{call-BTYlT2SU.d.ts → call-VKq1I8yP.d.ts} +158 -5
- package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
- package/dist/chunk-7R6LOOAI.js.map +1 -0
- package/dist/{chunk-5BGEHMPP.js → chunk-EYEDPD7Y.js} +146 -20
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
- package/dist/chunk-LMJXX4YM.js.map +1 -0
- package/dist/element/index.d.ts +162 -7
- package/dist/element/index.js +866 -421
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-D9nbeY08.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
- package/dist/{engine-Doa6Brkm.d.ts → engine-C0BOkIcW.d.ts} +1 -1
- package/dist/index.cjs +586 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +335 -8
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +15 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +14 -3
- package/dist/react/index.d.ts +14 -3
- package/dist/react/index.js +2 -2
- package/dist/{softphone-DhuhcKCy.d.cts → softphone-Cyq3hiIM.d.ts} +17 -2
- package/dist/{softphone-DHmhotbf.d.ts → softphone-DlGV12Xp.d.cts} +17 -2
- package/dist/softphone.css +477 -35
- package/dist/{sources-N4BOEDRL.js → sources-IRKNQPWW.js} +108 -5
- package/dist/sources-IRKNQPWW.js.map +1 -0
- package/dist/testing/index.cjs +41 -5
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +8 -2
- package/dist/testing/index.d.ts +8 -2
- package/dist/testing/index.js +41 -5
- package/dist/testing/index.js.map +1 -1
- package/dist/ui/index.cjs +846 -410
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +17 -6
- package/dist/ui/index.d.ts +17 -6
- package/dist/ui/index.js +878 -456
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +144 -18
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/dist/zadarma/index.js +1 -1
- package/dist/zadarma-engine-L5DEFXUS.js +5 -0
- package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3V2L6UTK.js.map +0 -1
- package/dist/chunk-5BGEHMPP.js.map +0 -1
- package/dist/chunk-N2N2EQQJ.js.map +0 -1
- package/dist/sources-N4BOEDRL.js.map +0 -1
- package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
package/dist/zadarma/index.cjs
CHANGED
|
@@ -421,6 +421,14 @@ var SipCallEngine = class {
|
|
|
421
421
|
#uaInFlight = null;
|
|
422
422
|
#domain = "";
|
|
423
423
|
#activeCall = null;
|
|
424
|
+
/**
|
|
425
|
+
* The consult leg from `startConsult` (SPEC §5.6), if one is live on this UA right now.
|
|
426
|
+
*
|
|
427
|
+
* Tracked separately from `#activeCall` so `#maybeTeardownUA` can tell "both legs are actually gone"
|
|
428
|
+
* apart from "the main leg ended, but a consult is still up" — see that method's own comment for the
|
|
429
|
+
* bug this exists to prevent.
|
|
430
|
+
*/
|
|
431
|
+
#consultCall = null;
|
|
424
432
|
/** The chosen microphone, or `null` for the browser default. Read fresh by every new leg's
|
|
425
433
|
* `mediaConstraints`, and pushed live to `#activeCall` by `setInputDevice`. */
|
|
426
434
|
#inputDeviceId = null;
|
|
@@ -539,9 +547,7 @@ var SipCallEngine = class {
|
|
|
539
547
|
const ms = this.provider.quirks?.noInviteTeardownMs;
|
|
540
548
|
if (!ms) return;
|
|
541
549
|
this.#clearNoInviteTimer();
|
|
542
|
-
this.#noInviteTimer = setTimeout(() =>
|
|
543
|
-
if (!this.#activeCall) this.#teardownUA();
|
|
544
|
-
}, ms);
|
|
550
|
+
this.#noInviteTimer = setTimeout(() => this.#maybeTeardownUA(), ms);
|
|
545
551
|
}
|
|
546
552
|
/**
|
|
547
553
|
* Rebuild the incoming-signal channel with fresh credentials, after a registration failure.
|
|
@@ -663,6 +669,7 @@ var SipCallEngine = class {
|
|
|
663
669
|
}
|
|
664
670
|
this.#ua = null;
|
|
665
671
|
this.#activeCall = null;
|
|
672
|
+
this.#consultCall = null;
|
|
666
673
|
}
|
|
667
674
|
/** Place a call once the UA is registered (its 200 OK for REGISTER has arrived). */
|
|
668
675
|
/**
|
|
@@ -705,7 +712,7 @@ var SipCallEngine = class {
|
|
|
705
712
|
const code = missed ? "call/missed" : CALL_END_CODES[event.reason];
|
|
706
713
|
if (code) this.#reportError(code, event.cause);
|
|
707
714
|
if (this.#activeCall === call) this.#activeCall = null;
|
|
708
|
-
|
|
715
|
+
this.#maybeTeardownUA();
|
|
709
716
|
}
|
|
710
717
|
});
|
|
711
718
|
return call;
|
|
@@ -716,12 +723,38 @@ var SipCallEngine = class {
|
|
|
716
723
|
mediaConstraints: buildAudioConstraints(this.#inputDeviceId),
|
|
717
724
|
pcConfig: PC_CONFIG
|
|
718
725
|
});
|
|
719
|
-
|
|
726
|
+
const consultCall = new SipEngineCall(consultSession, {
|
|
720
727
|
id: nextCallId(),
|
|
721
728
|
buildTarget: (x) => this.#buildTarget(x),
|
|
722
729
|
placeConsult: () => Promise.reject(new Error("cannot consult from a consult leg")),
|
|
723
730
|
iceGatheringCapMs: this.provider.quirks?.iceGatheringCapMs ?? DEFAULT_ICE_CAP_MS
|
|
724
731
|
});
|
|
732
|
+
this.#consultCall = consultCall;
|
|
733
|
+
consultCall.onEvent((event) => {
|
|
734
|
+
if (event.type === "ended") {
|
|
735
|
+
if (this.#consultCall === consultCall) this.#consultCall = null;
|
|
736
|
+
this.#maybeTeardownUA();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
return consultCall;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* On-demand registration (SPEC §3.3): the UA is disposable once NOTHING is left on it — but "the
|
|
743
|
+
* main leg ended" (or "no INVITE followed this push") is not the same fact as "nothing is left."
|
|
744
|
+
* `completeTransfer()` (SPEC §5.6) ends the main leg via a REFER…Replaces BYE *while the consult leg
|
|
745
|
+
* is still what the PBX needs to bridge the caller onto* — tearing the UA down (and with it, EVERY
|
|
746
|
+
* session on it, via `terminateSessions`) the instant the main leg's `ended` fires killed that
|
|
747
|
+
* consult leg out from under the transfer before the bridge could use it, and the caller was simply
|
|
748
|
+
* gone. Two real call sites hit this independently (found live, 2026-08-04): the main leg's own
|
|
749
|
+
* `ended` handler, and `#armNoInviteTeardown`'s 5s timer — the latter re-arms on EVERY push,
|
|
750
|
+
* including unrelated "answered" pushes Zadarma sends around other account activity, so it can fire
|
|
751
|
+
* minutes after a transfer with no `#activeCall` and no idea a consult leg is mid-handoff. Both now
|
|
752
|
+
* route through here rather than checking `#activeCall` alone.
|
|
753
|
+
*/
|
|
754
|
+
#maybeTeardownUA() {
|
|
755
|
+
if (this.provider.registrationMode !== "on-demand") return;
|
|
756
|
+
if (this.#activeCall || this.#consultCall) return;
|
|
757
|
+
this.#teardownUA();
|
|
725
758
|
}
|
|
726
759
|
#startRejectGuard() {
|
|
727
760
|
const ms = this.provider.quirks?.rejectGuardMs;
|
|
@@ -756,6 +789,8 @@ async function loadJsSipFactory() {
|
|
|
756
789
|
// src/transports/providers/zadarma.ts
|
|
757
790
|
var PING = { localMs: 3e4, globalMs: 6e4 };
|
|
758
791
|
var KEY_REFRESH_BUFFER_MS = 2 * 60 * 60 * 1e3;
|
|
792
|
+
var REBUILD_CONNECT_TIMEOUT_MS = 1e4;
|
|
793
|
+
var REBUILD_RETRY_MS = 3e4;
|
|
759
794
|
var ZADARMA_QUIRKS = {
|
|
760
795
|
// Zadarma's registrar REFUSES to register a WebRTC client with any other User-Agent — a clean 401
|
|
761
796
|
// after a valid digest challenge, indistinguishable from a wrong password (§3.1). The old CRM
|
|
@@ -783,6 +818,12 @@ var ZadarmaPushSignal = class {
|
|
|
783
818
|
#connected = false;
|
|
784
819
|
#pingTimers = [];
|
|
785
820
|
#refreshTimer;
|
|
821
|
+
/** The replacement socket of a rebuild in flight, not yet current. */
|
|
822
|
+
#candidate = null;
|
|
823
|
+
/** Abandons the rebuild in flight (closing its candidate). */
|
|
824
|
+
#abortRebuild = null;
|
|
825
|
+
#rebuilding = false;
|
|
826
|
+
#retryTimer;
|
|
786
827
|
#unwatchBackground;
|
|
787
828
|
#closed = false;
|
|
788
829
|
#listeners = /* @__PURE__ */ new Set();
|
|
@@ -798,20 +839,35 @@ var ZadarmaPushSignal = class {
|
|
|
798
839
|
if (this.#closed) return;
|
|
799
840
|
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
800
841
|
this.#socket = socket;
|
|
842
|
+
this.#attach(socket);
|
|
843
|
+
this.#armRefresh(keyExpiresAt);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Wires one socket's events. `this.#socket` is read when an event FIRES, not when it is wired: a socket that
|
|
847
|
+
* has been replaced must stop driving connectivity (our own `close()` of it emits `disconnect`, which would
|
|
848
|
+
* otherwise flip the state to offline right after a perfectly healthy swap). `update` is the exception — it is
|
|
849
|
+
* forwarded from any socket not yet closed, because a push seen twice is harmless (the engine ignores a
|
|
850
|
+
* repeated signal mid-call and reuses a UA that already exists) while one seen by neither is a missed call.
|
|
851
|
+
*
|
|
852
|
+
* With `hooks`, the socket is a rebuild's candidate: its first `connect` or failure goes to the rebuild instead
|
|
853
|
+
* of changing the connection state (it is not current yet).
|
|
854
|
+
*/
|
|
855
|
+
#attach(socket, hooks) {
|
|
801
856
|
socket.on("connect", () => {
|
|
857
|
+
if (hooks && socket === this.#candidate) return hooks.onConnect();
|
|
858
|
+
if (socket !== this.#socket) return;
|
|
802
859
|
this.#setConnected(true);
|
|
803
860
|
this.#startPings(socket);
|
|
804
861
|
});
|
|
805
862
|
socket.on("update", () => this.#emit());
|
|
806
|
-
|
|
863
|
+
const lost = () => {
|
|
864
|
+
if (hooks && socket === this.#candidate) return hooks.onFailure();
|
|
865
|
+
if (socket !== this.#socket) return;
|
|
807
866
|
this.#stopPings();
|
|
808
867
|
this.#setConnected(false);
|
|
809
|
-
}
|
|
810
|
-
socket.on("
|
|
811
|
-
|
|
812
|
-
this.#setConnected(false);
|
|
813
|
-
});
|
|
814
|
-
this.#armRefresh(keyExpiresAt);
|
|
868
|
+
};
|
|
869
|
+
socket.on("disconnect", lost);
|
|
870
|
+
socket.on("connect_error", lost);
|
|
815
871
|
}
|
|
816
872
|
/**
|
|
817
873
|
* Presence pings — **the reason an idle tab keeps receiving calls**.
|
|
@@ -850,22 +906,92 @@ var ZadarmaPushSignal = class {
|
|
|
850
906
|
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
851
907
|
this.#refreshTimer = setTimeout(() => void this.#reopen(), ms);
|
|
852
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Replaces the push socket with a fresh one — MAKE-BEFORE-BREAK (presence-client-reporting.md §4.1, T8).
|
|
911
|
+
*
|
|
912
|
+
* Why it must happen at all: Zadarma's gateway decides the extension is alive from the raw Engine.IO ping, which
|
|
913
|
+
* a hidden tab's throttled timers silently stop sending, so while the tab is hidden the socket is rebuilt every
|
|
914
|
+
* 20 s (`push-socket-reliability.md` §5.3), and once before the key expires. Why it must not leave a gap: the
|
|
915
|
+
* old close-then-reopen reported `connected = false` for the ~0.5 s in between, which took the softphone
|
|
916
|
+
* `offline`, tore down the presence socket and left Zadarma with no socket to push an incoming call to.
|
|
917
|
+
*
|
|
918
|
+
* Now the OLD socket stays up and stays current until the NEW one has actually connected; only then do we swap,
|
|
919
|
+
* move the pings and close the old one. A healthy rebuild never changes `connected`. A failed one — the key fetch
|
|
920
|
+
* throws, the socket errors, or it has not connected within `REBUILD_CONNECT_TIMEOUT_MS` — keeps the old socket
|
|
921
|
+
* and retries, instead of leaving the tab disconnected. Zadarma does not evict the older of two sockets for one
|
|
922
|
+
* sip (multi-tab-leader-election.md §1), so the overlap is safe.
|
|
923
|
+
*/
|
|
853
924
|
async #reopen() {
|
|
854
|
-
if (this.#closed) return;
|
|
855
|
-
this.#
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
925
|
+
if (this.#closed || this.#rebuilding) return;
|
|
926
|
+
if (!this.#socket) {
|
|
927
|
+
try {
|
|
928
|
+
await this.#open();
|
|
929
|
+
} catch {
|
|
930
|
+
}
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
this.#rebuilding = true;
|
|
859
934
|
try {
|
|
860
|
-
await this
|
|
935
|
+
const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
|
|
936
|
+
if (this.#closed) return;
|
|
937
|
+
const swapped = await new Promise((resolve) => {
|
|
938
|
+
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
939
|
+
this.#candidate = socket;
|
|
940
|
+
let settled = false;
|
|
941
|
+
const finish = (ok) => {
|
|
942
|
+
if (settled) return;
|
|
943
|
+
settled = true;
|
|
944
|
+
clearTimeout(timer);
|
|
945
|
+
this.#abortRebuild = null;
|
|
946
|
+
if (ok && socket.connected) {
|
|
947
|
+
this.#swapIn(socket, keyExpiresAt);
|
|
948
|
+
} else {
|
|
949
|
+
this.#candidate = null;
|
|
950
|
+
socket.close();
|
|
951
|
+
}
|
|
952
|
+
resolve(ok && socket.connected);
|
|
953
|
+
};
|
|
954
|
+
const timer = setTimeout(() => finish(false), REBUILD_CONNECT_TIMEOUT_MS);
|
|
955
|
+
this.#abortRebuild = () => finish(false);
|
|
956
|
+
this.#attach(socket, { onConnect: () => finish(true), onFailure: () => finish(false) });
|
|
957
|
+
});
|
|
958
|
+
if (!swapped) this.#armRetry();
|
|
861
959
|
} catch {
|
|
960
|
+
this.#armRetry();
|
|
961
|
+
} finally {
|
|
962
|
+
this.#rebuilding = false;
|
|
862
963
|
}
|
|
863
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* The moment of the swap — synchronous, called from the candidate's own `connect` event so nothing can run
|
|
967
|
+
* between "the new socket is up" and "it is the current one". Order matters: `#socket` moves first, so the old
|
|
968
|
+
* socket's `disconnect` (our own `close()` of it, just below) is ignored by `#attach`.
|
|
969
|
+
*/
|
|
970
|
+
#swapIn(socket, keyExpiresAt) {
|
|
971
|
+
const previous = this.#socket;
|
|
972
|
+
this.#socket = socket;
|
|
973
|
+
this.#candidate = null;
|
|
974
|
+
clearTimeout(this.#retryTimer);
|
|
975
|
+
this.#retryTimer = void 0;
|
|
976
|
+
this.#stopPings();
|
|
977
|
+
this.#startPings(socket);
|
|
978
|
+
this.#setConnected(true);
|
|
979
|
+
previous?.close();
|
|
980
|
+
this.#armRefresh(keyExpiresAt);
|
|
981
|
+
}
|
|
982
|
+
#armRetry() {
|
|
983
|
+
if (this.#closed) return;
|
|
984
|
+
clearTimeout(this.#retryTimer);
|
|
985
|
+
this.#retryTimer = setTimeout(() => void this.#reopen(), REBUILD_RETRY_MS);
|
|
986
|
+
}
|
|
864
987
|
disconnect() {
|
|
865
988
|
this.#closed = true;
|
|
866
989
|
this.#stopPings();
|
|
867
990
|
clearTimeout(this.#refreshTimer);
|
|
868
991
|
this.#refreshTimer = void 0;
|
|
992
|
+
clearTimeout(this.#retryTimer);
|
|
993
|
+
this.#retryTimer = void 0;
|
|
994
|
+
this.#abortRebuild?.();
|
|
869
995
|
this.#unwatchBackground?.();
|
|
870
996
|
this.#unwatchBackground = void 0;
|
|
871
997
|
this.#socket?.close();
|