@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.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { isBrowser } from './chunk-KAXEMJ7E.js';
|
|
2
2
|
export { isBrowser, isWebRtcSupported } from './chunk-KAXEMJ7E.js';
|
|
3
|
-
export { CallEvent, SoftphoneEvent } from './chunk-
|
|
3
|
+
export { CallEvent, SoftphoneEvent } from './chunk-N4PLEYFJ.js';
|
|
4
4
|
export { isOutboundNumberRefused } from './chunk-5E5OEXS2.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import { normalizeApiBase } from './chunk-M76SAXXM.js';
|
|
6
|
+
import { createError, isSoftphoneError, isFatal } from './chunk-FRXHEHQA.js';
|
|
7
|
+
export { ERROR_CATALOGUE, isFatal } from './chunk-FRXHEHQA.js';
|
|
7
8
|
import { cachedTokenSource } from './chunk-MHOJHG5E.js';
|
|
8
9
|
|
|
9
10
|
// src/core/emitter.ts
|
|
@@ -285,7 +286,7 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
|
|
|
285
286
|
}
|
|
286
287
|
let pending = null;
|
|
287
288
|
const load = () => {
|
|
288
|
-
pending ??= import('./sources-
|
|
289
|
+
pending ??= import('./sources-QL2LRZVH.js').then(
|
|
289
290
|
(m) => m.coventiaSources({ ...config, baseUrl, tokenSource })
|
|
290
291
|
);
|
|
291
292
|
return pending;
|
|
@@ -316,13 +317,15 @@ function isWebLocksSupported() {
|
|
|
316
317
|
return isBrowser() && typeof navigator.locks?.request === "function";
|
|
317
318
|
}
|
|
318
319
|
function webLocksTabLock(lockName = LOCK_NAME) {
|
|
319
|
-
|
|
320
|
+
let controller;
|
|
320
321
|
let releaseHold;
|
|
321
322
|
return {
|
|
322
323
|
acquireLeadership() {
|
|
323
324
|
if (!isWebLocksSupported()) return Promise.resolve();
|
|
325
|
+
const { signal } = controller = new AbortController();
|
|
324
326
|
return new Promise((resolveAcquired) => {
|
|
325
|
-
navigator.locks.request(lockName, { signal
|
|
327
|
+
navigator.locks.request(lockName, { signal }, () => {
|
|
328
|
+
if (signal.aborted) return Promise.resolve();
|
|
326
329
|
resolveAcquired();
|
|
327
330
|
return new Promise((resolveHold) => {
|
|
328
331
|
releaseHold = resolveHold;
|
|
@@ -337,7 +340,7 @@ function webLocksTabLock(lockName = LOCK_NAME) {
|
|
|
337
340
|
releaseHold = void 0;
|
|
338
341
|
resolve();
|
|
339
342
|
} else {
|
|
340
|
-
controller
|
|
343
|
+
controller?.abort();
|
|
341
344
|
}
|
|
342
345
|
}
|
|
343
346
|
};
|
|
@@ -422,13 +425,19 @@ function startTransferWatch(params) {
|
|
|
422
425
|
var RECONNECT_BASE_MS = 1e3;
|
|
423
426
|
var RECONNECT_MAX_MS = 3e4;
|
|
424
427
|
var NOOP_WATCH = { reportCallState: () => {
|
|
425
|
-
}, stop: () => {
|
|
428
|
+
}, claimLine: () => false, stop: () => {
|
|
426
429
|
} };
|
|
427
430
|
function startPresenceSocket(params) {
|
|
428
431
|
const {
|
|
429
432
|
source,
|
|
430
433
|
getIsLeader,
|
|
431
434
|
onAgentsChanged,
|
|
435
|
+
clientId,
|
|
436
|
+
onLine,
|
|
437
|
+
onClaimResult,
|
|
438
|
+
onLineUnsupported,
|
|
439
|
+
onConnectFailed,
|
|
440
|
+
onDisconnect,
|
|
432
441
|
setTimeout: scheduleTimeout = globalThis.setTimeout,
|
|
433
442
|
clearTimeout: cancelTimeout = globalThis.clearTimeout,
|
|
434
443
|
random = Math.random
|
|
@@ -439,6 +448,8 @@ function startPresenceSocket(params) {
|
|
|
439
448
|
let reconnectTimer = null;
|
|
440
449
|
let agents = /* @__PURE__ */ new Map();
|
|
441
450
|
let lastReportedOnCall = null;
|
|
451
|
+
let sentOnCall = null;
|
|
452
|
+
let line = null;
|
|
442
453
|
let connection = null;
|
|
443
454
|
let byeOnArrival = false;
|
|
444
455
|
const stop = (options) => {
|
|
@@ -455,6 +466,12 @@ function startPresenceSocket(params) {
|
|
|
455
466
|
connection?.close();
|
|
456
467
|
connection = null;
|
|
457
468
|
};
|
|
469
|
+
const send = (onCall) => {
|
|
470
|
+
if (!connection) return;
|
|
471
|
+
connection.reportCallState(onCall);
|
|
472
|
+
sentOnCall = onCall;
|
|
473
|
+
};
|
|
474
|
+
const mayReport = (onCall) => line !== "elsewhere" || !onCall && sentOnCall === true;
|
|
458
475
|
const scheduleReconnect = () => {
|
|
459
476
|
if (stopped) return;
|
|
460
477
|
attempt += 1;
|
|
@@ -483,10 +500,23 @@ function startPresenceSocket(params) {
|
|
|
483
500
|
if (connection === null) return;
|
|
484
501
|
connection = null;
|
|
485
502
|
scheduleReconnect();
|
|
503
|
+
onDisconnect?.();
|
|
504
|
+
},
|
|
505
|
+
onLine: (update) => {
|
|
506
|
+
if (stopped) return;
|
|
507
|
+
const was = line;
|
|
508
|
+
line = update.state;
|
|
509
|
+
if (was === "elsewhere" && line === "here" && lastReportedOnCall !== null) send(lastReportedOnCall);
|
|
510
|
+
onLine?.(update);
|
|
511
|
+
},
|
|
512
|
+
onClaimResult: (result) => {
|
|
513
|
+
if (!stopped) onClaimResult?.(result);
|
|
486
514
|
}
|
|
487
515
|
});
|
|
488
516
|
} catch {
|
|
489
|
-
if (
|
|
517
|
+
if (stopped) return;
|
|
518
|
+
scheduleReconnect();
|
|
519
|
+
onConnectFailed?.();
|
|
490
520
|
return;
|
|
491
521
|
}
|
|
492
522
|
if (stopped) {
|
|
@@ -496,22 +526,63 @@ function startPresenceSocket(params) {
|
|
|
496
526
|
}
|
|
497
527
|
connection = opened;
|
|
498
528
|
attempt = 0;
|
|
499
|
-
|
|
529
|
+
sentOnCall = null;
|
|
530
|
+
line = null;
|
|
531
|
+
if (clientId !== void 0 && opened.hello) opened.hello(clientId);
|
|
532
|
+
else onLineUnsupported?.();
|
|
533
|
+
if (lastReportedOnCall !== null) send(lastReportedOnCall);
|
|
500
534
|
};
|
|
501
535
|
void connect();
|
|
502
536
|
return {
|
|
503
537
|
reportCallState: (onCall) => {
|
|
504
538
|
if (onCall === lastReportedOnCall) return;
|
|
505
539
|
lastReportedOnCall = onCall;
|
|
506
|
-
|
|
540
|
+
if (mayReport(onCall)) send(onCall);
|
|
541
|
+
},
|
|
542
|
+
claimLine: () => {
|
|
543
|
+
if (!connection?.claimLine) return false;
|
|
544
|
+
connection.claimLine();
|
|
545
|
+
return true;
|
|
507
546
|
},
|
|
508
547
|
stop
|
|
509
548
|
};
|
|
510
549
|
}
|
|
511
550
|
|
|
551
|
+
// src/core/line.ts
|
|
552
|
+
var CLIENT_ID_KEY = "coventia-softphone:client-id";
|
|
553
|
+
var CLIENT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
554
|
+
function lineClientId() {
|
|
555
|
+
const stored = readStoredId();
|
|
556
|
+
if (stored) return stored;
|
|
557
|
+
const id = freshClientId();
|
|
558
|
+
try {
|
|
559
|
+
if (isBrowser()) globalThis.sessionStorage?.setItem(CLIENT_ID_KEY, id);
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
return id;
|
|
563
|
+
}
|
|
564
|
+
function readStoredId() {
|
|
565
|
+
if (!isBrowser()) return null;
|
|
566
|
+
try {
|
|
567
|
+
const value = globalThis.sessionStorage?.getItem(CLIENT_ID_KEY);
|
|
568
|
+
return value && CLIENT_ID_PATTERN.test(value) ? value : null;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function freshClientId() {
|
|
574
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
575
|
+
if (uuid) return uuid;
|
|
576
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
512
579
|
// src/core/softphone-impl.ts
|
|
513
580
|
var liveInstance = false;
|
|
514
581
|
var PRESENCE_TEARDOWN_GRACE_MS = 1e4;
|
|
582
|
+
var LINE_ANSWER_TIMEOUT_MS = 4e3;
|
|
583
|
+
var CALL_DECISION_TIMEOUT_MS = LINE_ANSWER_TIMEOUT_MS + 2e3;
|
|
584
|
+
var LINE_CLAIM_TIMEOUT_MS = 1e4;
|
|
585
|
+
var LINE_LAPSE_MS = 1e4;
|
|
515
586
|
var BrowserAudioDevices = class {
|
|
516
587
|
inputs = [];
|
|
517
588
|
outputs = [];
|
|
@@ -619,6 +690,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
619
690
|
#activeCall = null;
|
|
620
691
|
#destroyed = false;
|
|
621
692
|
#startedHere = false;
|
|
693
|
+
/** Bumped by every `stop()`, so a `start()` still waiting to lead can tell it was stopped. */
|
|
694
|
+
#run = 0;
|
|
622
695
|
#isLeader = false;
|
|
623
696
|
#engine;
|
|
624
697
|
#tabLock;
|
|
@@ -643,6 +716,52 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
643
716
|
/** A pending state-driven teardown of the watch — see `PRESENCE_TEARDOWN_GRACE_MS`. */
|
|
644
717
|
#presenceTeardownTimer = null;
|
|
645
718
|
#presenceAgents = [];
|
|
719
|
+
// ── The line owner (`docs/specs/line-owner.md` §5) ──────────────────────────────────────────────
|
|
720
|
+
// Heard over the presence watch: the server names ONE softphone per extension that listens, and this one
|
|
721
|
+
// runs its engine only while that is it (or nobody can say). `#engineOn` and `#standby` are the two halves
|
|
722
|
+
// of that decision, kept apart because neither is true while `start()` is still waiting to make it.
|
|
723
|
+
#line = "unknown";
|
|
724
|
+
#lineWhere = null;
|
|
725
|
+
/** How the last attempt to bring the line here went (`lineClaim`), whoever made it. */
|
|
726
|
+
#lineClaim = "idle";
|
|
727
|
+
/** This tab's id in `hello` (`lineClientId`), read once — one id for every reconnect of this softphone. */
|
|
728
|
+
#clientId = null;
|
|
729
|
+
/** What the line wants: the engine running. Set the moment the line decides; the engine follows (`#queueEngine`). */
|
|
730
|
+
#engineOn = false;
|
|
731
|
+
/**
|
|
732
|
+
* What the engine was last TOLD by the queue — started or stopped. Apart from `#engineOn` because the line
|
|
733
|
+
* can flip twice while a start is still running: the stop and start queued meanwhile then find nothing
|
|
734
|
+
* to do, instead of stopping the engine (and the softphone with it) for a moment.
|
|
735
|
+
*/
|
|
736
|
+
#engineRunning = false;
|
|
737
|
+
/** Waiting in `standby`: the engine is stopped ON PURPOSE, so its own `stopped` is not the softphone's. */
|
|
738
|
+
#standby = false;
|
|
739
|
+
/**
|
|
740
|
+
* Every engine start and stop the line asks for, one after another: a promotion can follow a demotion
|
|
741
|
+
* within a moment, and an engine started while its own stop is still running can end up neither.
|
|
742
|
+
*/
|
|
743
|
+
#engineSteps = Promise.resolve();
|
|
744
|
+
/** Dials in flight (`call()` awaiting `placeCall`): a call about to exist holds the line like one that does. */
|
|
745
|
+
#dialing = 0;
|
|
746
|
+
/**
|
|
747
|
+
* Open from `start()`'s first line until it has decided whether the engine runs — or given up (stopped,
|
|
748
|
+
* destroyed, failed). `call()` waits on it, so it never dials ahead of that decision: not on a phone
|
|
749
|
+
* stopped meanwhile, and not without the line.
|
|
750
|
+
*/
|
|
751
|
+
#startDecision = null;
|
|
752
|
+
/**
|
|
753
|
+
* Who decides whether the engine runs. `off`: nobody — not started, a follower, or `stop()` has begun
|
|
754
|
+
* (whatever its teardown sets off, like a call ending, must not start anything). `deciding`: `start()`
|
|
755
|
+
* has leadership and owes the FIRST decision itself, since it must await the engine start — a line
|
|
756
|
+
* message meanwhile only records what it says. `on`: the line decides (`#applyLine`).
|
|
757
|
+
*/
|
|
758
|
+
#lineMode = "off";
|
|
759
|
+
/** Ends `start()`'s wait for the server's first word on the line early (`#waitForFirstLine`). */
|
|
760
|
+
#releaseFirstLine = null;
|
|
761
|
+
/** The `takeLine()` in flight — one at a time; a second caller shares it. */
|
|
762
|
+
#claim = null;
|
|
763
|
+
/** Pending: what the server said about the line stops counting (`LINE_LAPSE_MS`). */
|
|
764
|
+
#lineLapseTimer = null;
|
|
646
765
|
/**
|
|
647
766
|
* `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
|
|
648
767
|
* internal seam, not something an integrator tunes). Tests construct this class directly with a
|
|
@@ -660,7 +779,9 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
660
779
|
this.sources = config.coventia || config.apiBase ? coventiaBackend(config.coventia ?? {}, cachedTokenSource(config.tokenSource), config.apiBase) : null;
|
|
661
780
|
this.#resolveContact = config.resolveContact ?? this.sources?.resolveContact;
|
|
662
781
|
this.#engine.onIncoming((engineCall) => this.#onIncoming(engineCall));
|
|
663
|
-
this.#engine.onStateChange((state) =>
|
|
782
|
+
this.#engine.onStateChange((state) => {
|
|
783
|
+
if (!this.#standby) this.#setState(state);
|
|
784
|
+
});
|
|
664
785
|
this.#engine.onError?.((error) => this.#onError(error));
|
|
665
786
|
}
|
|
666
787
|
get state() {
|
|
@@ -678,6 +799,15 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
678
799
|
get presence() {
|
|
679
800
|
return this.#presenceAgents;
|
|
680
801
|
}
|
|
802
|
+
get line() {
|
|
803
|
+
return this.#line;
|
|
804
|
+
}
|
|
805
|
+
get lineWhere() {
|
|
806
|
+
return this.#lineWhere;
|
|
807
|
+
}
|
|
808
|
+
get lineClaim() {
|
|
809
|
+
return this.#lineClaim;
|
|
810
|
+
}
|
|
681
811
|
get canPlayAudio() {
|
|
682
812
|
return true;
|
|
683
813
|
}
|
|
@@ -693,15 +823,33 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
693
823
|
}
|
|
694
824
|
liveInstance = true;
|
|
695
825
|
this.#startedHere = true;
|
|
696
|
-
this.#
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
826
|
+
const run = this.#run;
|
|
827
|
+
const decision = this.#openStartDecision();
|
|
828
|
+
try {
|
|
829
|
+
this.#setState("starting");
|
|
830
|
+
await this.#tabLock.acquireLeadership();
|
|
831
|
+
if (this.#destroyed || run !== this.#run) return;
|
|
832
|
+
this.#lineMode = "deciding";
|
|
833
|
+
this.#setLeader(true);
|
|
834
|
+
if (this.#presenceWatch && this.#line === "unknown") await this.#waitForFirstLine();
|
|
835
|
+
if (this.#destroyed || run !== this.#run) return;
|
|
836
|
+
this.#lineMode = "on";
|
|
837
|
+
const engineStart = this.#applyLine();
|
|
838
|
+
decision.end();
|
|
839
|
+
await engineStart;
|
|
840
|
+
} finally {
|
|
841
|
+
decision.end();
|
|
842
|
+
}
|
|
701
843
|
}
|
|
702
844
|
async stop() {
|
|
703
845
|
if (!this.#startedHere) return;
|
|
846
|
+
this.#run += 1;
|
|
847
|
+
this.#lineMode = "off";
|
|
704
848
|
this.#sayGoodbye();
|
|
849
|
+
this.#startDecision?.end();
|
|
850
|
+
this.#standby = false;
|
|
851
|
+
this.#engineOn = false;
|
|
852
|
+
this.#engineRunning = false;
|
|
705
853
|
this.#tabLock.release();
|
|
706
854
|
if (this.#isLeader) {
|
|
707
855
|
await this.#engine.stop();
|
|
@@ -723,16 +871,50 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
723
871
|
this.#setState("destroyed");
|
|
724
872
|
this.clear();
|
|
725
873
|
}
|
|
874
|
+
async takeLine() {
|
|
875
|
+
this.#assertUsable();
|
|
876
|
+
if (this.#line === "here") return;
|
|
877
|
+
if (this.#claim) return this.#claim.promise;
|
|
878
|
+
const watch = this.#presenceWatch;
|
|
879
|
+
if (!watch) {
|
|
880
|
+
this.#setLineClaim("failed");
|
|
881
|
+
throw createError("line/unavailable");
|
|
882
|
+
}
|
|
883
|
+
const claim = this.#openClaim();
|
|
884
|
+
if (!watch.claimLine()) this.#settleClaim(createError("line/unavailable"));
|
|
885
|
+
return claim;
|
|
886
|
+
}
|
|
726
887
|
async call(destination, options) {
|
|
727
888
|
this.#assertUsable();
|
|
889
|
+
const run = this.#run;
|
|
890
|
+
if (this.#startDecision) {
|
|
891
|
+
let timer;
|
|
892
|
+
const decided = await Promise.race([
|
|
893
|
+
this.#startDecision.promise.then(() => true),
|
|
894
|
+
new Promise((resolve) => {
|
|
895
|
+
timer = setTimeout(() => resolve(false), CALL_DECISION_TIMEOUT_MS);
|
|
896
|
+
})
|
|
897
|
+
]);
|
|
898
|
+
clearTimeout(timer);
|
|
899
|
+
if (!decided) throw createError("engine/not-started");
|
|
900
|
+
}
|
|
901
|
+
if (run === this.#run && this.#state === "standby") await this.takeLine();
|
|
902
|
+
this.#assertUsable();
|
|
903
|
+
if (run !== this.#run) throw createError("engine/not-started");
|
|
728
904
|
if (this.#state !== "online" && this.#state !== "offline" && this.#state !== "starting") {
|
|
729
905
|
throw new Error(`Cannot place a call while softphone is '${this.#state}'.`);
|
|
730
906
|
}
|
|
731
|
-
if (this.#activeCall) {
|
|
907
|
+
if (this.#activeCall || this.#dialing > 0) {
|
|
732
908
|
throw new Error("A call is already in progress. Hang up before placing another.");
|
|
733
909
|
}
|
|
734
|
-
|
|
735
|
-
|
|
910
|
+
this.#dialing += 1;
|
|
911
|
+
try {
|
|
912
|
+
const engineCall = await this.#engine.placeCall(destination, options);
|
|
913
|
+
return this.#adopt(engineCall);
|
|
914
|
+
} finally {
|
|
915
|
+
this.#dialing -= 1;
|
|
916
|
+
this.#syncLine();
|
|
917
|
+
}
|
|
736
918
|
}
|
|
737
919
|
// ── internals ───────────────────────────────────────────────────────────────
|
|
738
920
|
#onIncoming(engineCall) {
|
|
@@ -761,6 +943,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
761
943
|
this.#activeCall = null;
|
|
762
944
|
this.emit("activeCallChanged" /* ActiveCallChanged */, { call: null });
|
|
763
945
|
this.#syncCallState();
|
|
946
|
+
this.#syncLine();
|
|
764
947
|
}
|
|
765
948
|
});
|
|
766
949
|
if (this.#resolveContact) {
|
|
@@ -790,7 +973,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
790
973
|
/**
|
|
791
974
|
* Reports the current on-a-call value to the backend. The watch de-duplicates (it only sends on change) and
|
|
792
975
|
* remembers the latest value to replay after a reconnect, so this can be called on every transition without
|
|
793
|
-
* ceremony
|
|
976
|
+
* ceremony — and holds it back while the line is elsewhere, since only the owner's reports count (line-owner
|
|
977
|
+
* R8). A no-op while there is no presence watch (stopped, not the leader, no backend configured).
|
|
794
978
|
*/
|
|
795
979
|
#syncCallState() {
|
|
796
980
|
this.#presenceWatch?.reportCallState(this.#isOnCall());
|
|
@@ -811,17 +995,22 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
811
995
|
* `presence-socket.ts`'s own `getIsLeader` is checked at start and before each reconnect attempt,
|
|
812
996
|
* not on a poll tick the way `startTransferWatch`'s is — see that module's own doc comment for why.
|
|
813
997
|
* This still passes `getIsLeader: () => this.#isLeader` for that narrower, defensive recheck; the
|
|
814
|
-
* BROAD
|
|
998
|
+
* BROAD started/leader gate — whether to be connected AT ALL — is owned entirely here, the same
|
|
815
999
|
* split `#onTransfer` already has with `startTransferWatch`.
|
|
816
1000
|
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
1001
|
+
* The watch runs for a leader that is `starting`, `online` or in `standby` — not only `online`, as it did
|
|
1002
|
+
* before the line owner (`docs/specs/line-owner.md` §5.4): while starting it is how this softphone learns
|
|
1003
|
+
* whether to listen at all, and in standby how it learns the line came back.
|
|
1004
|
+
*
|
|
1005
|
+
* A leader that is merely `offline` or `reconnecting` keeps its watch for `PRESENCE_TEARDOWN_GRACE_MS` — a
|
|
1006
|
+
* blip shorter than that never touches the presence socket at all; a longer outage closes it, which lets
|
|
1007
|
+
* the server hand the line to a softphone that can still take calls. Anything else (stopped, destroyed, or
|
|
1008
|
+
* no longer the leader) tears it down at once: those are not blips, and a follower must never hold a
|
|
1009
|
+
* connection.
|
|
821
1010
|
*/
|
|
822
1011
|
#syncPresenceWatch() {
|
|
823
1012
|
const eligible = Boolean(this.sources) && this.#isLeader;
|
|
824
|
-
const shouldRun = eligible && this.#state
|
|
1013
|
+
const shouldRun = eligible && this.#presenceRunsIn(this.#state);
|
|
825
1014
|
const blip = eligible && (this.#state === "offline" || this.#state === "reconnecting");
|
|
826
1015
|
if (!blip) this.#cancelPresenceTeardown();
|
|
827
1016
|
if (blip && this.#presenceWatch !== null) {
|
|
@@ -830,13 +1019,22 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
830
1019
|
}
|
|
831
1020
|
if (shouldRun === (this.#presenceWatch !== null)) return;
|
|
832
1021
|
if (shouldRun) {
|
|
1022
|
+
this.#clientId ??= lineClientId();
|
|
833
1023
|
this.#presenceWatch = startPresenceSocket({
|
|
834
1024
|
source: this.sources.presence,
|
|
835
1025
|
getIsLeader: () => this.#isLeader,
|
|
836
1026
|
onAgentsChanged: (agents) => {
|
|
837
1027
|
this.#presenceAgents = agents;
|
|
838
1028
|
this.emit("presenceChanged" /* PresenceChanged */, { agents });
|
|
839
|
-
}
|
|
1029
|
+
},
|
|
1030
|
+
clientId: this.#clientId,
|
|
1031
|
+
onLine: (update) => this.#onLine(update),
|
|
1032
|
+
onClaimResult: (result) => this.#onClaimResult(result),
|
|
1033
|
+
// A source that cannot take part will never answer, and one that cannot connect cannot answer yet:
|
|
1034
|
+
// `start()` need not wait out its 4 s for either (R7). A later answer still applies.
|
|
1035
|
+
onLineUnsupported: () => this.#releaseFirstLine?.(),
|
|
1036
|
+
onConnectFailed: () => this.#releaseFirstLine?.(),
|
|
1037
|
+
onDisconnect: () => this.#onPresenceDisconnect()
|
|
840
1038
|
});
|
|
841
1039
|
this.#syncCallState();
|
|
842
1040
|
} else {
|
|
@@ -851,19 +1049,24 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
851
1049
|
if (this.#presenceTeardownTimer !== null) return;
|
|
852
1050
|
this.#presenceTeardownTimer = setTimeout(() => {
|
|
853
1051
|
this.#presenceTeardownTimer = null;
|
|
854
|
-
if (this.#
|
|
1052
|
+
if (this.#isLeader && this.#presenceRunsIn(this.#state)) return;
|
|
855
1053
|
this.#stopPresence();
|
|
856
1054
|
}, PRESENCE_TEARDOWN_GRACE_MS);
|
|
857
1055
|
}
|
|
1056
|
+
/** The states in which a leader keeps its presence watch with no clock running (`#syncPresenceWatch`). */
|
|
1057
|
+
#presenceRunsIn(state) {
|
|
1058
|
+
return state === "online" || state === "starting" || state === "standby";
|
|
1059
|
+
}
|
|
858
1060
|
#cancelPresenceTeardown() {
|
|
859
1061
|
if (this.#presenceTeardownTimer === null) return;
|
|
860
1062
|
clearTimeout(this.#presenceTeardownTimer);
|
|
861
1063
|
this.#presenceTeardownTimer = null;
|
|
862
1064
|
}
|
|
863
1065
|
/**
|
|
864
|
-
* Stops the presence watch and forgets its roster. A STATE-DRIVEN teardown (
|
|
865
|
-
*
|
|
866
|
-
* Any pending grace-period teardown is cancelled — there is nothing
|
|
1066
|
+
* Stops the presence watch and forgets its roster, and the line. A STATE-DRIVEN teardown (an outage
|
|
1067
|
+
* outlasted the grace, the engine stopped, or leadership was lost) passes nothing and says no goodbye;
|
|
1068
|
+
* only a deliberate stop passes `bye`. Any pending grace-period teardown is cancelled — there is nothing
|
|
1069
|
+
* left for it to do.
|
|
867
1070
|
*/
|
|
868
1071
|
#stopPresence(options) {
|
|
869
1072
|
this.#cancelPresenceTeardown();
|
|
@@ -872,6 +1075,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
872
1075
|
this.#presenceWatch = null;
|
|
873
1076
|
this.#presenceAgents = [];
|
|
874
1077
|
this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
|
|
1078
|
+
this.#forgetLine();
|
|
875
1079
|
}
|
|
876
1080
|
/**
|
|
877
1081
|
* A DELIBERATE stop (`stop()` / `destroy()`): say any call is over, then say goodbye, so the backend drops
|
|
@@ -881,6 +1085,195 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
881
1085
|
this.#presenceWatch?.reportCallState(false);
|
|
882
1086
|
this.#stopPresence({ bye: true });
|
|
883
1087
|
}
|
|
1088
|
+
// ── the line owner (`docs/specs/line-owner.md` §5) ──────────────────────────────────────────────
|
|
1089
|
+
/**
|
|
1090
|
+
* THE decision: from where the line is and whether a call is in progress, whether the engine runs, and
|
|
1091
|
+
* the state that says so. Synchronous, so the state moves the instant the line does — the waiting screen
|
|
1092
|
+
* goes the moment the line comes here, and a `call()` that took the line dials from `starting` without
|
|
1093
|
+
* waiting out the engine start. Only the engine start or stop is queued (`#queueEngine`). Returns that
|
|
1094
|
+
* queued step, or `null` when nothing had to change.
|
|
1095
|
+
*/
|
|
1096
|
+
#applyLine() {
|
|
1097
|
+
if (this.#lineMode !== "on") return null;
|
|
1098
|
+
const run = this.#run;
|
|
1099
|
+
if (this.#line === "elsewhere") {
|
|
1100
|
+
if (this.#standby || this.#activeCall || this.#dialing > 0) return null;
|
|
1101
|
+
this.#standby = true;
|
|
1102
|
+
this.#setState("standby");
|
|
1103
|
+
if (!this.#engineOn) return null;
|
|
1104
|
+
this.#engineOn = false;
|
|
1105
|
+
return this.#queueEngine(run);
|
|
1106
|
+
}
|
|
1107
|
+
if (this.#engineOn) return null;
|
|
1108
|
+
this.#engineOn = true;
|
|
1109
|
+
if (this.#standby) {
|
|
1110
|
+
this.#standby = false;
|
|
1111
|
+
this.#setState("starting");
|
|
1112
|
+
}
|
|
1113
|
+
return this.#queueEngine(run);
|
|
1114
|
+
}
|
|
1115
|
+
/** `#applyLine` from an event — nobody to hand a failure to, and `#engineStartFailed` has reported it. */
|
|
1116
|
+
#syncLine() {
|
|
1117
|
+
void this.#applyLine()?.catch(() => {
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Brings the engine to what the line wants (`#engineOn`) once every step already queued has run — and does
|
|
1122
|
+
* nothing when it is already there: the line can flip twice while a start is still running, and the stop
|
|
1123
|
+
* and start queued meanwhile must not take the engine (and the softphone's state) down and up for nothing.
|
|
1124
|
+
* Nothing at all once `stop()` has run since `run` was read: a stopped softphone's engine is `stop()`'s
|
|
1125
|
+
* alone. Rejects with a failed start's cause; the queue itself never rejects, so one failure blocks nothing.
|
|
1126
|
+
*/
|
|
1127
|
+
#queueEngine(run) {
|
|
1128
|
+
const step = this.#engineSteps.then(async () => {
|
|
1129
|
+
if (run !== this.#run || this.#engineOn === this.#engineRunning) return;
|
|
1130
|
+
this.#engineRunning = this.#engineOn;
|
|
1131
|
+
if (this.#engineRunning) await this.#startEngine(run);
|
|
1132
|
+
else await this.#engine.stop();
|
|
1133
|
+
});
|
|
1134
|
+
this.#engineSteps = step.catch(() => {
|
|
1135
|
+
});
|
|
1136
|
+
return step;
|
|
1137
|
+
}
|
|
1138
|
+
async #startEngine(run) {
|
|
1139
|
+
const reported = this.#lastError;
|
|
1140
|
+
try {
|
|
1141
|
+
await this.#engine.start();
|
|
1142
|
+
} catch (cause) {
|
|
1143
|
+
if (run === this.#run) this.#engineStartFailed(cause, reported);
|
|
1144
|
+
throw cause;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* An engine start the line asked for failed. Kept as it was, this softphone would stay the owner on the
|
|
1149
|
+
* server — `starting`, its presence socket renewing the line — with no push socket anywhere: nobody would
|
|
1150
|
+
* listen. So it gives the line up the ordinary way, by closing its presence socket without a goodbye (the
|
|
1151
|
+
* phone was not switched off): the server hands the line on after its grace (R4). The failure is reported
|
|
1152
|
+
* like any engine failure — `SoftphoneEvent.Error` and `lastError`, which is the widget's error gate and
|
|
1153
|
+
* its "Reintentar" (`stop()` then `start()`, which asks for the line again). Nothing retries on its own.
|
|
1154
|
+
*/
|
|
1155
|
+
#engineStartFailed(cause, reportedBefore) {
|
|
1156
|
+
this.#engineOn = false;
|
|
1157
|
+
this.#engineRunning = false;
|
|
1158
|
+
this.#lineMode = "off";
|
|
1159
|
+
if (this.#lastError === reportedBefore) {
|
|
1160
|
+
this.#onError(isSoftphoneError(cause) && isFatal(cause) ? cause : createError("net/transport-failed", cause));
|
|
1161
|
+
}
|
|
1162
|
+
this.#stopPresence();
|
|
1163
|
+
this.#setState("offline");
|
|
1164
|
+
}
|
|
1165
|
+
/** `start()`'s decision (`#startDecision`): ended once, by whichever comes first — the decision or its end. */
|
|
1166
|
+
#openStartDecision() {
|
|
1167
|
+
let resolve;
|
|
1168
|
+
const promise = new Promise((r) => {
|
|
1169
|
+
resolve = r;
|
|
1170
|
+
});
|
|
1171
|
+
const decision = {
|
|
1172
|
+
promise,
|
|
1173
|
+
end: () => {
|
|
1174
|
+
resolve();
|
|
1175
|
+
if (this.#startDecision === decision) this.#startDecision = null;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
this.#startDecision = decision;
|
|
1179
|
+
return decision;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* `start()`'s wait for the server's first word on the line — released by that word, by a source that
|
|
1183
|
+
* will never say it, by the watch going away (`stop()` among them), or after `LINE_ANSWER_TIMEOUT_MS`.
|
|
1184
|
+
*/
|
|
1185
|
+
#waitForFirstLine() {
|
|
1186
|
+
return new Promise((resolve) => {
|
|
1187
|
+
const release = () => {
|
|
1188
|
+
clearTimeout(timer);
|
|
1189
|
+
if (this.#releaseFirstLine === release) this.#releaseFirstLine = null;
|
|
1190
|
+
resolve();
|
|
1191
|
+
};
|
|
1192
|
+
const timer = setTimeout(release, LINE_ANSWER_TIMEOUT_MS);
|
|
1193
|
+
this.#releaseFirstLine = release;
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
#onLine(update) {
|
|
1197
|
+
this.#cancelLineLapse();
|
|
1198
|
+
this.#setLine(update.state, update.state === "here" ? null : update.where);
|
|
1199
|
+
if (this.#releaseFirstLine) this.#releaseFirstLine();
|
|
1200
|
+
else this.#syncLine();
|
|
1201
|
+
if (update.state === "here") this.#settleClaim();
|
|
1202
|
+
}
|
|
1203
|
+
#onClaimResult(result) {
|
|
1204
|
+
if (result.ok) {
|
|
1205
|
+
if (this.#line === "here") this.#settleClaim();
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
this.#settleClaim(createError(result.reason === "on_call" ? "line/claim-refused" : "line/unavailable"));
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* The presence socket dropped; the watch is reconnecting. A claim sent on it will never be answered, so
|
|
1212
|
+
* it fails now rather than on its timeout. What the server said about the line holds for `LINE_LAPSE_MS`
|
|
1213
|
+
* — a reconnect is normally answered well inside that — and then stops counting: `unknown`, which a
|
|
1214
|
+
* waiting softphone answers by listening again.
|
|
1215
|
+
*/
|
|
1216
|
+
#onPresenceDisconnect() {
|
|
1217
|
+
this.#settleClaim(createError("line/unavailable"));
|
|
1218
|
+
if (this.#line === "unknown" || this.#lineLapseTimer !== null) return;
|
|
1219
|
+
this.#lineLapseTimer = setTimeout(() => {
|
|
1220
|
+
this.#lineLapseTimer = null;
|
|
1221
|
+
this.#setLine("unknown", null);
|
|
1222
|
+
this.#syncLine();
|
|
1223
|
+
}, LINE_LAPSE_MS);
|
|
1224
|
+
}
|
|
1225
|
+
#cancelLineLapse() {
|
|
1226
|
+
if (this.#lineLapseTimer === null) return;
|
|
1227
|
+
clearTimeout(this.#lineLapseTimer);
|
|
1228
|
+
this.#lineLapseTimer = null;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* The presence watch is gone, so nothing the server said about the line holds any more: it reads
|
|
1232
|
+
* `unknown`, and every wait on it ends. Not followed by `#applyLine` — this runs inside a state change
|
|
1233
|
+
* (`#syncPresenceWatch`) or a stop, and a softphone whose watch is gone is not in `standby` anyway: that
|
|
1234
|
+
* state keeps the watch.
|
|
1235
|
+
*/
|
|
1236
|
+
#forgetLine() {
|
|
1237
|
+
this.#cancelLineLapse();
|
|
1238
|
+
this.#releaseFirstLine?.();
|
|
1239
|
+
this.#settleClaim(createError("line/unavailable"));
|
|
1240
|
+
this.#setLine("unknown", null);
|
|
1241
|
+
this.#setLineClaim("idle");
|
|
1242
|
+
}
|
|
1243
|
+
#setLine(line, where) {
|
|
1244
|
+
if (line === "here") this.#setLineClaim("idle");
|
|
1245
|
+
if (line === this.#line && where === this.#lineWhere) return;
|
|
1246
|
+
this.#line = line;
|
|
1247
|
+
this.#lineWhere = where;
|
|
1248
|
+
this.emit("lineChanged" /* LineChanged */, { line, where });
|
|
1249
|
+
}
|
|
1250
|
+
#setLineClaim(claim) {
|
|
1251
|
+
if (claim === this.#lineClaim) return;
|
|
1252
|
+
this.#lineClaim = claim;
|
|
1253
|
+
this.emit("lineClaimChanged" /* LineClaimChanged */, { claim });
|
|
1254
|
+
}
|
|
1255
|
+
#openClaim() {
|
|
1256
|
+
let settle;
|
|
1257
|
+
const promise = new Promise((resolve, reject) => {
|
|
1258
|
+
const timer = setTimeout(() => this.#settleClaim(createError("line/unavailable")), LINE_CLAIM_TIMEOUT_MS);
|
|
1259
|
+
settle = (error) => {
|
|
1260
|
+
clearTimeout(timer);
|
|
1261
|
+
if (error) reject(error);
|
|
1262
|
+
else resolve();
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
this.#claim = { promise, settle };
|
|
1266
|
+
this.#setLineClaim("claiming");
|
|
1267
|
+
return promise;
|
|
1268
|
+
}
|
|
1269
|
+
/** Ends the claim in flight, if any — resolved without `error`, rejected with it. */
|
|
1270
|
+
#settleClaim(error) {
|
|
1271
|
+
const claim = this.#claim;
|
|
1272
|
+
if (!claim) return;
|
|
1273
|
+
this.#claim = null;
|
|
1274
|
+
this.#setLineClaim(!error ? "idle" : error.code === "line/claim-refused" ? "refused" : "failed");
|
|
1275
|
+
claim.settle(error);
|
|
1276
|
+
}
|
|
884
1277
|
/**
|
|
885
1278
|
* Phase A (transfer outcome relay, `docs/specs/transfer-outcome-relay.md`) — `CallImpl.transfer()`'s
|
|
886
1279
|
* internal seam calls this. No-ops entirely when no Coventia backend is configured (`sources` is
|
|
@@ -951,11 +1344,12 @@ var LazyRelay = class {
|
|
|
951
1344
|
function defaultZadarmaEngine(config) {
|
|
952
1345
|
let real = null;
|
|
953
1346
|
let pending = null;
|
|
1347
|
+
let run = 0;
|
|
954
1348
|
const incoming = new LazyRelay();
|
|
955
1349
|
const stateChange = new LazyRelay();
|
|
956
1350
|
const error = new LazyRelay();
|
|
957
1351
|
function load() {
|
|
958
|
-
pending ??= import('./zadarma-engine-
|
|
1352
|
+
pending ??= import('./zadarma-engine-TIUE7PZ7.js').then(({ createZadarmaEngine }) => {
|
|
959
1353
|
const engine = createZadarmaEngine(config);
|
|
960
1354
|
incoming.attach({ on: (l) => engine.onIncoming(l) });
|
|
961
1355
|
stateChange.attach({ on: (l) => engine.onStateChange(l) });
|
|
@@ -974,13 +1368,20 @@ function defaultZadarmaEngine(config) {
|
|
|
974
1368
|
return real?.state ?? "stopped";
|
|
975
1369
|
},
|
|
976
1370
|
async start() {
|
|
977
|
-
|
|
1371
|
+
const current = run;
|
|
1372
|
+
const engine = await load();
|
|
1373
|
+
if (current !== run) return;
|
|
1374
|
+
await engine.start();
|
|
978
1375
|
},
|
|
979
1376
|
async stop() {
|
|
1377
|
+
run += 1;
|
|
980
1378
|
if (real) await real.stop();
|
|
981
1379
|
},
|
|
982
1380
|
async placeCall(destination, options) {
|
|
983
|
-
|
|
1381
|
+
const current = run;
|
|
1382
|
+
const engine = await load();
|
|
1383
|
+
if (current !== run) throw createError("engine/not-started");
|
|
1384
|
+
return engine.placeCall(destination, options);
|
|
984
1385
|
},
|
|
985
1386
|
onIncoming: (listener) => incoming.subscribe(listener),
|
|
986
1387
|
onStateChange: (listener) => stateChange.subscribe(listener),
|
|
@@ -990,7 +1391,16 @@ function defaultZadarmaEngine(config) {
|
|
|
990
1391
|
|
|
991
1392
|
// src/core/create.ts
|
|
992
1393
|
function createSoftphone(config) {
|
|
993
|
-
|
|
1394
|
+
const resolved = withNormalizedBases(config);
|
|
1395
|
+
return new SoftphoneImpl(resolveEngine(resolved), resolved);
|
|
1396
|
+
}
|
|
1397
|
+
function withNormalizedBases(config) {
|
|
1398
|
+
const { apiBase, coventia } = config;
|
|
1399
|
+
return {
|
|
1400
|
+
...config,
|
|
1401
|
+
...apiBase ? { apiBase: normalizeApiBase(apiBase) } : {},
|
|
1402
|
+
...coventia?.baseUrl ? { coventia: { ...coventia, baseUrl: normalizeApiBase(coventia.baseUrl) } } : {}
|
|
1403
|
+
};
|
|
994
1404
|
}
|
|
995
1405
|
function resolveEngine(config) {
|
|
996
1406
|
if (config.provider !== void 0) {
|
|
@@ -1015,7 +1425,7 @@ function isCallEngine(value) {
|
|
|
1015
1425
|
}
|
|
1016
1426
|
|
|
1017
1427
|
// src/index.ts
|
|
1018
|
-
var VERSION = "0.
|
|
1428
|
+
var VERSION = "0.5.0";
|
|
1019
1429
|
|
|
1020
1430
|
export { CALL_TRANSITIONS, INITIAL_STATES, VERSION, canTransition, createSoftphone, isTerminal };
|
|
1021
1431
|
//# sourceMappingURL=index.js.map
|