@coventia/softphone 0.4.0 → 0.5.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-C6YpT6uR.d.cts → call-FqcvDg1t.d.cts} +105 -12
- package/dist/{call-C6YpT6uR.d.ts → call-FqcvDg1t.d.ts} +105 -12
- package/dist/{chunk-EYEDPD7Y.js → chunk-6RR6G35E.js} +92 -17
- package/dist/chunk-6RR6G35E.js.map +1 -0
- package/dist/{chunk-2ZSOGGPO.js → chunk-FRXHEHQA.js} +15 -2
- package/dist/chunk-FRXHEHQA.js.map +1 -0
- package/dist/{chunk-UHNLEBBO.js → chunk-I3RSC52R.js} +28 -3
- package/dist/chunk-I3RSC52R.js.map +1 -0
- package/dist/chunk-M76SAXXM.js +13 -0
- package/dist/chunk-M76SAXXM.js.map +1 -0
- package/dist/{chunk-7R6LOOAI.js → chunk-N4PLEYFJ.js} +4 -2
- package/dist/chunk-N4PLEYFJ.js.map +1 -0
- package/dist/element/index.d.ts +142 -6
- package/dist/element/index.js +138 -43
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-DPft99Wj.d.ts → engine-CsNRD7B7.d.ts} +1 -1
- package/dist/{engine-CDdaSojx.d.cts → engine-CzOCrfY9.d.cts} +1 -1
- package/dist/index.cjs +579 -47
- 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 +447 -37
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +26 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +24 -5
- package/dist/react/index.d.ts +24 -5
- package/dist/react/index.js +2 -2
- package/dist/{softphone-CY5d2WWY.d.cts → softphone-By9sizlV.d.ts} +42 -3
- package/dist/{softphone-DXgmTyrX.d.ts → softphone-JZvJdzU3.d.cts} +42 -3
- package/dist/{sources-ONYQW733.js → sources-QL2LRZVH.js} +19 -2
- package/dist/sources-QL2LRZVH.js.map +1 -0
- package/dist/testing/index.cjs +13 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +1 -1
- package/dist/ui/index.cjs +92 -2
- 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 +69 -4
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +111 -14
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +5 -6
- package/dist/zadarma/index.d.ts +5 -6
- package/dist/zadarma/index.js +3 -2
- package/dist/zadarma-engine-TIUE7PZ7.js +6 -0
- package/dist/{zadarma-engine-L5DEFXUS.js.map → zadarma-engine-TIUE7PZ7.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-2ZSOGGPO.js.map +0 -1
- package/dist/chunk-7R6LOOAI.js.map +0 -1
- package/dist/chunk-EYEDPD7Y.js.map +0 -1
- package/dist/chunk-UHNLEBBO.js.map +0 -1
- package/dist/sources-ONYQW733.js.map +0 -1
- package/dist/zadarma-engine-L5DEFXUS.js +0 -5
package/dist/index.cjs
CHANGED
|
@@ -316,6 +316,16 @@ function coventiaSources(config) {
|
|
|
316
316
|
if (ws.readyState !== WS.OPEN) return;
|
|
317
317
|
ws.send(JSON.stringify({ type: "bye" }));
|
|
318
318
|
},
|
|
319
|
+
// `line: true` is what makes the server treat this socket as taking part in the line owner;
|
|
320
|
+
// without it (or with an id it rejects) the `hello` is ignored, as an older SDK's would be.
|
|
321
|
+
hello: (clientId) => {
|
|
322
|
+
if (ws.readyState !== WS.OPEN) return;
|
|
323
|
+
ws.send(JSON.stringify({ type: "hello", clientId, line: true }));
|
|
324
|
+
},
|
|
325
|
+
claimLine: () => {
|
|
326
|
+
if (ws.readyState !== WS.OPEN) return;
|
|
327
|
+
ws.send(JSON.stringify({ type: "line-claim" }));
|
|
328
|
+
},
|
|
319
329
|
close: () => {
|
|
320
330
|
ws.onclose = null;
|
|
321
331
|
ws.onerror = null;
|
|
@@ -335,6 +345,12 @@ function coventiaSources(config) {
|
|
|
335
345
|
callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));
|
|
336
346
|
} else if (msg.type === "delta" && msg.agent) {
|
|
337
347
|
callbacks.onDelta(toPresenceEntry(msg.agent));
|
|
348
|
+
} else if (msg.type === "line" && (msg.state === "here" || msg.state === "elsewhere")) {
|
|
349
|
+
callbacks.onLine?.({ state: msg.state, where: msg.state === "here" ? null : toLineLocation(msg.where) });
|
|
350
|
+
} else if (msg.type === "line-claim-result") {
|
|
351
|
+
callbacks.onClaimResult?.(
|
|
352
|
+
msg.ok === true ? { ok: true } : { ok: false, reason: typeof msg.reason === "string" ? msg.reason : "unavailable" }
|
|
353
|
+
);
|
|
338
354
|
}
|
|
339
355
|
};
|
|
340
356
|
ws.onclose = () => {
|
|
@@ -478,7 +494,7 @@ function coventiaSources(config) {
|
|
|
478
494
|
outboundNumbers
|
|
479
495
|
};
|
|
480
496
|
}
|
|
481
|
-
var toContact, toDirection, otherParty, isMissed, toOutcome, toDate, toTransferOutcome, toPresenceState, toPresenceEntry, toWebSocketUrl;
|
|
497
|
+
var toContact, toDirection, otherParty, isMissed, toOutcome, toDate, toTransferOutcome, toPresenceState, toPresenceEntry, toLineLocation, toWebSocketUrl;
|
|
482
498
|
var init_sources2 = __esm({
|
|
483
499
|
"src/coventia/sources.ts"() {
|
|
484
500
|
init_sources();
|
|
@@ -527,6 +543,7 @@ var init_sources2 = __esm({
|
|
|
527
543
|
name: row.name ?? null,
|
|
528
544
|
state: toPresenceState(row.state)
|
|
529
545
|
});
|
|
546
|
+
toLineLocation = (raw) => raw === "desktop" || raw === "browser" ? raw : null;
|
|
530
547
|
toWebSocketUrl = (httpUrl) => httpUrl.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
531
548
|
}
|
|
532
549
|
});
|
|
@@ -679,6 +696,19 @@ var init_errors = __esm({
|
|
|
679
696
|
message: "The provider does not support this operation.",
|
|
680
697
|
solution: "Guard the UI on the operation before offering it."
|
|
681
698
|
},
|
|
699
|
+
// ── Line owner (docs/specs/line-owner.md §5) ────────────────────────────────
|
|
700
|
+
// What `takeLine()` rejects with — and `call()` from `standby`, which takes the line first. Never
|
|
701
|
+
// emitted on `SoftphoneEvent.Error`: the promise IS the answer, and nothing about the phone is broken.
|
|
702
|
+
"line/claim-refused": {
|
|
703
|
+
fatal: false,
|
|
704
|
+
message: "The line was not moved here: the user is on a call on the softphone that has it.",
|
|
705
|
+
solution: "Nothing to fix. The line never moves during a call; take it again once that call has ended. Incoming calls keep ringing where they ring now."
|
|
706
|
+
},
|
|
707
|
+
"line/unavailable": {
|
|
708
|
+
fatal: false,
|
|
709
|
+
message: "The line could not be taken: nothing decided where it is.",
|
|
710
|
+
solution: "The presence connection is down, the backend predates the line owner, or it did not answer in time. Retry in a moment; incoming calls keep ringing where they ring now."
|
|
711
|
+
},
|
|
682
712
|
// ── Provider ────────────────────────────────────────────────────────────────
|
|
683
713
|
"provider/provision-failed": {
|
|
684
714
|
fatal: true,
|
|
@@ -978,9 +1008,22 @@ var init_ua = __esm({
|
|
|
978
1008
|
provider;
|
|
979
1009
|
factory;
|
|
980
1010
|
#state = "stopped";
|
|
1011
|
+
/**
|
|
1012
|
+
* Which `start()`…`stop()` cycle this is — bumped by `stop()`, which ends one. Everything that awaits
|
|
1013
|
+
* and then acts (builds a UA, subscribes to the signal, sets the state) captures it first and gives up
|
|
1014
|
+
* if a stop has happened since. Not bumped by `start()`: that would cancel work begun just before it,
|
|
1015
|
+
* such as a call placed while the softphone is still starting.
|
|
1016
|
+
*
|
|
1017
|
+
* Without it a stopped phone came back to life: a provisioning or a signal connect still in flight
|
|
1018
|
+
* when `stop()` ran finished afterwards and REGISTERed — and since the PBX routes a call to whichever
|
|
1019
|
+
* client registered last, it quietly took calls from the user's other device while its UI said off.
|
|
1020
|
+
*/
|
|
1021
|
+
#run = 0;
|
|
981
1022
|
#ua = null;
|
|
982
1023
|
/** In-flight `#createUA()`. THE concurrency guard — see `#ensureUA`. */
|
|
983
1024
|
#uaInFlight = null;
|
|
1025
|
+
/** Calls waiting for the current UA to register (`#callWhenRegistered`), failed by `#teardownUA`. */
|
|
1026
|
+
#awaitingRegistration = /* @__PURE__ */ new Set();
|
|
984
1027
|
#domain = "";
|
|
985
1028
|
#activeCall = null;
|
|
986
1029
|
/**
|
|
@@ -1063,14 +1106,25 @@ var init_ua = __esm({
|
|
|
1063
1106
|
for (const listener of [...this.#errorListeners]) listener(error);
|
|
1064
1107
|
}
|
|
1065
1108
|
async start() {
|
|
1109
|
+
const run = this.#run;
|
|
1066
1110
|
this.#setState("starting");
|
|
1067
1111
|
if (this.provider.registrationMode === "persistent") {
|
|
1068
|
-
|
|
1112
|
+
try {
|
|
1113
|
+
await this.#ensureUA();
|
|
1114
|
+
} catch (cause) {
|
|
1115
|
+
if (run === this.#run) throw cause;
|
|
1116
|
+
}
|
|
1117
|
+
if (run !== this.#run) return;
|
|
1069
1118
|
this.#setState("online");
|
|
1070
1119
|
} else {
|
|
1071
1120
|
const signal = this.provider.incomingSignal;
|
|
1072
1121
|
if (signal) {
|
|
1073
|
-
|
|
1122
|
+
try {
|
|
1123
|
+
await signal.connect();
|
|
1124
|
+
} catch (cause) {
|
|
1125
|
+
if (run === this.#run) throw cause;
|
|
1126
|
+
}
|
|
1127
|
+
if (run !== this.#run) return;
|
|
1074
1128
|
this.#signalUnsub = signal.onSignal(() => void this.#onIncomingSignal());
|
|
1075
1129
|
this.#signalConnectionUnsub = signal.onConnectionChange?.(
|
|
1076
1130
|
(connected) => this.#setState(connected ? "online" : "offline")
|
|
@@ -1080,6 +1134,7 @@ var init_ua = __esm({
|
|
|
1080
1134
|
}
|
|
1081
1135
|
}
|
|
1082
1136
|
async stop() {
|
|
1137
|
+
this.#run += 1;
|
|
1083
1138
|
this.#clearNoInviteTimer();
|
|
1084
1139
|
this.#signalUnsub?.();
|
|
1085
1140
|
this.#signalUnsub = null;
|
|
@@ -1098,11 +1153,12 @@ var init_ua = __esm({
|
|
|
1098
1153
|
async #onIncomingSignal() {
|
|
1099
1154
|
if (this.now() < this.#rejectGuardUntil) return;
|
|
1100
1155
|
if (this.#activeCall) return;
|
|
1156
|
+
const run = this.#run;
|
|
1101
1157
|
try {
|
|
1102
1158
|
await this.#ensureUA();
|
|
1103
1159
|
this.#armNoInviteTeardown();
|
|
1104
1160
|
} catch {
|
|
1105
|
-
this.#teardownUA();
|
|
1161
|
+
if (run === this.#run) this.#teardownUA();
|
|
1106
1162
|
}
|
|
1107
1163
|
}
|
|
1108
1164
|
#armNoInviteTeardown() {
|
|
@@ -1164,12 +1220,16 @@ var init_ua = __esm({
|
|
|
1164
1220
|
*/
|
|
1165
1221
|
async #ensureUA() {
|
|
1166
1222
|
if (this.#ua) return this.#ua;
|
|
1167
|
-
this.#uaInFlight
|
|
1168
|
-
this.#
|
|
1169
|
-
|
|
1223
|
+
if (!this.#uaInFlight) {
|
|
1224
|
+
const inFlight = this.#createUA().finally(() => {
|
|
1225
|
+
if (this.#uaInFlight === inFlight) this.#uaInFlight = null;
|
|
1226
|
+
});
|
|
1227
|
+
this.#uaInFlight = inFlight;
|
|
1228
|
+
}
|
|
1170
1229
|
return this.#uaInFlight;
|
|
1171
1230
|
}
|
|
1172
1231
|
async #createUA() {
|
|
1232
|
+
const run = this.#run;
|
|
1173
1233
|
const factory = await this.#resolveFactory();
|
|
1174
1234
|
let credentials;
|
|
1175
1235
|
try {
|
|
@@ -1179,17 +1239,28 @@ var init_ua = __esm({
|
|
|
1179
1239
|
else this.#reportError("provider/provision-failed", cause);
|
|
1180
1240
|
throw cause;
|
|
1181
1241
|
}
|
|
1242
|
+
if (run !== this.#run) throw createError("engine/not-started");
|
|
1182
1243
|
this.#domain = domainOf(credentials);
|
|
1183
1244
|
const config = this.#uaConfig(credentials, factory);
|
|
1184
1245
|
const ua = factory.createUA(config);
|
|
1185
|
-
ua.on("newRTCSession", ({ session, originator }) =>
|
|
1246
|
+
ua.on("newRTCSession", ({ session, originator }) => {
|
|
1247
|
+
if (ua !== this.#ua) {
|
|
1248
|
+
if (originator === "remote") session.terminate({ status_code: 486, reason_phrase: "Busy Here" });
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
this.#onNewSession(session, originator);
|
|
1252
|
+
});
|
|
1186
1253
|
ua.on("disconnected", ({ error }) => {
|
|
1254
|
+
if (ua !== this.#ua) return;
|
|
1187
1255
|
if (!error) return;
|
|
1188
1256
|
this.#setState("reconnecting");
|
|
1189
1257
|
this.#reportError("net/transport-failed", error);
|
|
1190
1258
|
});
|
|
1191
|
-
ua.on("connected", () =>
|
|
1259
|
+
ua.on("connected", () => {
|
|
1260
|
+
if (ua === this.#ua) this.#setState("online");
|
|
1261
|
+
});
|
|
1192
1262
|
ua.on("registrationFailed", (e) => {
|
|
1263
|
+
if (run !== this.#run) return;
|
|
1193
1264
|
const detail = e;
|
|
1194
1265
|
this.#reportError("net/registration-failed", {
|
|
1195
1266
|
cause: detail?.cause,
|
|
@@ -1197,7 +1268,7 @@ var init_ua = __esm({
|
|
|
1197
1268
|
domain: this.#domain,
|
|
1198
1269
|
hint: "A bare 401 with no auth challenge usually means the registrar is in the wrong datacenter (SPEC \xA73.1)."
|
|
1199
1270
|
});
|
|
1200
|
-
this.#teardownUA();
|
|
1271
|
+
if (ua === this.#ua) this.#teardownUA();
|
|
1201
1272
|
void this.#refreshSignal();
|
|
1202
1273
|
});
|
|
1203
1274
|
this.#ua = ua;
|
|
@@ -1224,6 +1295,7 @@ var init_ua = __esm({
|
|
|
1224
1295
|
#teardownUA() {
|
|
1225
1296
|
this.#uaInFlight = null;
|
|
1226
1297
|
if (!this.#ua) return;
|
|
1298
|
+
for (const fail of [...this.#awaitingRegistration]) fail();
|
|
1227
1299
|
try {
|
|
1228
1300
|
this.#ua.terminateSessions({ status_code: 487 });
|
|
1229
1301
|
this.#ua.stop();
|
|
@@ -1247,8 +1319,27 @@ var init_ua = __esm({
|
|
|
1247
1319
|
#callWhenRegistered(ua, target) {
|
|
1248
1320
|
const options = { mediaConstraints: buildAudioConstraints(this.#inputDeviceId), pcConfig: PC_CONFIG };
|
|
1249
1321
|
if (ua.isRegistered()) return Promise.resolve(ua.call(target, options));
|
|
1250
|
-
|
|
1251
|
-
|
|
1322
|
+
const run = this.#run;
|
|
1323
|
+
return new Promise((resolve, reject) => {
|
|
1324
|
+
let settled = false;
|
|
1325
|
+
const fail = () => {
|
|
1326
|
+
if (settled) return;
|
|
1327
|
+
settled = true;
|
|
1328
|
+
this.#awaitingRegistration.delete(fail);
|
|
1329
|
+
reject(createError(run === this.#run ? "call/failed" : "engine/not-started"));
|
|
1330
|
+
};
|
|
1331
|
+
this.#awaitingRegistration.add(fail);
|
|
1332
|
+
ua.on("registered", () => {
|
|
1333
|
+
if (settled) return;
|
|
1334
|
+
if (ua !== this.#ua) return fail();
|
|
1335
|
+
settled = true;
|
|
1336
|
+
this.#awaitingRegistration.delete(fail);
|
|
1337
|
+
try {
|
|
1338
|
+
resolve(ua.call(target, options));
|
|
1339
|
+
} catch (cause) {
|
|
1340
|
+
reject(cause);
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1252
1343
|
});
|
|
1253
1344
|
}
|
|
1254
1345
|
#wrapCall(session) {
|
|
@@ -1398,6 +1489,12 @@ var init_zadarma = __esm({
|
|
|
1398
1489
|
#retryTimer;
|
|
1399
1490
|
#unwatchBackground;
|
|
1400
1491
|
#closed = false;
|
|
1492
|
+
/**
|
|
1493
|
+
* Bumped by `disconnect()`. An open or a rebuild still awaiting its key when the signal is disconnected
|
|
1494
|
+
* must build nothing afterwards — and `#closed` alone cannot say so once a later `connect()` (stop, then
|
|
1495
|
+
* start again) has reset it: the late socket was built beside the new one, and nothing ever closed it.
|
|
1496
|
+
*/
|
|
1497
|
+
#run = 0;
|
|
1401
1498
|
#listeners = /* @__PURE__ */ new Set();
|
|
1402
1499
|
#connectionListeners = /* @__PURE__ */ new Set();
|
|
1403
1500
|
async connect() {
|
|
@@ -1407,8 +1504,9 @@ var init_zadarma = __esm({
|
|
|
1407
1504
|
await this.#open();
|
|
1408
1505
|
}
|
|
1409
1506
|
async #open() {
|
|
1507
|
+
const run = this.#run;
|
|
1410
1508
|
const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
|
|
1411
|
-
if (this.#
|
|
1509
|
+
if (run !== this.#run) return;
|
|
1412
1510
|
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
1413
1511
|
this.#socket = socket;
|
|
1414
1512
|
this.#attach(socket);
|
|
@@ -1503,9 +1601,10 @@ var init_zadarma = __esm({
|
|
|
1503
1601
|
return;
|
|
1504
1602
|
}
|
|
1505
1603
|
this.#rebuilding = true;
|
|
1604
|
+
const run = this.#run;
|
|
1506
1605
|
try {
|
|
1507
1606
|
const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
|
|
1508
|
-
if (this.#
|
|
1607
|
+
if (run !== this.#run) return;
|
|
1509
1608
|
const swapped = await new Promise((resolve) => {
|
|
1510
1609
|
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
1511
1610
|
this.#candidate = socket;
|
|
@@ -1558,6 +1657,7 @@ var init_zadarma = __esm({
|
|
|
1558
1657
|
}
|
|
1559
1658
|
disconnect() {
|
|
1560
1659
|
this.#closed = true;
|
|
1660
|
+
this.#run += 1;
|
|
1561
1661
|
this.#stopPings();
|
|
1562
1662
|
clearTimeout(this.#refreshTimer);
|
|
1563
1663
|
this.#refreshTimer = void 0;
|
|
@@ -1801,6 +1901,20 @@ var init_zadarma_browser = __esm({
|
|
|
1801
1901
|
}
|
|
1802
1902
|
});
|
|
1803
1903
|
|
|
1904
|
+
// src/core/api-base.ts
|
|
1905
|
+
function normalizeApiBase(url) {
|
|
1906
|
+
const trimmed = url.replace(/\/+$/, "");
|
|
1907
|
+
if (!trimmed.endsWith("/api/v1")) return trimmed;
|
|
1908
|
+
console.warn(
|
|
1909
|
+
"[softphone] apiBase should be Coventia's domain only, without /api/v1. Ignoring the /api/v1 suffix."
|
|
1910
|
+
);
|
|
1911
|
+
return trimmed.slice(0, -"/api/v1".length).replace(/\/+$/, "");
|
|
1912
|
+
}
|
|
1913
|
+
var init_api_base = __esm({
|
|
1914
|
+
"src/core/api-base.ts"() {
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1804
1918
|
// src/transports/zadarma-engine.ts
|
|
1805
1919
|
var zadarma_engine_exports = {};
|
|
1806
1920
|
__export(zadarma_engine_exports, {
|
|
@@ -1808,7 +1922,11 @@ __export(zadarma_engine_exports, {
|
|
|
1808
1922
|
});
|
|
1809
1923
|
function createZadarmaEngine(config) {
|
|
1810
1924
|
const provider = new ZadarmaProvider(
|
|
1811
|
-
makeZadarmaBrowserDeps({
|
|
1925
|
+
makeZadarmaBrowserDeps({
|
|
1926
|
+
...config,
|
|
1927
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
1928
|
+
tokenSource: cachedTokenSource(config.tokenSource)
|
|
1929
|
+
})
|
|
1812
1930
|
);
|
|
1813
1931
|
return new SipCallEngine(provider, loadJsSipFactory);
|
|
1814
1932
|
}
|
|
@@ -1819,6 +1937,7 @@ var init_zadarma_engine = __esm({
|
|
|
1819
1937
|
init_zadarma();
|
|
1820
1938
|
init_zadarma_browser();
|
|
1821
1939
|
init_token_cache();
|
|
1940
|
+
init_api_base();
|
|
1822
1941
|
}
|
|
1823
1942
|
});
|
|
1824
1943
|
|
|
@@ -1882,6 +2001,8 @@ var SoftphoneEvent = /* @__PURE__ */ ((SoftphoneEvent2) => {
|
|
|
1882
2001
|
SoftphoneEvent2["LeaderChanged"] = "leaderChanged";
|
|
1883
2002
|
SoftphoneEvent2["TransferOutcome"] = "transferOutcome";
|
|
1884
2003
|
SoftphoneEvent2["PresenceChanged"] = "presenceChanged";
|
|
2004
|
+
SoftphoneEvent2["LineChanged"] = "lineChanged";
|
|
2005
|
+
SoftphoneEvent2["LineClaimChanged"] = "lineClaimChanged";
|
|
1885
2006
|
return SoftphoneEvent2;
|
|
1886
2007
|
})(SoftphoneEvent || {});
|
|
1887
2008
|
var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
|
|
@@ -2171,13 +2292,15 @@ function isWebLocksSupported() {
|
|
|
2171
2292
|
return isBrowser() && typeof navigator.locks?.request === "function";
|
|
2172
2293
|
}
|
|
2173
2294
|
function webLocksTabLock(lockName = LOCK_NAME) {
|
|
2174
|
-
|
|
2295
|
+
let controller;
|
|
2175
2296
|
let releaseHold;
|
|
2176
2297
|
return {
|
|
2177
2298
|
acquireLeadership() {
|
|
2178
2299
|
if (!isWebLocksSupported()) return Promise.resolve();
|
|
2300
|
+
const { signal } = controller = new AbortController();
|
|
2179
2301
|
return new Promise((resolveAcquired) => {
|
|
2180
|
-
navigator.locks.request(lockName, { signal
|
|
2302
|
+
navigator.locks.request(lockName, { signal }, () => {
|
|
2303
|
+
if (signal.aborted) return Promise.resolve();
|
|
2181
2304
|
resolveAcquired();
|
|
2182
2305
|
return new Promise((resolveHold) => {
|
|
2183
2306
|
releaseHold = resolveHold;
|
|
@@ -2192,7 +2315,7 @@ function webLocksTabLock(lockName = LOCK_NAME) {
|
|
|
2192
2315
|
releaseHold = void 0;
|
|
2193
2316
|
resolve();
|
|
2194
2317
|
} else {
|
|
2195
|
-
controller
|
|
2318
|
+
controller?.abort();
|
|
2196
2319
|
}
|
|
2197
2320
|
}
|
|
2198
2321
|
};
|
|
@@ -2277,13 +2400,19 @@ function startTransferWatch(params) {
|
|
|
2277
2400
|
var RECONNECT_BASE_MS = 1e3;
|
|
2278
2401
|
var RECONNECT_MAX_MS = 3e4;
|
|
2279
2402
|
var NOOP_WATCH = { reportCallState: () => {
|
|
2280
|
-
}, stop: () => {
|
|
2403
|
+
}, claimLine: () => false, stop: () => {
|
|
2281
2404
|
} };
|
|
2282
2405
|
function startPresenceSocket(params) {
|
|
2283
2406
|
const {
|
|
2284
2407
|
source,
|
|
2285
2408
|
getIsLeader,
|
|
2286
2409
|
onAgentsChanged,
|
|
2410
|
+
clientId,
|
|
2411
|
+
onLine,
|
|
2412
|
+
onClaimResult,
|
|
2413
|
+
onLineUnsupported,
|
|
2414
|
+
onConnectFailed,
|
|
2415
|
+
onDisconnect,
|
|
2287
2416
|
setTimeout: scheduleTimeout = globalThis.setTimeout,
|
|
2288
2417
|
clearTimeout: cancelTimeout = globalThis.clearTimeout,
|
|
2289
2418
|
random = Math.random
|
|
@@ -2294,6 +2423,8 @@ function startPresenceSocket(params) {
|
|
|
2294
2423
|
let reconnectTimer = null;
|
|
2295
2424
|
let agents = /* @__PURE__ */ new Map();
|
|
2296
2425
|
let lastReportedOnCall = null;
|
|
2426
|
+
let sentOnCall = null;
|
|
2427
|
+
let line = null;
|
|
2297
2428
|
let connection = null;
|
|
2298
2429
|
let byeOnArrival = false;
|
|
2299
2430
|
const stop = (options) => {
|
|
@@ -2310,6 +2441,12 @@ function startPresenceSocket(params) {
|
|
|
2310
2441
|
connection?.close();
|
|
2311
2442
|
connection = null;
|
|
2312
2443
|
};
|
|
2444
|
+
const send = (onCall) => {
|
|
2445
|
+
if (!connection) return;
|
|
2446
|
+
connection.reportCallState(onCall);
|
|
2447
|
+
sentOnCall = onCall;
|
|
2448
|
+
};
|
|
2449
|
+
const mayReport = (onCall) => line !== "elsewhere" || !onCall && sentOnCall === true;
|
|
2313
2450
|
const scheduleReconnect = () => {
|
|
2314
2451
|
if (stopped) return;
|
|
2315
2452
|
attempt += 1;
|
|
@@ -2338,10 +2475,23 @@ function startPresenceSocket(params) {
|
|
|
2338
2475
|
if (connection === null) return;
|
|
2339
2476
|
connection = null;
|
|
2340
2477
|
scheduleReconnect();
|
|
2478
|
+
onDisconnect?.();
|
|
2479
|
+
},
|
|
2480
|
+
onLine: (update) => {
|
|
2481
|
+
if (stopped) return;
|
|
2482
|
+
const was = line;
|
|
2483
|
+
line = update.state;
|
|
2484
|
+
if (was === "elsewhere" && line === "here" && lastReportedOnCall !== null) send(lastReportedOnCall);
|
|
2485
|
+
onLine?.(update);
|
|
2486
|
+
},
|
|
2487
|
+
onClaimResult: (result) => {
|
|
2488
|
+
if (!stopped) onClaimResult?.(result);
|
|
2341
2489
|
}
|
|
2342
2490
|
});
|
|
2343
2491
|
} catch {
|
|
2344
|
-
if (
|
|
2492
|
+
if (stopped) return;
|
|
2493
|
+
scheduleReconnect();
|
|
2494
|
+
onConnectFailed?.();
|
|
2345
2495
|
return;
|
|
2346
2496
|
}
|
|
2347
2497
|
if (stopped) {
|
|
@@ -2351,23 +2501,64 @@ function startPresenceSocket(params) {
|
|
|
2351
2501
|
}
|
|
2352
2502
|
connection = opened;
|
|
2353
2503
|
attempt = 0;
|
|
2354
|
-
|
|
2504
|
+
sentOnCall = null;
|
|
2505
|
+
line = null;
|
|
2506
|
+
if (clientId !== void 0 && opened.hello) opened.hello(clientId);
|
|
2507
|
+
else onLineUnsupported?.();
|
|
2508
|
+
if (lastReportedOnCall !== null) send(lastReportedOnCall);
|
|
2355
2509
|
};
|
|
2356
2510
|
void connect();
|
|
2357
2511
|
return {
|
|
2358
2512
|
reportCallState: (onCall) => {
|
|
2359
2513
|
if (onCall === lastReportedOnCall) return;
|
|
2360
2514
|
lastReportedOnCall = onCall;
|
|
2361
|
-
|
|
2515
|
+
if (mayReport(onCall)) send(onCall);
|
|
2516
|
+
},
|
|
2517
|
+
claimLine: () => {
|
|
2518
|
+
if (!connection?.claimLine) return false;
|
|
2519
|
+
connection.claimLine();
|
|
2520
|
+
return true;
|
|
2362
2521
|
},
|
|
2363
2522
|
stop
|
|
2364
2523
|
};
|
|
2365
2524
|
}
|
|
2366
2525
|
|
|
2526
|
+
// src/core/line.ts
|
|
2527
|
+
var CLIENT_ID_KEY = "coventia-softphone:client-id";
|
|
2528
|
+
var CLIENT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
2529
|
+
function lineClientId() {
|
|
2530
|
+
const stored = readStoredId();
|
|
2531
|
+
if (stored) return stored;
|
|
2532
|
+
const id = freshClientId();
|
|
2533
|
+
try {
|
|
2534
|
+
if (isBrowser()) globalThis.sessionStorage?.setItem(CLIENT_ID_KEY, id);
|
|
2535
|
+
} catch {
|
|
2536
|
+
}
|
|
2537
|
+
return id;
|
|
2538
|
+
}
|
|
2539
|
+
function readStoredId() {
|
|
2540
|
+
if (!isBrowser()) return null;
|
|
2541
|
+
try {
|
|
2542
|
+
const value = globalThis.sessionStorage?.getItem(CLIENT_ID_KEY);
|
|
2543
|
+
return value && CLIENT_ID_PATTERN.test(value) ? value : null;
|
|
2544
|
+
} catch {
|
|
2545
|
+
return null;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
function freshClientId() {
|
|
2549
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
2550
|
+
if (uuid) return uuid;
|
|
2551
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2367
2554
|
// src/core/softphone-impl.ts
|
|
2368
2555
|
init_errors();
|
|
2369
2556
|
var liveInstance = false;
|
|
2370
2557
|
var PRESENCE_TEARDOWN_GRACE_MS = 1e4;
|
|
2558
|
+
var LINE_ANSWER_TIMEOUT_MS = 4e3;
|
|
2559
|
+
var CALL_DECISION_TIMEOUT_MS = LINE_ANSWER_TIMEOUT_MS + 2e3;
|
|
2560
|
+
var LINE_CLAIM_TIMEOUT_MS = 1e4;
|
|
2561
|
+
var LINE_LAPSE_MS = 1e4;
|
|
2371
2562
|
var BrowserAudioDevices = class {
|
|
2372
2563
|
inputs = [];
|
|
2373
2564
|
outputs = [];
|
|
@@ -2475,6 +2666,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2475
2666
|
#activeCall = null;
|
|
2476
2667
|
#destroyed = false;
|
|
2477
2668
|
#startedHere = false;
|
|
2669
|
+
/** Bumped by every `stop()`, so a `start()` still waiting to lead can tell it was stopped. */
|
|
2670
|
+
#run = 0;
|
|
2478
2671
|
#isLeader = false;
|
|
2479
2672
|
#engine;
|
|
2480
2673
|
#tabLock;
|
|
@@ -2499,6 +2692,52 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2499
2692
|
/** A pending state-driven teardown of the watch — see `PRESENCE_TEARDOWN_GRACE_MS`. */
|
|
2500
2693
|
#presenceTeardownTimer = null;
|
|
2501
2694
|
#presenceAgents = [];
|
|
2695
|
+
// ── The line owner (`docs/specs/line-owner.md` §5) ──────────────────────────────────────────────
|
|
2696
|
+
// Heard over the presence watch: the server names ONE softphone per extension that listens, and this one
|
|
2697
|
+
// runs its engine only while that is it (or nobody can say). `#engineOn` and `#standby` are the two halves
|
|
2698
|
+
// of that decision, kept apart because neither is true while `start()` is still waiting to make it.
|
|
2699
|
+
#line = "unknown";
|
|
2700
|
+
#lineWhere = null;
|
|
2701
|
+
/** How the last attempt to bring the line here went (`lineClaim`), whoever made it. */
|
|
2702
|
+
#lineClaim = "idle";
|
|
2703
|
+
/** This tab's id in `hello` (`lineClientId`), read once — one id for every reconnect of this softphone. */
|
|
2704
|
+
#clientId = null;
|
|
2705
|
+
/** What the line wants: the engine running. Set the moment the line decides; the engine follows (`#queueEngine`). */
|
|
2706
|
+
#engineOn = false;
|
|
2707
|
+
/**
|
|
2708
|
+
* What the engine was last TOLD by the queue — started or stopped. Apart from `#engineOn` because the line
|
|
2709
|
+
* can flip twice while a start is still running: the stop and start queued meanwhile then find nothing
|
|
2710
|
+
* to do, instead of stopping the engine (and the softphone with it) for a moment.
|
|
2711
|
+
*/
|
|
2712
|
+
#engineRunning = false;
|
|
2713
|
+
/** Waiting in `standby`: the engine is stopped ON PURPOSE, so its own `stopped` is not the softphone's. */
|
|
2714
|
+
#standby = false;
|
|
2715
|
+
/**
|
|
2716
|
+
* Every engine start and stop the line asks for, one after another: a promotion can follow a demotion
|
|
2717
|
+
* within a moment, and an engine started while its own stop is still running can end up neither.
|
|
2718
|
+
*/
|
|
2719
|
+
#engineSteps = Promise.resolve();
|
|
2720
|
+
/** Dials in flight (`call()` awaiting `placeCall`): a call about to exist holds the line like one that does. */
|
|
2721
|
+
#dialing = 0;
|
|
2722
|
+
/**
|
|
2723
|
+
* Open from `start()`'s first line until it has decided whether the engine runs — or given up (stopped,
|
|
2724
|
+
* destroyed, failed). `call()` waits on it, so it never dials ahead of that decision: not on a phone
|
|
2725
|
+
* stopped meanwhile, and not without the line.
|
|
2726
|
+
*/
|
|
2727
|
+
#startDecision = null;
|
|
2728
|
+
/**
|
|
2729
|
+
* Who decides whether the engine runs. `off`: nobody — not started, a follower, or `stop()` has begun
|
|
2730
|
+
* (whatever its teardown sets off, like a call ending, must not start anything). `deciding`: `start()`
|
|
2731
|
+
* has leadership and owes the FIRST decision itself, since it must await the engine start — a line
|
|
2732
|
+
* message meanwhile only records what it says. `on`: the line decides (`#applyLine`).
|
|
2733
|
+
*/
|
|
2734
|
+
#lineMode = "off";
|
|
2735
|
+
/** Ends `start()`'s wait for the server's first word on the line early (`#waitForFirstLine`). */
|
|
2736
|
+
#releaseFirstLine = null;
|
|
2737
|
+
/** The `takeLine()` in flight — one at a time; a second caller shares it. */
|
|
2738
|
+
#claim = null;
|
|
2739
|
+
/** Pending: what the server said about the line stops counting (`LINE_LAPSE_MS`). */
|
|
2740
|
+
#lineLapseTimer = null;
|
|
2502
2741
|
/**
|
|
2503
2742
|
* `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
|
|
2504
2743
|
* internal seam, not something an integrator tunes). Tests construct this class directly with a
|
|
@@ -2516,7 +2755,9 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2516
2755
|
this.sources = config.coventia || config.apiBase ? coventiaBackend(config.coventia ?? {}, cachedTokenSource(config.tokenSource), config.apiBase) : null;
|
|
2517
2756
|
this.#resolveContact = config.resolveContact ?? this.sources?.resolveContact;
|
|
2518
2757
|
this.#engine.onIncoming((engineCall) => this.#onIncoming(engineCall));
|
|
2519
|
-
this.#engine.onStateChange((state) =>
|
|
2758
|
+
this.#engine.onStateChange((state) => {
|
|
2759
|
+
if (!this.#standby) this.#setState(state);
|
|
2760
|
+
});
|
|
2520
2761
|
this.#engine.onError?.((error) => this.#onError(error));
|
|
2521
2762
|
}
|
|
2522
2763
|
get state() {
|
|
@@ -2534,6 +2775,15 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2534
2775
|
get presence() {
|
|
2535
2776
|
return this.#presenceAgents;
|
|
2536
2777
|
}
|
|
2778
|
+
get line() {
|
|
2779
|
+
return this.#line;
|
|
2780
|
+
}
|
|
2781
|
+
get lineWhere() {
|
|
2782
|
+
return this.#lineWhere;
|
|
2783
|
+
}
|
|
2784
|
+
get lineClaim() {
|
|
2785
|
+
return this.#lineClaim;
|
|
2786
|
+
}
|
|
2537
2787
|
get canPlayAudio() {
|
|
2538
2788
|
return true;
|
|
2539
2789
|
}
|
|
@@ -2549,15 +2799,33 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2549
2799
|
}
|
|
2550
2800
|
liveInstance = true;
|
|
2551
2801
|
this.#startedHere = true;
|
|
2552
|
-
this.#
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2802
|
+
const run = this.#run;
|
|
2803
|
+
const decision = this.#openStartDecision();
|
|
2804
|
+
try {
|
|
2805
|
+
this.#setState("starting");
|
|
2806
|
+
await this.#tabLock.acquireLeadership();
|
|
2807
|
+
if (this.#destroyed || run !== this.#run) return;
|
|
2808
|
+
this.#lineMode = "deciding";
|
|
2809
|
+
this.#setLeader(true);
|
|
2810
|
+
if (this.#presenceWatch && this.#line === "unknown") await this.#waitForFirstLine();
|
|
2811
|
+
if (this.#destroyed || run !== this.#run) return;
|
|
2812
|
+
this.#lineMode = "on";
|
|
2813
|
+
const engineStart = this.#applyLine();
|
|
2814
|
+
decision.end();
|
|
2815
|
+
await engineStart;
|
|
2816
|
+
} finally {
|
|
2817
|
+
decision.end();
|
|
2818
|
+
}
|
|
2557
2819
|
}
|
|
2558
2820
|
async stop() {
|
|
2559
2821
|
if (!this.#startedHere) return;
|
|
2822
|
+
this.#run += 1;
|
|
2823
|
+
this.#lineMode = "off";
|
|
2560
2824
|
this.#sayGoodbye();
|
|
2825
|
+
this.#startDecision?.end();
|
|
2826
|
+
this.#standby = false;
|
|
2827
|
+
this.#engineOn = false;
|
|
2828
|
+
this.#engineRunning = false;
|
|
2561
2829
|
this.#tabLock.release();
|
|
2562
2830
|
if (this.#isLeader) {
|
|
2563
2831
|
await this.#engine.stop();
|
|
@@ -2579,16 +2847,50 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2579
2847
|
this.#setState("destroyed");
|
|
2580
2848
|
this.clear();
|
|
2581
2849
|
}
|
|
2850
|
+
async takeLine() {
|
|
2851
|
+
this.#assertUsable();
|
|
2852
|
+
if (this.#line === "here") return;
|
|
2853
|
+
if (this.#claim) return this.#claim.promise;
|
|
2854
|
+
const watch = this.#presenceWatch;
|
|
2855
|
+
if (!watch) {
|
|
2856
|
+
this.#setLineClaim("failed");
|
|
2857
|
+
throw createError("line/unavailable");
|
|
2858
|
+
}
|
|
2859
|
+
const claim = this.#openClaim();
|
|
2860
|
+
if (!watch.claimLine()) this.#settleClaim(createError("line/unavailable"));
|
|
2861
|
+
return claim;
|
|
2862
|
+
}
|
|
2582
2863
|
async call(destination, options) {
|
|
2583
2864
|
this.#assertUsable();
|
|
2865
|
+
const run = this.#run;
|
|
2866
|
+
if (this.#startDecision) {
|
|
2867
|
+
let timer;
|
|
2868
|
+
const decided = await Promise.race([
|
|
2869
|
+
this.#startDecision.promise.then(() => true),
|
|
2870
|
+
new Promise((resolve) => {
|
|
2871
|
+
timer = setTimeout(() => resolve(false), CALL_DECISION_TIMEOUT_MS);
|
|
2872
|
+
})
|
|
2873
|
+
]);
|
|
2874
|
+
clearTimeout(timer);
|
|
2875
|
+
if (!decided) throw createError("engine/not-started");
|
|
2876
|
+
}
|
|
2877
|
+
if (run === this.#run && this.#state === "standby") await this.takeLine();
|
|
2878
|
+
this.#assertUsable();
|
|
2879
|
+
if (run !== this.#run) throw createError("engine/not-started");
|
|
2584
2880
|
if (this.#state !== "online" && this.#state !== "offline" && this.#state !== "starting") {
|
|
2585
2881
|
throw new Error(`Cannot place a call while softphone is '${this.#state}'.`);
|
|
2586
2882
|
}
|
|
2587
|
-
if (this.#activeCall) {
|
|
2883
|
+
if (this.#activeCall || this.#dialing > 0) {
|
|
2588
2884
|
throw new Error("A call is already in progress. Hang up before placing another.");
|
|
2589
2885
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2886
|
+
this.#dialing += 1;
|
|
2887
|
+
try {
|
|
2888
|
+
const engineCall = await this.#engine.placeCall(destination, options);
|
|
2889
|
+
return this.#adopt(engineCall);
|
|
2890
|
+
} finally {
|
|
2891
|
+
this.#dialing -= 1;
|
|
2892
|
+
this.#syncLine();
|
|
2893
|
+
}
|
|
2592
2894
|
}
|
|
2593
2895
|
// ── internals ───────────────────────────────────────────────────────────────
|
|
2594
2896
|
#onIncoming(engineCall) {
|
|
@@ -2617,6 +2919,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2617
2919
|
this.#activeCall = null;
|
|
2618
2920
|
this.emit("activeCallChanged" /* ActiveCallChanged */, { call: null });
|
|
2619
2921
|
this.#syncCallState();
|
|
2922
|
+
this.#syncLine();
|
|
2620
2923
|
}
|
|
2621
2924
|
});
|
|
2622
2925
|
if (this.#resolveContact) {
|
|
@@ -2646,7 +2949,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2646
2949
|
/**
|
|
2647
2950
|
* Reports the current on-a-call value to the backend. The watch de-duplicates (it only sends on change) and
|
|
2648
2951
|
* remembers the latest value to replay after a reconnect, so this can be called on every transition without
|
|
2649
|
-
* ceremony
|
|
2952
|
+
* ceremony — and holds it back while the line is elsewhere, since only the owner's reports count (line-owner
|
|
2953
|
+
* R8). A no-op while there is no presence watch (stopped, not the leader, no backend configured).
|
|
2650
2954
|
*/
|
|
2651
2955
|
#syncCallState() {
|
|
2652
2956
|
this.#presenceWatch?.reportCallState(this.#isOnCall());
|
|
@@ -2667,17 +2971,22 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2667
2971
|
* `presence-socket.ts`'s own `getIsLeader` is checked at start and before each reconnect attempt,
|
|
2668
2972
|
* not on a poll tick the way `startTransferWatch`'s is — see that module's own doc comment for why.
|
|
2669
2973
|
* This still passes `getIsLeader: () => this.#isLeader` for that narrower, defensive recheck; the
|
|
2670
|
-
* BROAD
|
|
2974
|
+
* BROAD started/leader gate — whether to be connected AT ALL — is owned entirely here, the same
|
|
2671
2975
|
* split `#onTransfer` already has with `startTransferWatch`.
|
|
2672
2976
|
*
|
|
2673
|
-
*
|
|
2674
|
-
*
|
|
2675
|
-
*
|
|
2676
|
-
*
|
|
2977
|
+
* The watch runs for a leader that is `starting`, `online` or in `standby` — not only `online`, as it did
|
|
2978
|
+
* before the line owner (`docs/specs/line-owner.md` §5.4): while starting it is how this softphone learns
|
|
2979
|
+
* whether to listen at all, and in standby how it learns the line came back.
|
|
2980
|
+
*
|
|
2981
|
+
* A leader that is merely `offline` or `reconnecting` keeps its watch for `PRESENCE_TEARDOWN_GRACE_MS` — a
|
|
2982
|
+
* blip shorter than that never touches the presence socket at all; a longer outage closes it, which lets
|
|
2983
|
+
* the server hand the line to a softphone that can still take calls. Anything else (stopped, destroyed, or
|
|
2984
|
+
* no longer the leader) tears it down at once: those are not blips, and a follower must never hold a
|
|
2985
|
+
* connection.
|
|
2677
2986
|
*/
|
|
2678
2987
|
#syncPresenceWatch() {
|
|
2679
2988
|
const eligible = Boolean(this.sources) && this.#isLeader;
|
|
2680
|
-
const shouldRun = eligible && this.#state
|
|
2989
|
+
const shouldRun = eligible && this.#presenceRunsIn(this.#state);
|
|
2681
2990
|
const blip = eligible && (this.#state === "offline" || this.#state === "reconnecting");
|
|
2682
2991
|
if (!blip) this.#cancelPresenceTeardown();
|
|
2683
2992
|
if (blip && this.#presenceWatch !== null) {
|
|
@@ -2686,13 +2995,22 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2686
2995
|
}
|
|
2687
2996
|
if (shouldRun === (this.#presenceWatch !== null)) return;
|
|
2688
2997
|
if (shouldRun) {
|
|
2998
|
+
this.#clientId ??= lineClientId();
|
|
2689
2999
|
this.#presenceWatch = startPresenceSocket({
|
|
2690
3000
|
source: this.sources.presence,
|
|
2691
3001
|
getIsLeader: () => this.#isLeader,
|
|
2692
3002
|
onAgentsChanged: (agents) => {
|
|
2693
3003
|
this.#presenceAgents = agents;
|
|
2694
3004
|
this.emit("presenceChanged" /* PresenceChanged */, { agents });
|
|
2695
|
-
}
|
|
3005
|
+
},
|
|
3006
|
+
clientId: this.#clientId,
|
|
3007
|
+
onLine: (update) => this.#onLine(update),
|
|
3008
|
+
onClaimResult: (result) => this.#onClaimResult(result),
|
|
3009
|
+
// A source that cannot take part will never answer, and one that cannot connect cannot answer yet:
|
|
3010
|
+
// `start()` need not wait out its 4 s for either (R7). A later answer still applies.
|
|
3011
|
+
onLineUnsupported: () => this.#releaseFirstLine?.(),
|
|
3012
|
+
onConnectFailed: () => this.#releaseFirstLine?.(),
|
|
3013
|
+
onDisconnect: () => this.#onPresenceDisconnect()
|
|
2696
3014
|
});
|
|
2697
3015
|
this.#syncCallState();
|
|
2698
3016
|
} else {
|
|
@@ -2707,19 +3025,24 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2707
3025
|
if (this.#presenceTeardownTimer !== null) return;
|
|
2708
3026
|
this.#presenceTeardownTimer = setTimeout(() => {
|
|
2709
3027
|
this.#presenceTeardownTimer = null;
|
|
2710
|
-
if (this.#
|
|
3028
|
+
if (this.#isLeader && this.#presenceRunsIn(this.#state)) return;
|
|
2711
3029
|
this.#stopPresence();
|
|
2712
3030
|
}, PRESENCE_TEARDOWN_GRACE_MS);
|
|
2713
3031
|
}
|
|
3032
|
+
/** The states in which a leader keeps its presence watch with no clock running (`#syncPresenceWatch`). */
|
|
3033
|
+
#presenceRunsIn(state) {
|
|
3034
|
+
return state === "online" || state === "starting" || state === "standby";
|
|
3035
|
+
}
|
|
2714
3036
|
#cancelPresenceTeardown() {
|
|
2715
3037
|
if (this.#presenceTeardownTimer === null) return;
|
|
2716
3038
|
clearTimeout(this.#presenceTeardownTimer);
|
|
2717
3039
|
this.#presenceTeardownTimer = null;
|
|
2718
3040
|
}
|
|
2719
3041
|
/**
|
|
2720
|
-
* Stops the presence watch and forgets its roster. A STATE-DRIVEN teardown (
|
|
2721
|
-
*
|
|
2722
|
-
* Any pending grace-period teardown is cancelled — there is nothing
|
|
3042
|
+
* Stops the presence watch and forgets its roster, and the line. A STATE-DRIVEN teardown (an outage
|
|
3043
|
+
* outlasted the grace, the engine stopped, or leadership was lost) passes nothing and says no goodbye;
|
|
3044
|
+
* only a deliberate stop passes `bye`. Any pending grace-period teardown is cancelled — there is nothing
|
|
3045
|
+
* left for it to do.
|
|
2723
3046
|
*/
|
|
2724
3047
|
#stopPresence(options) {
|
|
2725
3048
|
this.#cancelPresenceTeardown();
|
|
@@ -2728,6 +3051,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2728
3051
|
this.#presenceWatch = null;
|
|
2729
3052
|
this.#presenceAgents = [];
|
|
2730
3053
|
this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
|
|
3054
|
+
this.#forgetLine();
|
|
2731
3055
|
}
|
|
2732
3056
|
/**
|
|
2733
3057
|
* A DELIBERATE stop (`stop()` / `destroy()`): say any call is over, then say goodbye, so the backend drops
|
|
@@ -2737,6 +3061,195 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2737
3061
|
this.#presenceWatch?.reportCallState(false);
|
|
2738
3062
|
this.#stopPresence({ bye: true });
|
|
2739
3063
|
}
|
|
3064
|
+
// ── the line owner (`docs/specs/line-owner.md` §5) ──────────────────────────────────────────────
|
|
3065
|
+
/**
|
|
3066
|
+
* THE decision: from where the line is and whether a call is in progress, whether the engine runs, and
|
|
3067
|
+
* the state that says so. Synchronous, so the state moves the instant the line does — the waiting screen
|
|
3068
|
+
* goes the moment the line comes here, and a `call()` that took the line dials from `starting` without
|
|
3069
|
+
* waiting out the engine start. Only the engine start or stop is queued (`#queueEngine`). Returns that
|
|
3070
|
+
* queued step, or `null` when nothing had to change.
|
|
3071
|
+
*/
|
|
3072
|
+
#applyLine() {
|
|
3073
|
+
if (this.#lineMode !== "on") return null;
|
|
3074
|
+
const run = this.#run;
|
|
3075
|
+
if (this.#line === "elsewhere") {
|
|
3076
|
+
if (this.#standby || this.#activeCall || this.#dialing > 0) return null;
|
|
3077
|
+
this.#standby = true;
|
|
3078
|
+
this.#setState("standby");
|
|
3079
|
+
if (!this.#engineOn) return null;
|
|
3080
|
+
this.#engineOn = false;
|
|
3081
|
+
return this.#queueEngine(run);
|
|
3082
|
+
}
|
|
3083
|
+
if (this.#engineOn) return null;
|
|
3084
|
+
this.#engineOn = true;
|
|
3085
|
+
if (this.#standby) {
|
|
3086
|
+
this.#standby = false;
|
|
3087
|
+
this.#setState("starting");
|
|
3088
|
+
}
|
|
3089
|
+
return this.#queueEngine(run);
|
|
3090
|
+
}
|
|
3091
|
+
/** `#applyLine` from an event — nobody to hand a failure to, and `#engineStartFailed` has reported it. */
|
|
3092
|
+
#syncLine() {
|
|
3093
|
+
void this.#applyLine()?.catch(() => {
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Brings the engine to what the line wants (`#engineOn`) once every step already queued has run — and does
|
|
3098
|
+
* nothing when it is already there: the line can flip twice while a start is still running, and the stop
|
|
3099
|
+
* and start queued meanwhile must not take the engine (and the softphone's state) down and up for nothing.
|
|
3100
|
+
* Nothing at all once `stop()` has run since `run` was read: a stopped softphone's engine is `stop()`'s
|
|
3101
|
+
* alone. Rejects with a failed start's cause; the queue itself never rejects, so one failure blocks nothing.
|
|
3102
|
+
*/
|
|
3103
|
+
#queueEngine(run) {
|
|
3104
|
+
const step = this.#engineSteps.then(async () => {
|
|
3105
|
+
if (run !== this.#run || this.#engineOn === this.#engineRunning) return;
|
|
3106
|
+
this.#engineRunning = this.#engineOn;
|
|
3107
|
+
if (this.#engineRunning) await this.#startEngine(run);
|
|
3108
|
+
else await this.#engine.stop();
|
|
3109
|
+
});
|
|
3110
|
+
this.#engineSteps = step.catch(() => {
|
|
3111
|
+
});
|
|
3112
|
+
return step;
|
|
3113
|
+
}
|
|
3114
|
+
async #startEngine(run) {
|
|
3115
|
+
const reported = this.#lastError;
|
|
3116
|
+
try {
|
|
3117
|
+
await this.#engine.start();
|
|
3118
|
+
} catch (cause) {
|
|
3119
|
+
if (run === this.#run) this.#engineStartFailed(cause, reported);
|
|
3120
|
+
throw cause;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* An engine start the line asked for failed. Kept as it was, this softphone would stay the owner on the
|
|
3125
|
+
* server — `starting`, its presence socket renewing the line — with no push socket anywhere: nobody would
|
|
3126
|
+
* listen. So it gives the line up the ordinary way, by closing its presence socket without a goodbye (the
|
|
3127
|
+
* phone was not switched off): the server hands the line on after its grace (R4). The failure is reported
|
|
3128
|
+
* like any engine failure — `SoftphoneEvent.Error` and `lastError`, which is the widget's error gate and
|
|
3129
|
+
* its "Reintentar" (`stop()` then `start()`, which asks for the line again). Nothing retries on its own.
|
|
3130
|
+
*/
|
|
3131
|
+
#engineStartFailed(cause, reportedBefore) {
|
|
3132
|
+
this.#engineOn = false;
|
|
3133
|
+
this.#engineRunning = false;
|
|
3134
|
+
this.#lineMode = "off";
|
|
3135
|
+
if (this.#lastError === reportedBefore) {
|
|
3136
|
+
this.#onError(isSoftphoneError(cause) && isFatal(cause) ? cause : createError("net/transport-failed", cause));
|
|
3137
|
+
}
|
|
3138
|
+
this.#stopPresence();
|
|
3139
|
+
this.#setState("offline");
|
|
3140
|
+
}
|
|
3141
|
+
/** `start()`'s decision (`#startDecision`): ended once, by whichever comes first — the decision or its end. */
|
|
3142
|
+
#openStartDecision() {
|
|
3143
|
+
let resolve;
|
|
3144
|
+
const promise = new Promise((r) => {
|
|
3145
|
+
resolve = r;
|
|
3146
|
+
});
|
|
3147
|
+
const decision = {
|
|
3148
|
+
promise,
|
|
3149
|
+
end: () => {
|
|
3150
|
+
resolve();
|
|
3151
|
+
if (this.#startDecision === decision) this.#startDecision = null;
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
this.#startDecision = decision;
|
|
3155
|
+
return decision;
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* `start()`'s wait for the server's first word on the line — released by that word, by a source that
|
|
3159
|
+
* will never say it, by the watch going away (`stop()` among them), or after `LINE_ANSWER_TIMEOUT_MS`.
|
|
3160
|
+
*/
|
|
3161
|
+
#waitForFirstLine() {
|
|
3162
|
+
return new Promise((resolve) => {
|
|
3163
|
+
const release = () => {
|
|
3164
|
+
clearTimeout(timer);
|
|
3165
|
+
if (this.#releaseFirstLine === release) this.#releaseFirstLine = null;
|
|
3166
|
+
resolve();
|
|
3167
|
+
};
|
|
3168
|
+
const timer = setTimeout(release, LINE_ANSWER_TIMEOUT_MS);
|
|
3169
|
+
this.#releaseFirstLine = release;
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
#onLine(update) {
|
|
3173
|
+
this.#cancelLineLapse();
|
|
3174
|
+
this.#setLine(update.state, update.state === "here" ? null : update.where);
|
|
3175
|
+
if (this.#releaseFirstLine) this.#releaseFirstLine();
|
|
3176
|
+
else this.#syncLine();
|
|
3177
|
+
if (update.state === "here") this.#settleClaim();
|
|
3178
|
+
}
|
|
3179
|
+
#onClaimResult(result) {
|
|
3180
|
+
if (result.ok) {
|
|
3181
|
+
if (this.#line === "here") this.#settleClaim();
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
this.#settleClaim(createError(result.reason === "on_call" ? "line/claim-refused" : "line/unavailable"));
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* The presence socket dropped; the watch is reconnecting. A claim sent on it will never be answered, so
|
|
3188
|
+
* it fails now rather than on its timeout. What the server said about the line holds for `LINE_LAPSE_MS`
|
|
3189
|
+
* — a reconnect is normally answered well inside that — and then stops counting: `unknown`, which a
|
|
3190
|
+
* waiting softphone answers by listening again.
|
|
3191
|
+
*/
|
|
3192
|
+
#onPresenceDisconnect() {
|
|
3193
|
+
this.#settleClaim(createError("line/unavailable"));
|
|
3194
|
+
if (this.#line === "unknown" || this.#lineLapseTimer !== null) return;
|
|
3195
|
+
this.#lineLapseTimer = setTimeout(() => {
|
|
3196
|
+
this.#lineLapseTimer = null;
|
|
3197
|
+
this.#setLine("unknown", null);
|
|
3198
|
+
this.#syncLine();
|
|
3199
|
+
}, LINE_LAPSE_MS);
|
|
3200
|
+
}
|
|
3201
|
+
#cancelLineLapse() {
|
|
3202
|
+
if (this.#lineLapseTimer === null) return;
|
|
3203
|
+
clearTimeout(this.#lineLapseTimer);
|
|
3204
|
+
this.#lineLapseTimer = null;
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* The presence watch is gone, so nothing the server said about the line holds any more: it reads
|
|
3208
|
+
* `unknown`, and every wait on it ends. Not followed by `#applyLine` — this runs inside a state change
|
|
3209
|
+
* (`#syncPresenceWatch`) or a stop, and a softphone whose watch is gone is not in `standby` anyway: that
|
|
3210
|
+
* state keeps the watch.
|
|
3211
|
+
*/
|
|
3212
|
+
#forgetLine() {
|
|
3213
|
+
this.#cancelLineLapse();
|
|
3214
|
+
this.#releaseFirstLine?.();
|
|
3215
|
+
this.#settleClaim(createError("line/unavailable"));
|
|
3216
|
+
this.#setLine("unknown", null);
|
|
3217
|
+
this.#setLineClaim("idle");
|
|
3218
|
+
}
|
|
3219
|
+
#setLine(line, where) {
|
|
3220
|
+
if (line === "here") this.#setLineClaim("idle");
|
|
3221
|
+
if (line === this.#line && where === this.#lineWhere) return;
|
|
3222
|
+
this.#line = line;
|
|
3223
|
+
this.#lineWhere = where;
|
|
3224
|
+
this.emit("lineChanged" /* LineChanged */, { line, where });
|
|
3225
|
+
}
|
|
3226
|
+
#setLineClaim(claim) {
|
|
3227
|
+
if (claim === this.#lineClaim) return;
|
|
3228
|
+
this.#lineClaim = claim;
|
|
3229
|
+
this.emit("lineClaimChanged" /* LineClaimChanged */, { claim });
|
|
3230
|
+
}
|
|
3231
|
+
#openClaim() {
|
|
3232
|
+
let settle;
|
|
3233
|
+
const promise = new Promise((resolve, reject) => {
|
|
3234
|
+
const timer = setTimeout(() => this.#settleClaim(createError("line/unavailable")), LINE_CLAIM_TIMEOUT_MS);
|
|
3235
|
+
settle = (error) => {
|
|
3236
|
+
clearTimeout(timer);
|
|
3237
|
+
if (error) reject(error);
|
|
3238
|
+
else resolve();
|
|
3239
|
+
};
|
|
3240
|
+
});
|
|
3241
|
+
this.#claim = { promise, settle };
|
|
3242
|
+
this.#setLineClaim("claiming");
|
|
3243
|
+
return promise;
|
|
3244
|
+
}
|
|
3245
|
+
/** Ends the claim in flight, if any — resolved without `error`, rejected with it. */
|
|
3246
|
+
#settleClaim(error) {
|
|
3247
|
+
const claim = this.#claim;
|
|
3248
|
+
if (!claim) return;
|
|
3249
|
+
this.#claim = null;
|
|
3250
|
+
this.#setLineClaim(!error ? "idle" : error.code === "line/claim-refused" ? "refused" : "failed");
|
|
3251
|
+
claim.settle(error);
|
|
3252
|
+
}
|
|
2740
3253
|
/**
|
|
2741
3254
|
* Phase A (transfer outcome relay, `docs/specs/transfer-outcome-relay.md`) — `CallImpl.transfer()`'s
|
|
2742
3255
|
* internal seam calls this. No-ops entirely when no Coventia backend is configured (`sources` is
|
|
@@ -2785,6 +3298,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2785
3298
|
};
|
|
2786
3299
|
|
|
2787
3300
|
// src/core/default-provider.ts
|
|
3301
|
+
init_errors();
|
|
2788
3302
|
var LazyRelay = class {
|
|
2789
3303
|
#listeners = /* @__PURE__ */ new Set();
|
|
2790
3304
|
#realOff = /* @__PURE__ */ new Map();
|
|
@@ -2807,6 +3321,7 @@ var LazyRelay = class {
|
|
|
2807
3321
|
function defaultZadarmaEngine(config) {
|
|
2808
3322
|
let real = null;
|
|
2809
3323
|
let pending = null;
|
|
3324
|
+
let run = 0;
|
|
2810
3325
|
const incoming = new LazyRelay();
|
|
2811
3326
|
const stateChange = new LazyRelay();
|
|
2812
3327
|
const error = new LazyRelay();
|
|
@@ -2830,13 +3345,20 @@ function defaultZadarmaEngine(config) {
|
|
|
2830
3345
|
return real?.state ?? "stopped";
|
|
2831
3346
|
},
|
|
2832
3347
|
async start() {
|
|
2833
|
-
|
|
3348
|
+
const current = run;
|
|
3349
|
+
const engine = await load();
|
|
3350
|
+
if (current !== run) return;
|
|
3351
|
+
await engine.start();
|
|
2834
3352
|
},
|
|
2835
3353
|
async stop() {
|
|
3354
|
+
run += 1;
|
|
2836
3355
|
if (real) await real.stop();
|
|
2837
3356
|
},
|
|
2838
3357
|
async placeCall(destination, options) {
|
|
2839
|
-
|
|
3358
|
+
const current = run;
|
|
3359
|
+
const engine = await load();
|
|
3360
|
+
if (current !== run) throw createError("engine/not-started");
|
|
3361
|
+
return engine.placeCall(destination, options);
|
|
2840
3362
|
},
|
|
2841
3363
|
onIncoming: (listener) => incoming.subscribe(listener),
|
|
2842
3364
|
onStateChange: (listener) => stateChange.subscribe(listener),
|
|
@@ -2845,8 +3367,18 @@ function defaultZadarmaEngine(config) {
|
|
|
2845
3367
|
}
|
|
2846
3368
|
|
|
2847
3369
|
// src/core/create.ts
|
|
3370
|
+
init_api_base();
|
|
2848
3371
|
function createSoftphone(config) {
|
|
2849
|
-
|
|
3372
|
+
const resolved = withNormalizedBases(config);
|
|
3373
|
+
return new SoftphoneImpl(resolveEngine(resolved), resolved);
|
|
3374
|
+
}
|
|
3375
|
+
function withNormalizedBases(config) {
|
|
3376
|
+
const { apiBase, coventia } = config;
|
|
3377
|
+
return {
|
|
3378
|
+
...config,
|
|
3379
|
+
...apiBase ? { apiBase: normalizeApiBase(apiBase) } : {},
|
|
3380
|
+
...coventia?.baseUrl ? { coventia: { ...coventia, baseUrl: normalizeApiBase(coventia.baseUrl) } } : {}
|
|
3381
|
+
};
|
|
2850
3382
|
}
|
|
2851
3383
|
function resolveEngine(config) {
|
|
2852
3384
|
if (config.provider !== void 0) {
|
|
@@ -2873,7 +3405,7 @@ function isCallEngine(value) {
|
|
|
2873
3405
|
// src/index.ts
|
|
2874
3406
|
init_errors();
|
|
2875
3407
|
init_outbound_numbers();
|
|
2876
|
-
var VERSION = "0.
|
|
3408
|
+
var VERSION = "0.5.0";
|
|
2877
3409
|
|
|
2878
3410
|
exports.CALL_TRANSITIONS = CALL_TRANSITIONS;
|
|
2879
3411
|
exports.CallEvent = CallEvent;
|