@coventia/softphone 0.3.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-CaPDWVk3.d.cts → call-FqcvDg1t.d.cts} +168 -12
- package/dist/{call-CaPDWVk3.d.ts → call-FqcvDg1t.d.ts} +168 -12
- package/dist/chunk-5E5OEXS2.js +14 -0
- package/dist/chunk-5E5OEXS2.js.map +1 -0
- 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 +198 -6
- package/dist/element/index.js +1160 -489
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-BgBsIcEa.d.ts → engine-CsNRD7B7.d.ts} +1 -1
- package/dist/{engine-CU2rEo3g.d.cts → engine-CzOCrfY9.d.cts} +1 -1
- package/dist/index.cjs +675 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -6
- package/dist/index.d.ts +16 -6
- package/dist/index.js +453 -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-DEt0TtQw.d.ts → softphone-By9sizlV.d.ts} +44 -3
- package/dist/{softphone-dZMkW8V1.d.cts → softphone-JZvJdzU3.d.cts} +44 -3
- package/dist/softphone.css +254 -23
- package/dist/{sources-M3GYMS6V.js → sources-QL2LRZVH.js} +93 -8
- 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 +670 -20
- 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 +642 -22
- 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-M3GYMS6V.js.map +0 -1
- package/dist/zadarma-engine-L5DEFXUS.js +0 -5
package/dist/index.cjs
CHANGED
|
@@ -110,9 +110,9 @@ function createRequest(config, method) {
|
|
|
110
110
|
});
|
|
111
111
|
const body = await res.json().catch(() => null);
|
|
112
112
|
const message = body?.message ?? `${path} \u2014 HTTP ${res.status}`;
|
|
113
|
-
if (!res.ok) throw new ApiError(res.status, message);
|
|
113
|
+
if (!res.ok) throw new ApiError(res.status, message, body);
|
|
114
114
|
if (body?.success === false) {
|
|
115
|
-
throw new ApiError(res.status, message);
|
|
115
|
+
throw new ApiError(res.status, message, body);
|
|
116
116
|
}
|
|
117
117
|
return body;
|
|
118
118
|
}
|
|
@@ -138,15 +138,32 @@ var init_api = __esm({
|
|
|
138
138
|
init_token_cache();
|
|
139
139
|
ApiError = class extends Error {
|
|
140
140
|
status;
|
|
141
|
-
|
|
141
|
+
body;
|
|
142
|
+
constructor(status, message, body = null) {
|
|
142
143
|
super(message);
|
|
143
144
|
this.name = "ApiError";
|
|
144
145
|
this.status = status;
|
|
146
|
+
this.body = body;
|
|
145
147
|
}
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
150
|
});
|
|
149
151
|
|
|
152
|
+
// src/core/outbound-numbers.ts
|
|
153
|
+
function isOutboundNumberRefused(value) {
|
|
154
|
+
const refusal = value?.refusal;
|
|
155
|
+
return value instanceof Error && !!refusal && (refusal.reason === "platform" || refusal.reason === "user") && typeof refusal.retryAfterSeconds === "number";
|
|
156
|
+
}
|
|
157
|
+
function outboundNumberRefused(refusal) {
|
|
158
|
+
const error = new Error(`Outbound number change refused (${refusal.reason}); retry in ${refusal.retryAfterSeconds}s`);
|
|
159
|
+
error.name = "OutboundNumberRefused";
|
|
160
|
+
return Object.assign(error, { refusal });
|
|
161
|
+
}
|
|
162
|
+
var init_outbound_numbers = __esm({
|
|
163
|
+
"src/core/outbound-numbers.ts"() {
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
150
167
|
// src/coventia/sources.ts
|
|
151
168
|
var sources_exports = {};
|
|
152
169
|
__export(sources_exports, {
|
|
@@ -228,6 +245,8 @@ function coventiaSources(config) {
|
|
|
228
245
|
// row's `called_did` is the OTHER party (already `number` above); showing it again as "DID"
|
|
229
246
|
// would just repeat the row under a wrong label.
|
|
230
247
|
...toDirection(row.direction) === "inbound" && row.called_did ? { did: row.called_did } : {},
|
|
248
|
+
// And the other way round: which of the org's numbers an outbound call showed.
|
|
249
|
+
...toDirection(row.direction) === "outbound" && row.from_number ? { from: row.from_number } : {},
|
|
231
250
|
recorded,
|
|
232
251
|
handledBy,
|
|
233
252
|
// The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with
|
|
@@ -297,6 +316,16 @@ function coventiaSources(config) {
|
|
|
297
316
|
if (ws.readyState !== WS.OPEN) return;
|
|
298
317
|
ws.send(JSON.stringify({ type: "bye" }));
|
|
299
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
|
+
},
|
|
300
329
|
close: () => {
|
|
301
330
|
ws.onclose = null;
|
|
302
331
|
ws.onerror = null;
|
|
@@ -316,6 +345,12 @@ function coventiaSources(config) {
|
|
|
316
345
|
callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));
|
|
317
346
|
} else if (msg.type === "delta" && msg.agent) {
|
|
318
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
|
+
);
|
|
319
354
|
}
|
|
320
355
|
};
|
|
321
356
|
ws.onclose = () => {
|
|
@@ -345,10 +380,10 @@ function coventiaSources(config) {
|
|
|
345
380
|
return { catalogue, orgDefault: body.orgDefault ?? null, mine: body.user ?? null, effective };
|
|
346
381
|
};
|
|
347
382
|
const CACHE_PREFIX = "coventia-softphone:ringtones:";
|
|
348
|
-
const cacheKey = async () => {
|
|
383
|
+
const cacheKey = async (prefix = CACHE_PREFIX) => {
|
|
349
384
|
try {
|
|
350
385
|
const { identity } = await config.tokenSource.getToken();
|
|
351
|
-
return `${
|
|
386
|
+
return `${prefix}${fileBase}|${config.organizationId ?? "-"}|${identity}`;
|
|
352
387
|
} catch {
|
|
353
388
|
return null;
|
|
354
389
|
}
|
|
@@ -381,6 +416,68 @@ function coventiaSources(config) {
|
|
|
381
416
|
),
|
|
382
417
|
cached: recall
|
|
383
418
|
};
|
|
419
|
+
const KINDS = /* @__PURE__ */ new Set(["landline", "mobile", "commercial", "other"]);
|
|
420
|
+
const STATES = /* @__PURE__ */ new Set(["applied", "pending", "failed", "unassigned"]);
|
|
421
|
+
const toOutboundSettings = (body) => {
|
|
422
|
+
const numbers = (body.numbers ?? []).map((n) => ({
|
|
423
|
+
id: n.id,
|
|
424
|
+
number: n.number,
|
|
425
|
+
label: n.label ?? null,
|
|
426
|
+
kind: KINDS.has(n.kind ?? "") ? n.kind : "other"
|
|
427
|
+
}));
|
|
428
|
+
const known = (id) => id && numbers.some((n) => n.id === id) ? id : null;
|
|
429
|
+
return {
|
|
430
|
+
numbers,
|
|
431
|
+
orgDefault: body.orgDefault ?? null,
|
|
432
|
+
mine: known(body.mine),
|
|
433
|
+
// Defensive: a malformed answer naming a number that is not in the list reads as the org default.
|
|
434
|
+
effective: known(body.effective) ?? known(body.orgDefault),
|
|
435
|
+
extension: body.extension ?? null,
|
|
436
|
+
state: STATES.has(body.state ?? "") ? body.state : "applied",
|
|
437
|
+
changeAvailableInSeconds: Math.max(0, Math.ceil(Number(body.changeAvailableInSeconds) || 0))
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
const OUTBOUND_PREFIX = "coventia-softphone:outbound-number:";
|
|
441
|
+
const rememberOutbound = async (settings) => {
|
|
442
|
+
const key = await cacheKey(OUTBOUND_PREFIX);
|
|
443
|
+
try {
|
|
444
|
+
if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
return settings;
|
|
448
|
+
};
|
|
449
|
+
const recallOutbound = async () => {
|
|
450
|
+
const key = await cacheKey(OUTBOUND_PREFIX);
|
|
451
|
+
try {
|
|
452
|
+
const raw = key ? globalThis.localStorage?.getItem(key) : null;
|
|
453
|
+
const s = raw ? JSON.parse(raw) : null;
|
|
454
|
+
const ok = s && Array.isArray(s.numbers) && s.numbers.every((n) => typeof n?.id === "string" && typeof n.number === "string") && (s.effective === null || s.numbers.some((n) => n.id === s.effective));
|
|
455
|
+
return ok ? { ...s, changeAvailableInSeconds: 0 } : null;
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const outboundNumbers = {
|
|
461
|
+
get: async (signal) => rememberOutbound(toOutboundSettings(await get(`${base}/outbound-numbers`, org, signal))),
|
|
462
|
+
// `null` = back to the org default: the empty id is dropped from the query, which the backend reads that way.
|
|
463
|
+
setMine: async (id) => {
|
|
464
|
+
try {
|
|
465
|
+
return rememberOutbound(
|
|
466
|
+
toOutboundSettings(await post(`${base}/outbound-numbers/user`, { ...org, did_id: id ?? "" }))
|
|
467
|
+
);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
const body = error instanceof ApiError ? error.body : null;
|
|
470
|
+
if (error instanceof ApiError && error.status === 429 && body?.code === "rate_limited") {
|
|
471
|
+
throw outboundNumberRefused({
|
|
472
|
+
reason: body.reason === "user" ? "user" : "platform",
|
|
473
|
+
retryAfterSeconds: Math.max(1, Math.ceil(Number(body.retryAfterSeconds) || 1))
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
cached: recallOutbound
|
|
480
|
+
};
|
|
384
481
|
const contacts = config.contacts ?? coventiaContacts;
|
|
385
482
|
const resolve = config.resolveContact ?? (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);
|
|
386
483
|
const history = withNameResolution(rawHistory, resolve);
|
|
@@ -393,14 +490,16 @@ function coventiaSources(config) {
|
|
|
393
490
|
listAgents: coventiaListAgents,
|
|
394
491
|
transferOutcomes,
|
|
395
492
|
presence,
|
|
396
|
-
ringtones
|
|
493
|
+
ringtones,
|
|
494
|
+
outboundNumbers
|
|
397
495
|
};
|
|
398
496
|
}
|
|
399
|
-
var toContact, toDirection, otherParty, isMissed, toOutcome, toDate, toTransferOutcome, toPresenceState, toPresenceEntry, toWebSocketUrl;
|
|
497
|
+
var toContact, toDirection, otherParty, isMissed, toOutcome, toDate, toTransferOutcome, toPresenceState, toPresenceEntry, toLineLocation, toWebSocketUrl;
|
|
400
498
|
var init_sources2 = __esm({
|
|
401
499
|
"src/coventia/sources.ts"() {
|
|
402
500
|
init_sources();
|
|
403
501
|
init_api();
|
|
502
|
+
init_outbound_numbers();
|
|
404
503
|
toContact = (row) => ({
|
|
405
504
|
id: row.wa_id,
|
|
406
505
|
name: row.name,
|
|
@@ -444,6 +543,7 @@ var init_sources2 = __esm({
|
|
|
444
543
|
name: row.name ?? null,
|
|
445
544
|
state: toPresenceState(row.state)
|
|
446
545
|
});
|
|
546
|
+
toLineLocation = (raw) => raw === "desktop" || raw === "browser" ? raw : null;
|
|
447
547
|
toWebSocketUrl = (httpUrl) => httpUrl.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
448
548
|
}
|
|
449
549
|
});
|
|
@@ -596,6 +696,19 @@ var init_errors = __esm({
|
|
|
596
696
|
message: "The provider does not support this operation.",
|
|
597
697
|
solution: "Guard the UI on the operation before offering it."
|
|
598
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
|
+
},
|
|
599
712
|
// ── Provider ────────────────────────────────────────────────────────────────
|
|
600
713
|
"provider/provision-failed": {
|
|
601
714
|
fatal: true,
|
|
@@ -895,9 +1008,22 @@ var init_ua = __esm({
|
|
|
895
1008
|
provider;
|
|
896
1009
|
factory;
|
|
897
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;
|
|
898
1022
|
#ua = null;
|
|
899
1023
|
/** In-flight `#createUA()`. THE concurrency guard — see `#ensureUA`. */
|
|
900
1024
|
#uaInFlight = null;
|
|
1025
|
+
/** Calls waiting for the current UA to register (`#callWhenRegistered`), failed by `#teardownUA`. */
|
|
1026
|
+
#awaitingRegistration = /* @__PURE__ */ new Set();
|
|
901
1027
|
#domain = "";
|
|
902
1028
|
#activeCall = null;
|
|
903
1029
|
/**
|
|
@@ -980,14 +1106,25 @@ var init_ua = __esm({
|
|
|
980
1106
|
for (const listener of [...this.#errorListeners]) listener(error);
|
|
981
1107
|
}
|
|
982
1108
|
async start() {
|
|
1109
|
+
const run = this.#run;
|
|
983
1110
|
this.#setState("starting");
|
|
984
1111
|
if (this.provider.registrationMode === "persistent") {
|
|
985
|
-
|
|
1112
|
+
try {
|
|
1113
|
+
await this.#ensureUA();
|
|
1114
|
+
} catch (cause) {
|
|
1115
|
+
if (run === this.#run) throw cause;
|
|
1116
|
+
}
|
|
1117
|
+
if (run !== this.#run) return;
|
|
986
1118
|
this.#setState("online");
|
|
987
1119
|
} else {
|
|
988
1120
|
const signal = this.provider.incomingSignal;
|
|
989
1121
|
if (signal) {
|
|
990
|
-
|
|
1122
|
+
try {
|
|
1123
|
+
await signal.connect();
|
|
1124
|
+
} catch (cause) {
|
|
1125
|
+
if (run === this.#run) throw cause;
|
|
1126
|
+
}
|
|
1127
|
+
if (run !== this.#run) return;
|
|
991
1128
|
this.#signalUnsub = signal.onSignal(() => void this.#onIncomingSignal());
|
|
992
1129
|
this.#signalConnectionUnsub = signal.onConnectionChange?.(
|
|
993
1130
|
(connected) => this.#setState(connected ? "online" : "offline")
|
|
@@ -997,6 +1134,7 @@ var init_ua = __esm({
|
|
|
997
1134
|
}
|
|
998
1135
|
}
|
|
999
1136
|
async stop() {
|
|
1137
|
+
this.#run += 1;
|
|
1000
1138
|
this.#clearNoInviteTimer();
|
|
1001
1139
|
this.#signalUnsub?.();
|
|
1002
1140
|
this.#signalUnsub = null;
|
|
@@ -1015,11 +1153,12 @@ var init_ua = __esm({
|
|
|
1015
1153
|
async #onIncomingSignal() {
|
|
1016
1154
|
if (this.now() < this.#rejectGuardUntil) return;
|
|
1017
1155
|
if (this.#activeCall) return;
|
|
1156
|
+
const run = this.#run;
|
|
1018
1157
|
try {
|
|
1019
1158
|
await this.#ensureUA();
|
|
1020
1159
|
this.#armNoInviteTeardown();
|
|
1021
1160
|
} catch {
|
|
1022
|
-
this.#teardownUA();
|
|
1161
|
+
if (run === this.#run) this.#teardownUA();
|
|
1023
1162
|
}
|
|
1024
1163
|
}
|
|
1025
1164
|
#armNoInviteTeardown() {
|
|
@@ -1081,12 +1220,16 @@ var init_ua = __esm({
|
|
|
1081
1220
|
*/
|
|
1082
1221
|
async #ensureUA() {
|
|
1083
1222
|
if (this.#ua) return this.#ua;
|
|
1084
|
-
this.#uaInFlight
|
|
1085
|
-
this.#
|
|
1086
|
-
|
|
1223
|
+
if (!this.#uaInFlight) {
|
|
1224
|
+
const inFlight = this.#createUA().finally(() => {
|
|
1225
|
+
if (this.#uaInFlight === inFlight) this.#uaInFlight = null;
|
|
1226
|
+
});
|
|
1227
|
+
this.#uaInFlight = inFlight;
|
|
1228
|
+
}
|
|
1087
1229
|
return this.#uaInFlight;
|
|
1088
1230
|
}
|
|
1089
1231
|
async #createUA() {
|
|
1232
|
+
const run = this.#run;
|
|
1090
1233
|
const factory = await this.#resolveFactory();
|
|
1091
1234
|
let credentials;
|
|
1092
1235
|
try {
|
|
@@ -1096,17 +1239,28 @@ var init_ua = __esm({
|
|
|
1096
1239
|
else this.#reportError("provider/provision-failed", cause);
|
|
1097
1240
|
throw cause;
|
|
1098
1241
|
}
|
|
1242
|
+
if (run !== this.#run) throw createError("engine/not-started");
|
|
1099
1243
|
this.#domain = domainOf(credentials);
|
|
1100
1244
|
const config = this.#uaConfig(credentials, factory);
|
|
1101
1245
|
const ua = factory.createUA(config);
|
|
1102
|
-
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
|
+
});
|
|
1103
1253
|
ua.on("disconnected", ({ error }) => {
|
|
1254
|
+
if (ua !== this.#ua) return;
|
|
1104
1255
|
if (!error) return;
|
|
1105
1256
|
this.#setState("reconnecting");
|
|
1106
1257
|
this.#reportError("net/transport-failed", error);
|
|
1107
1258
|
});
|
|
1108
|
-
ua.on("connected", () =>
|
|
1259
|
+
ua.on("connected", () => {
|
|
1260
|
+
if (ua === this.#ua) this.#setState("online");
|
|
1261
|
+
});
|
|
1109
1262
|
ua.on("registrationFailed", (e) => {
|
|
1263
|
+
if (run !== this.#run) return;
|
|
1110
1264
|
const detail = e;
|
|
1111
1265
|
this.#reportError("net/registration-failed", {
|
|
1112
1266
|
cause: detail?.cause,
|
|
@@ -1114,7 +1268,7 @@ var init_ua = __esm({
|
|
|
1114
1268
|
domain: this.#domain,
|
|
1115
1269
|
hint: "A bare 401 with no auth challenge usually means the registrar is in the wrong datacenter (SPEC \xA73.1)."
|
|
1116
1270
|
});
|
|
1117
|
-
this.#teardownUA();
|
|
1271
|
+
if (ua === this.#ua) this.#teardownUA();
|
|
1118
1272
|
void this.#refreshSignal();
|
|
1119
1273
|
});
|
|
1120
1274
|
this.#ua = ua;
|
|
@@ -1141,6 +1295,7 @@ var init_ua = __esm({
|
|
|
1141
1295
|
#teardownUA() {
|
|
1142
1296
|
this.#uaInFlight = null;
|
|
1143
1297
|
if (!this.#ua) return;
|
|
1298
|
+
for (const fail of [...this.#awaitingRegistration]) fail();
|
|
1144
1299
|
try {
|
|
1145
1300
|
this.#ua.terminateSessions({ status_code: 487 });
|
|
1146
1301
|
this.#ua.stop();
|
|
@@ -1164,8 +1319,27 @@ var init_ua = __esm({
|
|
|
1164
1319
|
#callWhenRegistered(ua, target) {
|
|
1165
1320
|
const options = { mediaConstraints: buildAudioConstraints(this.#inputDeviceId), pcConfig: PC_CONFIG };
|
|
1166
1321
|
if (ua.isRegistered()) return Promise.resolve(ua.call(target, options));
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
+
});
|
|
1169
1343
|
});
|
|
1170
1344
|
}
|
|
1171
1345
|
#wrapCall(session) {
|
|
@@ -1315,6 +1489,12 @@ var init_zadarma = __esm({
|
|
|
1315
1489
|
#retryTimer;
|
|
1316
1490
|
#unwatchBackground;
|
|
1317
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;
|
|
1318
1498
|
#listeners = /* @__PURE__ */ new Set();
|
|
1319
1499
|
#connectionListeners = /* @__PURE__ */ new Set();
|
|
1320
1500
|
async connect() {
|
|
@@ -1324,8 +1504,9 @@ var init_zadarma = __esm({
|
|
|
1324
1504
|
await this.#open();
|
|
1325
1505
|
}
|
|
1326
1506
|
async #open() {
|
|
1507
|
+
const run = this.#run;
|
|
1327
1508
|
const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
|
|
1328
|
-
if (this.#
|
|
1509
|
+
if (run !== this.#run) return;
|
|
1329
1510
|
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
1330
1511
|
this.#socket = socket;
|
|
1331
1512
|
this.#attach(socket);
|
|
@@ -1420,9 +1601,10 @@ var init_zadarma = __esm({
|
|
|
1420
1601
|
return;
|
|
1421
1602
|
}
|
|
1422
1603
|
this.#rebuilding = true;
|
|
1604
|
+
const run = this.#run;
|
|
1423
1605
|
try {
|
|
1424
1606
|
const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
|
|
1425
|
-
if (this.#
|
|
1607
|
+
if (run !== this.#run) return;
|
|
1426
1608
|
const swapped = await new Promise((resolve) => {
|
|
1427
1609
|
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
1428
1610
|
this.#candidate = socket;
|
|
@@ -1475,6 +1657,7 @@ var init_zadarma = __esm({
|
|
|
1475
1657
|
}
|
|
1476
1658
|
disconnect() {
|
|
1477
1659
|
this.#closed = true;
|
|
1660
|
+
this.#run += 1;
|
|
1478
1661
|
this.#stopPings();
|
|
1479
1662
|
clearTimeout(this.#refreshTimer);
|
|
1480
1663
|
this.#refreshTimer = void 0;
|
|
@@ -1718,6 +1901,20 @@ var init_zadarma_browser = __esm({
|
|
|
1718
1901
|
}
|
|
1719
1902
|
});
|
|
1720
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
|
+
|
|
1721
1918
|
// src/transports/zadarma-engine.ts
|
|
1722
1919
|
var zadarma_engine_exports = {};
|
|
1723
1920
|
__export(zadarma_engine_exports, {
|
|
@@ -1725,7 +1922,11 @@ __export(zadarma_engine_exports, {
|
|
|
1725
1922
|
});
|
|
1726
1923
|
function createZadarmaEngine(config) {
|
|
1727
1924
|
const provider = new ZadarmaProvider(
|
|
1728
|
-
makeZadarmaBrowserDeps({
|
|
1925
|
+
makeZadarmaBrowserDeps({
|
|
1926
|
+
...config,
|
|
1927
|
+
apiBase: normalizeApiBase(config.apiBase),
|
|
1928
|
+
tokenSource: cachedTokenSource(config.tokenSource)
|
|
1929
|
+
})
|
|
1729
1930
|
);
|
|
1730
1931
|
return new SipCallEngine(provider, loadJsSipFactory);
|
|
1731
1932
|
}
|
|
@@ -1736,6 +1937,7 @@ var init_zadarma_engine = __esm({
|
|
|
1736
1937
|
init_zadarma();
|
|
1737
1938
|
init_zadarma_browser();
|
|
1738
1939
|
init_token_cache();
|
|
1940
|
+
init_api_base();
|
|
1739
1941
|
}
|
|
1740
1942
|
});
|
|
1741
1943
|
|
|
@@ -1799,6 +2001,8 @@ var SoftphoneEvent = /* @__PURE__ */ ((SoftphoneEvent2) => {
|
|
|
1799
2001
|
SoftphoneEvent2["LeaderChanged"] = "leaderChanged";
|
|
1800
2002
|
SoftphoneEvent2["TransferOutcome"] = "transferOutcome";
|
|
1801
2003
|
SoftphoneEvent2["PresenceChanged"] = "presenceChanged";
|
|
2004
|
+
SoftphoneEvent2["LineChanged"] = "lineChanged";
|
|
2005
|
+
SoftphoneEvent2["LineClaimChanged"] = "lineClaimChanged";
|
|
1802
2006
|
return SoftphoneEvent2;
|
|
1803
2007
|
})(SoftphoneEvent || {});
|
|
1804
2008
|
var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
|
|
@@ -2062,6 +2266,11 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
|
|
|
2062
2266
|
get: async (signal) => (await load()).ringtones.get(signal),
|
|
2063
2267
|
setMine: async (id) => (await load()).ringtones.setMine(id),
|
|
2064
2268
|
cached: async () => (await load()).ringtones.cached?.() ?? null
|
|
2269
|
+
},
|
|
2270
|
+
outboundNumbers: {
|
|
2271
|
+
get: async (signal) => (await load()).outboundNumbers.get(signal),
|
|
2272
|
+
setMine: async (id) => (await load()).outboundNumbers.setMine(id),
|
|
2273
|
+
cached: async () => (await load()).outboundNumbers.cached?.() ?? null
|
|
2065
2274
|
}
|
|
2066
2275
|
};
|
|
2067
2276
|
}
|
|
@@ -2083,13 +2292,15 @@ function isWebLocksSupported() {
|
|
|
2083
2292
|
return isBrowser() && typeof navigator.locks?.request === "function";
|
|
2084
2293
|
}
|
|
2085
2294
|
function webLocksTabLock(lockName = LOCK_NAME) {
|
|
2086
|
-
|
|
2295
|
+
let controller;
|
|
2087
2296
|
let releaseHold;
|
|
2088
2297
|
return {
|
|
2089
2298
|
acquireLeadership() {
|
|
2090
2299
|
if (!isWebLocksSupported()) return Promise.resolve();
|
|
2300
|
+
const { signal } = controller = new AbortController();
|
|
2091
2301
|
return new Promise((resolveAcquired) => {
|
|
2092
|
-
navigator.locks.request(lockName, { signal
|
|
2302
|
+
navigator.locks.request(lockName, { signal }, () => {
|
|
2303
|
+
if (signal.aborted) return Promise.resolve();
|
|
2093
2304
|
resolveAcquired();
|
|
2094
2305
|
return new Promise((resolveHold) => {
|
|
2095
2306
|
releaseHold = resolveHold;
|
|
@@ -2104,7 +2315,7 @@ function webLocksTabLock(lockName = LOCK_NAME) {
|
|
|
2104
2315
|
releaseHold = void 0;
|
|
2105
2316
|
resolve();
|
|
2106
2317
|
} else {
|
|
2107
|
-
controller
|
|
2318
|
+
controller?.abort();
|
|
2108
2319
|
}
|
|
2109
2320
|
}
|
|
2110
2321
|
};
|
|
@@ -2189,13 +2400,19 @@ function startTransferWatch(params) {
|
|
|
2189
2400
|
var RECONNECT_BASE_MS = 1e3;
|
|
2190
2401
|
var RECONNECT_MAX_MS = 3e4;
|
|
2191
2402
|
var NOOP_WATCH = { reportCallState: () => {
|
|
2192
|
-
}, stop: () => {
|
|
2403
|
+
}, claimLine: () => false, stop: () => {
|
|
2193
2404
|
} };
|
|
2194
2405
|
function startPresenceSocket(params) {
|
|
2195
2406
|
const {
|
|
2196
2407
|
source,
|
|
2197
2408
|
getIsLeader,
|
|
2198
2409
|
onAgentsChanged,
|
|
2410
|
+
clientId,
|
|
2411
|
+
onLine,
|
|
2412
|
+
onClaimResult,
|
|
2413
|
+
onLineUnsupported,
|
|
2414
|
+
onConnectFailed,
|
|
2415
|
+
onDisconnect,
|
|
2199
2416
|
setTimeout: scheduleTimeout = globalThis.setTimeout,
|
|
2200
2417
|
clearTimeout: cancelTimeout = globalThis.clearTimeout,
|
|
2201
2418
|
random = Math.random
|
|
@@ -2206,6 +2423,8 @@ function startPresenceSocket(params) {
|
|
|
2206
2423
|
let reconnectTimer = null;
|
|
2207
2424
|
let agents = /* @__PURE__ */ new Map();
|
|
2208
2425
|
let lastReportedOnCall = null;
|
|
2426
|
+
let sentOnCall = null;
|
|
2427
|
+
let line = null;
|
|
2209
2428
|
let connection = null;
|
|
2210
2429
|
let byeOnArrival = false;
|
|
2211
2430
|
const stop = (options) => {
|
|
@@ -2222,6 +2441,12 @@ function startPresenceSocket(params) {
|
|
|
2222
2441
|
connection?.close();
|
|
2223
2442
|
connection = null;
|
|
2224
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;
|
|
2225
2450
|
const scheduleReconnect = () => {
|
|
2226
2451
|
if (stopped) return;
|
|
2227
2452
|
attempt += 1;
|
|
@@ -2250,10 +2475,23 @@ function startPresenceSocket(params) {
|
|
|
2250
2475
|
if (connection === null) return;
|
|
2251
2476
|
connection = null;
|
|
2252
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);
|
|
2253
2489
|
}
|
|
2254
2490
|
});
|
|
2255
2491
|
} catch {
|
|
2256
|
-
if (
|
|
2492
|
+
if (stopped) return;
|
|
2493
|
+
scheduleReconnect();
|
|
2494
|
+
onConnectFailed?.();
|
|
2257
2495
|
return;
|
|
2258
2496
|
}
|
|
2259
2497
|
if (stopped) {
|
|
@@ -2263,23 +2501,64 @@ function startPresenceSocket(params) {
|
|
|
2263
2501
|
}
|
|
2264
2502
|
connection = opened;
|
|
2265
2503
|
attempt = 0;
|
|
2266
|
-
|
|
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);
|
|
2267
2509
|
};
|
|
2268
2510
|
void connect();
|
|
2269
2511
|
return {
|
|
2270
2512
|
reportCallState: (onCall) => {
|
|
2271
2513
|
if (onCall === lastReportedOnCall) return;
|
|
2272
2514
|
lastReportedOnCall = onCall;
|
|
2273
|
-
|
|
2515
|
+
if (mayReport(onCall)) send(onCall);
|
|
2516
|
+
},
|
|
2517
|
+
claimLine: () => {
|
|
2518
|
+
if (!connection?.claimLine) return false;
|
|
2519
|
+
connection.claimLine();
|
|
2520
|
+
return true;
|
|
2274
2521
|
},
|
|
2275
2522
|
stop
|
|
2276
2523
|
};
|
|
2277
2524
|
}
|
|
2278
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
|
+
|
|
2279
2554
|
// src/core/softphone-impl.ts
|
|
2280
2555
|
init_errors();
|
|
2281
2556
|
var liveInstance = false;
|
|
2282
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;
|
|
2283
2562
|
var BrowserAudioDevices = class {
|
|
2284
2563
|
inputs = [];
|
|
2285
2564
|
outputs = [];
|
|
@@ -2387,6 +2666,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2387
2666
|
#activeCall = null;
|
|
2388
2667
|
#destroyed = false;
|
|
2389
2668
|
#startedHere = false;
|
|
2669
|
+
/** Bumped by every `stop()`, so a `start()` still waiting to lead can tell it was stopped. */
|
|
2670
|
+
#run = 0;
|
|
2390
2671
|
#isLeader = false;
|
|
2391
2672
|
#engine;
|
|
2392
2673
|
#tabLock;
|
|
@@ -2411,6 +2692,52 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2411
2692
|
/** A pending state-driven teardown of the watch — see `PRESENCE_TEARDOWN_GRACE_MS`. */
|
|
2412
2693
|
#presenceTeardownTimer = null;
|
|
2413
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;
|
|
2414
2741
|
/**
|
|
2415
2742
|
* `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
|
|
2416
2743
|
* internal seam, not something an integrator tunes). Tests construct this class directly with a
|
|
@@ -2428,7 +2755,9 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2428
2755
|
this.sources = config.coventia || config.apiBase ? coventiaBackend(config.coventia ?? {}, cachedTokenSource(config.tokenSource), config.apiBase) : null;
|
|
2429
2756
|
this.#resolveContact = config.resolveContact ?? this.sources?.resolveContact;
|
|
2430
2757
|
this.#engine.onIncoming((engineCall) => this.#onIncoming(engineCall));
|
|
2431
|
-
this.#engine.onStateChange((state) =>
|
|
2758
|
+
this.#engine.onStateChange((state) => {
|
|
2759
|
+
if (!this.#standby) this.#setState(state);
|
|
2760
|
+
});
|
|
2432
2761
|
this.#engine.onError?.((error) => this.#onError(error));
|
|
2433
2762
|
}
|
|
2434
2763
|
get state() {
|
|
@@ -2446,6 +2775,15 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2446
2775
|
get presence() {
|
|
2447
2776
|
return this.#presenceAgents;
|
|
2448
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
|
+
}
|
|
2449
2787
|
get canPlayAudio() {
|
|
2450
2788
|
return true;
|
|
2451
2789
|
}
|
|
@@ -2461,15 +2799,33 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2461
2799
|
}
|
|
2462
2800
|
liveInstance = true;
|
|
2463
2801
|
this.#startedHere = true;
|
|
2464
|
-
this.#
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
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
|
+
}
|
|
2469
2819
|
}
|
|
2470
2820
|
async stop() {
|
|
2471
2821
|
if (!this.#startedHere) return;
|
|
2822
|
+
this.#run += 1;
|
|
2823
|
+
this.#lineMode = "off";
|
|
2472
2824
|
this.#sayGoodbye();
|
|
2825
|
+
this.#startDecision?.end();
|
|
2826
|
+
this.#standby = false;
|
|
2827
|
+
this.#engineOn = false;
|
|
2828
|
+
this.#engineRunning = false;
|
|
2473
2829
|
this.#tabLock.release();
|
|
2474
2830
|
if (this.#isLeader) {
|
|
2475
2831
|
await this.#engine.stop();
|
|
@@ -2491,16 +2847,50 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2491
2847
|
this.#setState("destroyed");
|
|
2492
2848
|
this.clear();
|
|
2493
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
|
+
}
|
|
2494
2863
|
async call(destination, options) {
|
|
2495
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");
|
|
2496
2880
|
if (this.#state !== "online" && this.#state !== "offline" && this.#state !== "starting") {
|
|
2497
2881
|
throw new Error(`Cannot place a call while softphone is '${this.#state}'.`);
|
|
2498
2882
|
}
|
|
2499
|
-
if (this.#activeCall) {
|
|
2883
|
+
if (this.#activeCall || this.#dialing > 0) {
|
|
2500
2884
|
throw new Error("A call is already in progress. Hang up before placing another.");
|
|
2501
2885
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
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
|
+
}
|
|
2504
2894
|
}
|
|
2505
2895
|
// ── internals ───────────────────────────────────────────────────────────────
|
|
2506
2896
|
#onIncoming(engineCall) {
|
|
@@ -2529,6 +2919,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2529
2919
|
this.#activeCall = null;
|
|
2530
2920
|
this.emit("activeCallChanged" /* ActiveCallChanged */, { call: null });
|
|
2531
2921
|
this.#syncCallState();
|
|
2922
|
+
this.#syncLine();
|
|
2532
2923
|
}
|
|
2533
2924
|
});
|
|
2534
2925
|
if (this.#resolveContact) {
|
|
@@ -2558,7 +2949,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2558
2949
|
/**
|
|
2559
2950
|
* Reports the current on-a-call value to the backend. The watch de-duplicates (it only sends on change) and
|
|
2560
2951
|
* remembers the latest value to replay after a reconnect, so this can be called on every transition without
|
|
2561
|
-
* 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).
|
|
2562
2954
|
*/
|
|
2563
2955
|
#syncCallState() {
|
|
2564
2956
|
this.#presenceWatch?.reportCallState(this.#isOnCall());
|
|
@@ -2579,17 +2971,22 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2579
2971
|
* `presence-socket.ts`'s own `getIsLeader` is checked at start and before each reconnect attempt,
|
|
2580
2972
|
* not on a poll tick the way `startTransferWatch`'s is — see that module's own doc comment for why.
|
|
2581
2973
|
* This still passes `getIsLeader: () => this.#isLeader` for that narrower, defensive recheck; the
|
|
2582
|
-
* BROAD
|
|
2974
|
+
* BROAD started/leader gate — whether to be connected AT ALL — is owned entirely here, the same
|
|
2583
2975
|
* split `#onTransfer` already has with `startTransferWatch`.
|
|
2584
2976
|
*
|
|
2585
|
-
*
|
|
2586
|
-
*
|
|
2587
|
-
*
|
|
2588
|
-
*
|
|
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.
|
|
2589
2986
|
*/
|
|
2590
2987
|
#syncPresenceWatch() {
|
|
2591
2988
|
const eligible = Boolean(this.sources) && this.#isLeader;
|
|
2592
|
-
const shouldRun = eligible && this.#state
|
|
2989
|
+
const shouldRun = eligible && this.#presenceRunsIn(this.#state);
|
|
2593
2990
|
const blip = eligible && (this.#state === "offline" || this.#state === "reconnecting");
|
|
2594
2991
|
if (!blip) this.#cancelPresenceTeardown();
|
|
2595
2992
|
if (blip && this.#presenceWatch !== null) {
|
|
@@ -2598,13 +2995,22 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2598
2995
|
}
|
|
2599
2996
|
if (shouldRun === (this.#presenceWatch !== null)) return;
|
|
2600
2997
|
if (shouldRun) {
|
|
2998
|
+
this.#clientId ??= lineClientId();
|
|
2601
2999
|
this.#presenceWatch = startPresenceSocket({
|
|
2602
3000
|
source: this.sources.presence,
|
|
2603
3001
|
getIsLeader: () => this.#isLeader,
|
|
2604
3002
|
onAgentsChanged: (agents) => {
|
|
2605
3003
|
this.#presenceAgents = agents;
|
|
2606
3004
|
this.emit("presenceChanged" /* PresenceChanged */, { agents });
|
|
2607
|
-
}
|
|
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()
|
|
2608
3014
|
});
|
|
2609
3015
|
this.#syncCallState();
|
|
2610
3016
|
} else {
|
|
@@ -2619,19 +3025,24 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2619
3025
|
if (this.#presenceTeardownTimer !== null) return;
|
|
2620
3026
|
this.#presenceTeardownTimer = setTimeout(() => {
|
|
2621
3027
|
this.#presenceTeardownTimer = null;
|
|
2622
|
-
if (this.#
|
|
3028
|
+
if (this.#isLeader && this.#presenceRunsIn(this.#state)) return;
|
|
2623
3029
|
this.#stopPresence();
|
|
2624
3030
|
}, PRESENCE_TEARDOWN_GRACE_MS);
|
|
2625
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
|
+
}
|
|
2626
3036
|
#cancelPresenceTeardown() {
|
|
2627
3037
|
if (this.#presenceTeardownTimer === null) return;
|
|
2628
3038
|
clearTimeout(this.#presenceTeardownTimer);
|
|
2629
3039
|
this.#presenceTeardownTimer = null;
|
|
2630
3040
|
}
|
|
2631
3041
|
/**
|
|
2632
|
-
* Stops the presence watch and forgets its roster. A STATE-DRIVEN teardown (
|
|
2633
|
-
*
|
|
2634
|
-
* 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.
|
|
2635
3046
|
*/
|
|
2636
3047
|
#stopPresence(options) {
|
|
2637
3048
|
this.#cancelPresenceTeardown();
|
|
@@ -2640,6 +3051,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2640
3051
|
this.#presenceWatch = null;
|
|
2641
3052
|
this.#presenceAgents = [];
|
|
2642
3053
|
this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
|
|
3054
|
+
this.#forgetLine();
|
|
2643
3055
|
}
|
|
2644
3056
|
/**
|
|
2645
3057
|
* A DELIBERATE stop (`stop()` / `destroy()`): say any call is over, then say goodbye, so the backend drops
|
|
@@ -2649,6 +3061,195 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2649
3061
|
this.#presenceWatch?.reportCallState(false);
|
|
2650
3062
|
this.#stopPresence({ bye: true });
|
|
2651
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
|
+
}
|
|
2652
3253
|
/**
|
|
2653
3254
|
* Phase A (transfer outcome relay, `docs/specs/transfer-outcome-relay.md`) — `CallImpl.transfer()`'s
|
|
2654
3255
|
* internal seam calls this. No-ops entirely when no Coventia backend is configured (`sources` is
|
|
@@ -2697,6 +3298,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2697
3298
|
};
|
|
2698
3299
|
|
|
2699
3300
|
// src/core/default-provider.ts
|
|
3301
|
+
init_errors();
|
|
2700
3302
|
var LazyRelay = class {
|
|
2701
3303
|
#listeners = /* @__PURE__ */ new Set();
|
|
2702
3304
|
#realOff = /* @__PURE__ */ new Map();
|
|
@@ -2719,6 +3321,7 @@ var LazyRelay = class {
|
|
|
2719
3321
|
function defaultZadarmaEngine(config) {
|
|
2720
3322
|
let real = null;
|
|
2721
3323
|
let pending = null;
|
|
3324
|
+
let run = 0;
|
|
2722
3325
|
const incoming = new LazyRelay();
|
|
2723
3326
|
const stateChange = new LazyRelay();
|
|
2724
3327
|
const error = new LazyRelay();
|
|
@@ -2742,13 +3345,20 @@ function defaultZadarmaEngine(config) {
|
|
|
2742
3345
|
return real?.state ?? "stopped";
|
|
2743
3346
|
},
|
|
2744
3347
|
async start() {
|
|
2745
|
-
|
|
3348
|
+
const current = run;
|
|
3349
|
+
const engine = await load();
|
|
3350
|
+
if (current !== run) return;
|
|
3351
|
+
await engine.start();
|
|
2746
3352
|
},
|
|
2747
3353
|
async stop() {
|
|
3354
|
+
run += 1;
|
|
2748
3355
|
if (real) await real.stop();
|
|
2749
3356
|
},
|
|
2750
3357
|
async placeCall(destination, options) {
|
|
2751
|
-
|
|
3358
|
+
const current = run;
|
|
3359
|
+
const engine = await load();
|
|
3360
|
+
if (current !== run) throw createError("engine/not-started");
|
|
3361
|
+
return engine.placeCall(destination, options);
|
|
2752
3362
|
},
|
|
2753
3363
|
onIncoming: (listener) => incoming.subscribe(listener),
|
|
2754
3364
|
onStateChange: (listener) => stateChange.subscribe(listener),
|
|
@@ -2757,8 +3367,18 @@ function defaultZadarmaEngine(config) {
|
|
|
2757
3367
|
}
|
|
2758
3368
|
|
|
2759
3369
|
// src/core/create.ts
|
|
3370
|
+
init_api_base();
|
|
2760
3371
|
function createSoftphone(config) {
|
|
2761
|
-
|
|
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
|
+
};
|
|
2762
3382
|
}
|
|
2763
3383
|
function resolveEngine(config) {
|
|
2764
3384
|
if (config.provider !== void 0) {
|
|
@@ -2784,7 +3404,8 @@ function isCallEngine(value) {
|
|
|
2784
3404
|
|
|
2785
3405
|
// src/index.ts
|
|
2786
3406
|
init_errors();
|
|
2787
|
-
|
|
3407
|
+
init_outbound_numbers();
|
|
3408
|
+
var VERSION = "0.5.0";
|
|
2788
3409
|
|
|
2789
3410
|
exports.CALL_TRANSITIONS = CALL_TRANSITIONS;
|
|
2790
3411
|
exports.CallEvent = CallEvent;
|
|
@@ -2795,6 +3416,7 @@ exports.canTransition = canTransition;
|
|
|
2795
3416
|
exports.createSoftphone = createSoftphone;
|
|
2796
3417
|
exports.isBrowser = isBrowser;
|
|
2797
3418
|
exports.isFatal = isFatal;
|
|
3419
|
+
exports.isOutboundNumberRefused = isOutboundNumberRefused;
|
|
2798
3420
|
exports.isTerminal = isTerminal;
|
|
2799
3421
|
exports.isWebRtcSupported = isWebRtcSupported;
|
|
2800
3422
|
//# sourceMappingURL=index.cjs.map
|