@coventia/softphone 0.1.0 → 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/{call-BTYlT2SU.d.cts → call-DUVnDxsz.d.cts} +152 -5
- package/dist/{call-BTYlT2SU.d.ts → call-DUVnDxsz.d.ts} +152 -5
- package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
- package/dist/chunk-7R6LOOAI.js.map +1 -0
- package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
- package/dist/chunk-LMJXX4YM.js.map +1 -0
- package/dist/{chunk-5BGEHMPP.js → chunk-YWI6EQYS.js} +40 -7
- package/dist/chunk-YWI6EQYS.js.map +1 -0
- package/dist/element/index.d.ts +154 -5
- package/dist/element/index.js +512 -152
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-D9nbeY08.d.cts → engine-CDjkrXHY.d.cts} +1 -1
- package/dist/{engine-Doa6Brkm.d.ts → engine-EPCKf-Bl.d.ts} +1 -1
- package/dist/index.cjs +396 -14
- 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 +256 -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-BtdgZWkm.d.ts} +17 -2
- package/dist/{softphone-DHmhotbf.d.ts → softphone-DHG9MTNg.d.cts} +17 -2
- package/dist/softphone.css +395 -1
- package/dist/{sources-N4BOEDRL.js → sources-5L2R4HTV.js} +104 -5
- package/dist/sources-5L2R4HTV.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 +490 -139
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +2 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +478 -141
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +38 -5
- 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-4VOPIBXG.js +5 -0
- package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-4VOPIBXG.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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-
|
|
1
|
+
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-DUVnDxsz.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-
|
|
1
|
+
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-DUVnDxsz.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
|
package/dist/index.cjs
CHANGED
|
@@ -92,7 +92,7 @@ var init_token_cache = __esm({
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
// src/coventia/api.ts
|
|
95
|
-
function
|
|
95
|
+
function createRequest(config, method) {
|
|
96
96
|
const doFetch = config.fetch ? config.fetch : (...args) => globalThis.fetch(...args);
|
|
97
97
|
const base = config.baseUrl.replace(/\/+$/, "");
|
|
98
98
|
async function attempt(path, params, signal) {
|
|
@@ -103,6 +103,7 @@ function createGet(config) {
|
|
|
103
103
|
}
|
|
104
104
|
const query = qs.toString();
|
|
105
105
|
const res = await doFetch(`${base}${path}${query ? `?${query}` : ""}`, {
|
|
106
|
+
method,
|
|
106
107
|
// The RAW token, no `Bearer ` prefix — see `VoiceToken.token` (SPEC §6.1).
|
|
107
108
|
headers: { Authorization: token },
|
|
108
109
|
...signal ? { signal } : {}
|
|
@@ -115,7 +116,7 @@ function createGet(config) {
|
|
|
115
116
|
}
|
|
116
117
|
return body;
|
|
117
118
|
}
|
|
118
|
-
return async function
|
|
119
|
+
return async function request(path, params = {}, signal) {
|
|
119
120
|
try {
|
|
120
121
|
return await attempt(path, params, signal);
|
|
121
122
|
} catch (error) {
|
|
@@ -125,6 +126,12 @@ function createGet(config) {
|
|
|
125
126
|
}
|
|
126
127
|
};
|
|
127
128
|
}
|
|
129
|
+
function createGet(config) {
|
|
130
|
+
return createRequest(config, "GET");
|
|
131
|
+
}
|
|
132
|
+
function createPost(config) {
|
|
133
|
+
return createRequest(config, "POST");
|
|
134
|
+
}
|
|
128
135
|
var ApiError;
|
|
129
136
|
var init_api = __esm({
|
|
130
137
|
"src/coventia/api.ts"() {
|
|
@@ -167,6 +174,7 @@ function resolverFromContacts(contacts) {
|
|
|
167
174
|
}
|
|
168
175
|
function coventiaSources(config) {
|
|
169
176
|
const get = createGet(config);
|
|
177
|
+
const post = createPost(config);
|
|
170
178
|
const base = config.basePath ?? "/phone";
|
|
171
179
|
const org = config.organizationId ? { organization_id: config.organizationId } : {};
|
|
172
180
|
const coventiaContacts = async ({ search, skip, limit }) => {
|
|
@@ -251,12 +259,89 @@ function coventiaSources(config) {
|
|
|
251
259
|
people: (body.people ?? []).map(toAgent)
|
|
252
260
|
};
|
|
253
261
|
};
|
|
262
|
+
const coventiaTransferOutcomes = async ({ since }) => {
|
|
263
|
+
const body = await get(
|
|
264
|
+
`${base}/transfers/outcomes`,
|
|
265
|
+
{ ...org, since }
|
|
266
|
+
);
|
|
267
|
+
return {
|
|
268
|
+
cursor: body.cursor ?? "0",
|
|
269
|
+
outcomes: (body.outcomes ?? []).map(toTransferOutcome)
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
const coventiaPresence = (callbacks) => {
|
|
273
|
+
const WS = config.WebSocket ?? globalThis.WebSocket;
|
|
274
|
+
return new Promise((resolve2, reject) => {
|
|
275
|
+
void (async () => {
|
|
276
|
+
let ticket;
|
|
277
|
+
try {
|
|
278
|
+
const body = await post(`${base}/presence/ticket`, org);
|
|
279
|
+
if (!body.ticket) throw new Error("presence ticket mint returned no ticket");
|
|
280
|
+
ticket = body.ticket;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const wsUrl = `${toWebSocketUrl(config.baseUrl)}${base}/presence?ticket=${encodeURIComponent(ticket)}`;
|
|
286
|
+
const ws = new WS(wsUrl);
|
|
287
|
+
let settled = false;
|
|
288
|
+
ws.onopen = () => {
|
|
289
|
+
if (settled) return;
|
|
290
|
+
settled = true;
|
|
291
|
+
resolve2({
|
|
292
|
+
reportCallState: (onCall) => {
|
|
293
|
+
if (ws.readyState !== WS.OPEN) return;
|
|
294
|
+
ws.send(JSON.stringify({ type: "call-state", onCall }));
|
|
295
|
+
},
|
|
296
|
+
close: () => {
|
|
297
|
+
ws.onclose = null;
|
|
298
|
+
ws.onerror = null;
|
|
299
|
+
ws.close();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
ws.onmessage = (event) => {
|
|
304
|
+
let data;
|
|
305
|
+
try {
|
|
306
|
+
data = JSON.parse(String(event.data));
|
|
307
|
+
} catch {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const msg = data;
|
|
311
|
+
if (msg.type === "snapshot") {
|
|
312
|
+
callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));
|
|
313
|
+
} else if (msg.type === "delta" && msg.agent) {
|
|
314
|
+
callbacks.onDelta(toPresenceEntry(msg.agent));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
ws.onclose = () => {
|
|
318
|
+
if (!settled) {
|
|
319
|
+
settled = true;
|
|
320
|
+
reject(new Error("presence socket closed before opening"));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
callbacks.onClose();
|
|
324
|
+
};
|
|
325
|
+
ws.onerror = () => {
|
|
326
|
+
};
|
|
327
|
+
})();
|
|
328
|
+
});
|
|
329
|
+
};
|
|
254
330
|
const contacts = config.contacts ?? coventiaContacts;
|
|
255
331
|
const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
|
|
256
332
|
const history = withNameResolution(rawHistory, resolve);
|
|
257
|
-
|
|
333
|
+
const transferOutcomes = config.transferOutcomes ?? coventiaTransferOutcomes;
|
|
334
|
+
const presence = config.presence ?? coventiaPresence;
|
|
335
|
+
return {
|
|
336
|
+
contacts,
|
|
337
|
+
history,
|
|
338
|
+
resolveContact: resolve,
|
|
339
|
+
listAgents: coventiaListAgents,
|
|
340
|
+
transferOutcomes,
|
|
341
|
+
presence
|
|
342
|
+
};
|
|
258
343
|
}
|
|
259
|
-
var toContact, toDirection, otherParty, isMissed, toOutcome, toDate;
|
|
344
|
+
var toContact, toDirection, otherParty, isMissed, toOutcome, toDate, toTransferOutcome, toPresenceState, toPresenceEntry, toWebSocketUrl;
|
|
260
345
|
var init_sources2 = __esm({
|
|
261
346
|
"src/coventia/sources.ts"() {
|
|
262
347
|
init_sources();
|
|
@@ -291,6 +376,20 @@ var init_sources2 = __esm({
|
|
|
291
376
|
const d = new Date(raw);
|
|
292
377
|
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
|
|
293
378
|
};
|
|
379
|
+
toTransferOutcome = (row) => ({
|
|
380
|
+
id: row.id ?? "",
|
|
381
|
+
target: row.target ?? "",
|
|
382
|
+
status: row.status === "connected" ? "connected" : "not-connected",
|
|
383
|
+
at: toDate(row.at)
|
|
384
|
+
});
|
|
385
|
+
toPresenceState = (raw) => raw === "disponible" || raw === "en_llamada" ? raw : "desconectado";
|
|
386
|
+
toPresenceEntry = (row) => ({
|
|
387
|
+
userId: row.userId ?? "",
|
|
388
|
+
extension: row.extension ?? "",
|
|
389
|
+
name: row.name ?? null,
|
|
390
|
+
state: toPresenceState(row.state)
|
|
391
|
+
});
|
|
392
|
+
toWebSocketUrl = (httpUrl) => httpUrl.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
294
393
|
}
|
|
295
394
|
});
|
|
296
395
|
|
|
@@ -746,6 +845,14 @@ var init_ua = __esm({
|
|
|
746
845
|
#uaInFlight = null;
|
|
747
846
|
#domain = "";
|
|
748
847
|
#activeCall = null;
|
|
848
|
+
/**
|
|
849
|
+
* The consult leg from `startConsult` (SPEC §5.6), if one is live on this UA right now.
|
|
850
|
+
*
|
|
851
|
+
* Tracked separately from `#activeCall` so `#maybeTeardownUA` can tell "both legs are actually gone"
|
|
852
|
+
* apart from "the main leg ended, but a consult is still up" — see that method's own comment for the
|
|
853
|
+
* bug this exists to prevent.
|
|
854
|
+
*/
|
|
855
|
+
#consultCall = null;
|
|
749
856
|
/** The chosen microphone, or `null` for the browser default. Read fresh by every new leg's
|
|
750
857
|
* `mediaConstraints`, and pushed live to `#activeCall` by `setInputDevice`. */
|
|
751
858
|
#inputDeviceId = null;
|
|
@@ -864,9 +971,7 @@ var init_ua = __esm({
|
|
|
864
971
|
const ms = this.provider.quirks?.noInviteTeardownMs;
|
|
865
972
|
if (!ms) return;
|
|
866
973
|
this.#clearNoInviteTimer();
|
|
867
|
-
this.#noInviteTimer = setTimeout(() =>
|
|
868
|
-
if (!this.#activeCall) this.#teardownUA();
|
|
869
|
-
}, ms);
|
|
974
|
+
this.#noInviteTimer = setTimeout(() => this.#maybeTeardownUA(), ms);
|
|
870
975
|
}
|
|
871
976
|
/**
|
|
872
977
|
* Rebuild the incoming-signal channel with fresh credentials, after a registration failure.
|
|
@@ -988,6 +1093,7 @@ var init_ua = __esm({
|
|
|
988
1093
|
}
|
|
989
1094
|
this.#ua = null;
|
|
990
1095
|
this.#activeCall = null;
|
|
1096
|
+
this.#consultCall = null;
|
|
991
1097
|
}
|
|
992
1098
|
/** Place a call once the UA is registered (its 200 OK for REGISTER has arrived). */
|
|
993
1099
|
/**
|
|
@@ -1030,7 +1136,7 @@ var init_ua = __esm({
|
|
|
1030
1136
|
const code = missed ? "call/missed" : CALL_END_CODES[event.reason];
|
|
1031
1137
|
if (code) this.#reportError(code, event.cause);
|
|
1032
1138
|
if (this.#activeCall === call) this.#activeCall = null;
|
|
1033
|
-
|
|
1139
|
+
this.#maybeTeardownUA();
|
|
1034
1140
|
}
|
|
1035
1141
|
});
|
|
1036
1142
|
return call;
|
|
@@ -1041,12 +1147,38 @@ var init_ua = __esm({
|
|
|
1041
1147
|
mediaConstraints: buildAudioConstraints(this.#inputDeviceId),
|
|
1042
1148
|
pcConfig: PC_CONFIG
|
|
1043
1149
|
});
|
|
1044
|
-
|
|
1150
|
+
const consultCall = new SipEngineCall(consultSession, {
|
|
1045
1151
|
id: nextCallId(),
|
|
1046
1152
|
buildTarget: (x) => this.#buildTarget(x),
|
|
1047
1153
|
placeConsult: () => Promise.reject(new Error("cannot consult from a consult leg")),
|
|
1048
1154
|
iceGatheringCapMs: this.provider.quirks?.iceGatheringCapMs ?? DEFAULT_ICE_CAP_MS
|
|
1049
1155
|
});
|
|
1156
|
+
this.#consultCall = consultCall;
|
|
1157
|
+
consultCall.onEvent((event) => {
|
|
1158
|
+
if (event.type === "ended") {
|
|
1159
|
+
if (this.#consultCall === consultCall) this.#consultCall = null;
|
|
1160
|
+
this.#maybeTeardownUA();
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
return consultCall;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* On-demand registration (SPEC §3.3): the UA is disposable once NOTHING is left on it — but "the
|
|
1167
|
+
* main leg ended" (or "no INVITE followed this push") is not the same fact as "nothing is left."
|
|
1168
|
+
* `completeTransfer()` (SPEC §5.6) ends the main leg via a REFER…Replaces BYE *while the consult leg
|
|
1169
|
+
* is still what the PBX needs to bridge the caller onto* — tearing the UA down (and with it, EVERY
|
|
1170
|
+
* session on it, via `terminateSessions`) the instant the main leg's `ended` fires killed that
|
|
1171
|
+
* consult leg out from under the transfer before the bridge could use it, and the caller was simply
|
|
1172
|
+
* gone. Two real call sites hit this independently (found live, 2026-08-04): the main leg's own
|
|
1173
|
+
* `ended` handler, and `#armNoInviteTeardown`'s 5s timer — the latter re-arms on EVERY push,
|
|
1174
|
+
* including unrelated "answered" pushes Zadarma sends around other account activity, so it can fire
|
|
1175
|
+
* minutes after a transfer with no `#activeCall` and no idea a consult leg is mid-handoff. Both now
|
|
1176
|
+
* route through here rather than checking `#activeCall` alone.
|
|
1177
|
+
*/
|
|
1178
|
+
#maybeTeardownUA() {
|
|
1179
|
+
if (this.provider.registrationMode !== "on-demand") return;
|
|
1180
|
+
if (this.#activeCall || this.#consultCall) return;
|
|
1181
|
+
this.#teardownUA();
|
|
1050
1182
|
}
|
|
1051
1183
|
#startRejectGuard() {
|
|
1052
1184
|
const ms = this.provider.quirks?.rejectGuardMs;
|
|
@@ -1517,6 +1649,8 @@ var SoftphoneEvent = /* @__PURE__ */ ((SoftphoneEvent2) => {
|
|
|
1517
1649
|
SoftphoneEvent2["DeviceChange"] = "deviceChange";
|
|
1518
1650
|
SoftphoneEvent2["Error"] = "error";
|
|
1519
1651
|
SoftphoneEvent2["LeaderChanged"] = "leaderChanged";
|
|
1652
|
+
SoftphoneEvent2["TransferOutcome"] = "transferOutcome";
|
|
1653
|
+
SoftphoneEvent2["PresenceChanged"] = "presenceChanged";
|
|
1520
1654
|
return SoftphoneEvent2;
|
|
1521
1655
|
})(SoftphoneEvent || {});
|
|
1522
1656
|
var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
|
|
@@ -1558,10 +1692,11 @@ function isTerminal(state) {
|
|
|
1558
1692
|
|
|
1559
1693
|
// src/core/call-impl.ts
|
|
1560
1694
|
var CallImpl = class _CallImpl extends Emitter {
|
|
1561
|
-
constructor(engineCall, onListenerError, onInternalError) {
|
|
1695
|
+
constructor(engineCall, onListenerError, onInternalError, onTransfer) {
|
|
1562
1696
|
super(onListenerError);
|
|
1563
1697
|
this.engineCall = engineCall;
|
|
1564
1698
|
this.onInternalError = onInternalError;
|
|
1699
|
+
this.onTransfer = onTransfer;
|
|
1565
1700
|
this.#onListenerError = onListenerError;
|
|
1566
1701
|
this.#state = INITIAL_STATES[engineCall.direction];
|
|
1567
1702
|
this.#remote = {
|
|
@@ -1573,6 +1708,7 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
1573
1708
|
}
|
|
1574
1709
|
engineCall;
|
|
1575
1710
|
onInternalError;
|
|
1711
|
+
onTransfer;
|
|
1576
1712
|
#state;
|
|
1577
1713
|
#remote;
|
|
1578
1714
|
#answeredAt = null;
|
|
@@ -1658,7 +1794,10 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
1658
1794
|
}
|
|
1659
1795
|
async hangup() {
|
|
1660
1796
|
if (isTerminal(this.#state)) return;
|
|
1661
|
-
|
|
1797
|
+
const consult = this.#consultCall;
|
|
1798
|
+
this.#consultCall = null;
|
|
1799
|
+
this.#consultEngineCall = null;
|
|
1800
|
+
await Promise.all([consult?.hangup() ?? Promise.resolve(), this.engineCall.hangup()]);
|
|
1662
1801
|
}
|
|
1663
1802
|
mute(muted) {
|
|
1664
1803
|
if (this.#muted === muted) return;
|
|
@@ -1680,6 +1819,7 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
1680
1819
|
}
|
|
1681
1820
|
async transfer(target) {
|
|
1682
1821
|
await this.engineCall.transfer(target);
|
|
1822
|
+
this.onTransfer?.(target, this.#remote.number.trim() || null);
|
|
1683
1823
|
}
|
|
1684
1824
|
async startConsult(target) {
|
|
1685
1825
|
await this.hold();
|
|
@@ -1715,6 +1855,12 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
1715
1855
|
}
|
|
1716
1856
|
#end(reason) {
|
|
1717
1857
|
if (isTerminal(this.#state)) return;
|
|
1858
|
+
if (this.#consultCall) {
|
|
1859
|
+
const consult = this.#consultCall;
|
|
1860
|
+
this.#consultCall = null;
|
|
1861
|
+
this.#consultEngineCall = null;
|
|
1862
|
+
void consult.hangup();
|
|
1863
|
+
}
|
|
1718
1864
|
const from = this.#state;
|
|
1719
1865
|
this.#state = "ended";
|
|
1720
1866
|
this.emit("stateChanged" /* StateChanged */, { state: "ended", previous: from });
|
|
@@ -1758,7 +1904,9 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
|
|
|
1758
1904
|
contacts: async (params) => (await load()).contacts(params),
|
|
1759
1905
|
history: async (params) => (await load()).history(params),
|
|
1760
1906
|
resolveContact: async (number) => (await load()).resolveContact(number),
|
|
1761
|
-
listAgents: async () => (await load()).listAgents()
|
|
1907
|
+
listAgents: async () => (await load()).listAgents(),
|
|
1908
|
+
transferOutcomes: async (params) => (await load()).transferOutcomes(params),
|
|
1909
|
+
presence: async (callbacks) => (await load()).presence(callbacks)
|
|
1762
1910
|
};
|
|
1763
1911
|
}
|
|
1764
1912
|
|
|
@@ -1806,6 +1954,166 @@ function webLocksTabLock(lockName = LOCK_NAME) {
|
|
|
1806
1954
|
};
|
|
1807
1955
|
}
|
|
1808
1956
|
|
|
1957
|
+
// src/core/transfer-watch.ts
|
|
1958
|
+
var FAST_INTERVAL_MS = 2e3;
|
|
1959
|
+
var FAST_WINDOW_MS = 1e4;
|
|
1960
|
+
var SLOW_INTERVAL_MS = 5e3;
|
|
1961
|
+
var DEADLINE_MS = 9e4;
|
|
1962
|
+
var EXTENSION_LIKE = /^\d{1,6}$/;
|
|
1963
|
+
function startTransferWatch(params) {
|
|
1964
|
+
const {
|
|
1965
|
+
target,
|
|
1966
|
+
callerNumber,
|
|
1967
|
+
source,
|
|
1968
|
+
getIsLeader,
|
|
1969
|
+
emit,
|
|
1970
|
+
setTimeout: scheduleTimeout = globalThis.setTimeout,
|
|
1971
|
+
clearTimeout: cancelTimeout = globalThis.clearTimeout,
|
|
1972
|
+
now = () => Date.now()
|
|
1973
|
+
} = params;
|
|
1974
|
+
if (!EXTENSION_LIKE.test(target)) return () => {
|
|
1975
|
+
};
|
|
1976
|
+
if (!getIsLeader()) return () => {
|
|
1977
|
+
};
|
|
1978
|
+
let stopped = false;
|
|
1979
|
+
let timer = null;
|
|
1980
|
+
let cursor = null;
|
|
1981
|
+
const startedAt = now();
|
|
1982
|
+
const stop = () => {
|
|
1983
|
+
stopped = true;
|
|
1984
|
+
if (timer !== null) {
|
|
1985
|
+
cancelTimeout(timer);
|
|
1986
|
+
timer = null;
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
const finish = (status) => {
|
|
1990
|
+
stop();
|
|
1991
|
+
emit({ status, target, caller: callerNumber, at: new Date(now()) });
|
|
1992
|
+
};
|
|
1993
|
+
const scheduleNext = () => {
|
|
1994
|
+
if (stopped) return;
|
|
1995
|
+
const elapsed = now() - startedAt;
|
|
1996
|
+
if (elapsed >= DEADLINE_MS) {
|
|
1997
|
+
stop();
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
timer = scheduleTimeout(() => void poll(), elapsed < FAST_WINDOW_MS ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS);
|
|
2001
|
+
};
|
|
2002
|
+
const poll = async () => {
|
|
2003
|
+
if (stopped) return;
|
|
2004
|
+
if (!getIsLeader()) {
|
|
2005
|
+
stop();
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
let page;
|
|
2009
|
+
try {
|
|
2010
|
+
page = await source(cursor === null ? {} : { since: cursor });
|
|
2011
|
+
} catch {
|
|
2012
|
+
if (!stopped) scheduleNext();
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (stopped) return;
|
|
2016
|
+
if (cursor === null) {
|
|
2017
|
+
cursor = page.cursor;
|
|
2018
|
+
} else {
|
|
2019
|
+
cursor = page.cursor;
|
|
2020
|
+
const match = page.outcomes.find((o) => o.target === target);
|
|
2021
|
+
if (match) {
|
|
2022
|
+
finish(match.status);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
scheduleNext();
|
|
2027
|
+
};
|
|
2028
|
+
void poll();
|
|
2029
|
+
return stop;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/core/presence-socket.ts
|
|
2033
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
2034
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
2035
|
+
var NOOP_WATCH = { reportCallState: () => {
|
|
2036
|
+
}, stop: () => {
|
|
2037
|
+
} };
|
|
2038
|
+
function startPresenceSocket(params) {
|
|
2039
|
+
const {
|
|
2040
|
+
source,
|
|
2041
|
+
getIsLeader,
|
|
2042
|
+
onAgentsChanged,
|
|
2043
|
+
setTimeout: scheduleTimeout = globalThis.setTimeout,
|
|
2044
|
+
clearTimeout: cancelTimeout = globalThis.clearTimeout,
|
|
2045
|
+
random = Math.random
|
|
2046
|
+
} = params;
|
|
2047
|
+
if (!getIsLeader()) return NOOP_WATCH;
|
|
2048
|
+
let stopped = false;
|
|
2049
|
+
let attempt = 0;
|
|
2050
|
+
let reconnectTimer = null;
|
|
2051
|
+
let agents = /* @__PURE__ */ new Map();
|
|
2052
|
+
let lastReportedOnCall = null;
|
|
2053
|
+
let connection = null;
|
|
2054
|
+
const stop = () => {
|
|
2055
|
+
if (stopped) return;
|
|
2056
|
+
stopped = true;
|
|
2057
|
+
if (reconnectTimer !== null) {
|
|
2058
|
+
cancelTimeout(reconnectTimer);
|
|
2059
|
+
reconnectTimer = null;
|
|
2060
|
+
}
|
|
2061
|
+
connection?.close();
|
|
2062
|
+
connection = null;
|
|
2063
|
+
};
|
|
2064
|
+
const scheduleReconnect = () => {
|
|
2065
|
+
if (stopped) return;
|
|
2066
|
+
attempt += 1;
|
|
2067
|
+
const capped = Math.min(RECONNECT_BASE_MS * 2 ** (attempt - 1), RECONNECT_MAX_MS);
|
|
2068
|
+
const delay = capped * random();
|
|
2069
|
+
reconnectTimer = scheduleTimeout(() => void connect(), delay);
|
|
2070
|
+
};
|
|
2071
|
+
const connect = async () => {
|
|
2072
|
+
if (stopped) return;
|
|
2073
|
+
if (!getIsLeader()) {
|
|
2074
|
+
stop();
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
let opened;
|
|
2078
|
+
try {
|
|
2079
|
+
opened = await source({
|
|
2080
|
+
onSnapshot: (snapshot) => {
|
|
2081
|
+
agents = new Map(snapshot.map((agent) => [agent.userId, agent]));
|
|
2082
|
+
onAgentsChanged([...agents.values()]);
|
|
2083
|
+
},
|
|
2084
|
+
onDelta: (agent) => {
|
|
2085
|
+
agents.set(agent.userId, agent);
|
|
2086
|
+
onAgentsChanged([...agents.values()]);
|
|
2087
|
+
},
|
|
2088
|
+
onClose: () => {
|
|
2089
|
+
if (connection === null) return;
|
|
2090
|
+
connection = null;
|
|
2091
|
+
scheduleReconnect();
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
} catch {
|
|
2095
|
+
if (!stopped) scheduleReconnect();
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (stopped) {
|
|
2099
|
+
opened.close();
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
connection = opened;
|
|
2103
|
+
attempt = 0;
|
|
2104
|
+
if (lastReportedOnCall !== null) connection.reportCallState(lastReportedOnCall);
|
|
2105
|
+
};
|
|
2106
|
+
void connect();
|
|
2107
|
+
return {
|
|
2108
|
+
reportCallState: (onCall) => {
|
|
2109
|
+
if (onCall === lastReportedOnCall) return;
|
|
2110
|
+
lastReportedOnCall = onCall;
|
|
2111
|
+
connection?.reportCallState(onCall);
|
|
2112
|
+
},
|
|
2113
|
+
stop
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
|
|
1809
2117
|
// src/core/softphone-impl.ts
|
|
1810
2118
|
init_errors();
|
|
1811
2119
|
var liveInstance = false;
|
|
@@ -1869,6 +2177,21 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
1869
2177
|
/** Contacts + history, whenever there is a Coventia backend to reach. The widget reads these. */
|
|
1870
2178
|
sources;
|
|
1871
2179
|
audio;
|
|
2180
|
+
/**
|
|
2181
|
+
* Every in-flight transfer-outcome watch (Phase A), so `destroy()` can stop all of them. Never
|
|
2182
|
+
* pruned as individual watches finish on their own — `stop()` is idempotent and cheap to call again,
|
|
2183
|
+
* and a session accumulating a handful of closures for however many transfers it makes is not worth
|
|
2184
|
+
* the bookkeeping a self-removing set would add.
|
|
2185
|
+
*/
|
|
2186
|
+
#activeWatches = /* @__PURE__ */ new Set();
|
|
2187
|
+
/**
|
|
2188
|
+
* Phase B (presence-gated transfer picker). ONE long-lived watch, not a member of `#activeWatches`
|
|
2189
|
+
* — that set is per-transfer instances a live call spawns and forgets; this is a single background
|
|
2190
|
+
* connection tied to the softphone's own lifecycle, started and stopped by `#syncPresenceWatch`
|
|
2191
|
+
* rather than by any one call.
|
|
2192
|
+
*/
|
|
2193
|
+
#presenceWatch = null;
|
|
2194
|
+
#presenceAgents = [];
|
|
1872
2195
|
/**
|
|
1873
2196
|
* `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
|
|
1874
2197
|
* internal seam, not something an integrator tunes). Tests construct this class directly with a
|
|
@@ -1901,6 +2224,9 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
1901
2224
|
get activeCall() {
|
|
1902
2225
|
return this.#activeCall;
|
|
1903
2226
|
}
|
|
2227
|
+
get presence() {
|
|
2228
|
+
return this.#presenceAgents;
|
|
2229
|
+
}
|
|
1904
2230
|
get canPlayAudio() {
|
|
1905
2231
|
return true;
|
|
1906
2232
|
}
|
|
@@ -1937,6 +2263,10 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
1937
2263
|
async destroy() {
|
|
1938
2264
|
if (this.#destroyed) return;
|
|
1939
2265
|
this.#destroyed = true;
|
|
2266
|
+
for (const stopWatch of this.#activeWatches) stopWatch();
|
|
2267
|
+
this.#activeWatches.clear();
|
|
2268
|
+
this.#presenceWatch?.stop();
|
|
2269
|
+
this.#presenceWatch = null;
|
|
1940
2270
|
await this.stop().catch(() => {
|
|
1941
2271
|
});
|
|
1942
2272
|
this.#setState("destroyed");
|
|
@@ -1967,7 +2297,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
1967
2297
|
const call = new CallImpl(
|
|
1968
2298
|
engineCall,
|
|
1969
2299
|
(event, error) => this.#reportListenerError("call", event, error),
|
|
1970
|
-
(message) => this.#reportInternalError(message)
|
|
2300
|
+
(message) => this.#reportInternalError(message),
|
|
2301
|
+
(target, callerNumber) => this.#onTransfer(target, callerNumber)
|
|
1971
2302
|
);
|
|
1972
2303
|
this.#activeCall = call;
|
|
1973
2304
|
const off = call.on("ended" /* Ended */, () => {
|
|
@@ -1992,11 +2323,62 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
1992
2323
|
this.#state = state;
|
|
1993
2324
|
if (state === "online") this.#lastError = null;
|
|
1994
2325
|
this.emit("stateChanged" /* StateChanged */, { state, previous });
|
|
2326
|
+
this.#syncPresenceWatch();
|
|
1995
2327
|
}
|
|
1996
2328
|
#setLeader(isLeader) {
|
|
1997
2329
|
if (isLeader === this.#isLeader) return;
|
|
1998
2330
|
this.#isLeader = isLeader;
|
|
1999
2331
|
this.emit("leaderChanged" /* LeaderChanged */, { isLeader });
|
|
2332
|
+
this.#syncPresenceWatch();
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Phase B (presence-gated transfer picker, `docs/specs/transfer-presence-picker.md` plan T6/T7).
|
|
2336
|
+
* The ONE place that decides whether the presence connection should be running right now — called
|
|
2337
|
+
* after every `#setState`/`#setLeader` transition rather than left to either alone, since either
|
|
2338
|
+
* one flipping can change the answer. No-ops entirely when no Coventia backend is configured
|
|
2339
|
+
* (`sources` is null) — same guard `#onTransfer` uses, and for the same reason: nothing to open.
|
|
2340
|
+
*
|
|
2341
|
+
* `presence-socket.ts`'s own `getIsLeader` is checked at start and before each reconnect attempt,
|
|
2342
|
+
* not on a poll tick the way `startTransferWatch`'s is — see that module's own doc comment for why.
|
|
2343
|
+
* This still passes `getIsLeader: () => this.#isLeader` for that narrower, defensive recheck; the
|
|
2344
|
+
* BROAD online/leader gate — whether to be connected AT ALL — is owned entirely here, the same
|
|
2345
|
+
* split `#onTransfer` already has with `startTransferWatch`.
|
|
2346
|
+
*/
|
|
2347
|
+
#syncPresenceWatch() {
|
|
2348
|
+
const shouldRun = Boolean(this.sources) && this.#state === "online" && this.#isLeader;
|
|
2349
|
+
if (shouldRun === (this.#presenceWatch !== null)) return;
|
|
2350
|
+
if (shouldRun) {
|
|
2351
|
+
this.#presenceWatch = startPresenceSocket({
|
|
2352
|
+
source: this.sources.presence,
|
|
2353
|
+
getIsLeader: () => this.#isLeader,
|
|
2354
|
+
onAgentsChanged: (agents) => {
|
|
2355
|
+
this.#presenceAgents = agents;
|
|
2356
|
+
this.emit("presenceChanged" /* PresenceChanged */, { agents });
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
} else {
|
|
2360
|
+
this.#presenceWatch?.stop();
|
|
2361
|
+
this.#presenceWatch = null;
|
|
2362
|
+
this.#presenceAgents = [];
|
|
2363
|
+
this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Phase A (transfer outcome relay, `docs/specs/transfer-outcome-relay.md`) — `CallImpl.transfer()`'s
|
|
2368
|
+
* internal seam calls this. No-ops entirely when no Coventia backend is configured (`sources` is
|
|
2369
|
+
* null): there is then nothing to poll, and behaviour is exactly what it was before this feature
|
|
2370
|
+
* existed (spec §5.4).
|
|
2371
|
+
*/
|
|
2372
|
+
#onTransfer(target, callerNumber) {
|
|
2373
|
+
if (!this.sources) return;
|
|
2374
|
+
const stopWatch = startTransferWatch({
|
|
2375
|
+
target,
|
|
2376
|
+
callerNumber,
|
|
2377
|
+
source: this.sources.transferOutcomes,
|
|
2378
|
+
getIsLeader: () => this.#isLeader,
|
|
2379
|
+
emit: (payload) => this.emit("transferOutcome" /* TransferOutcome */, payload)
|
|
2380
|
+
});
|
|
2381
|
+
this.#activeWatches.add(stopWatch);
|
|
2000
2382
|
}
|
|
2001
2383
|
/**
|
|
2002
2384
|
* Re-emit an engine failure to the host, and remember it if it is fatal.
|
|
@@ -2116,7 +2498,7 @@ function isCallEngine(value) {
|
|
|
2116
2498
|
|
|
2117
2499
|
// src/index.ts
|
|
2118
2500
|
init_errors();
|
|
2119
|
-
var VERSION = "0.
|
|
2501
|
+
var VERSION = "0.2.0";
|
|
2120
2502
|
|
|
2121
2503
|
exports.CALL_TRANSITIONS = CALL_TRANSITIONS;
|
|
2122
2504
|
exports.CallEvent = CallEvent;
|