@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.
Files changed (57) hide show
  1. package/dist/{call-C6YpT6uR.d.cts → call-FqcvDg1t.d.cts} +105 -12
  2. package/dist/{call-C6YpT6uR.d.ts → call-FqcvDg1t.d.ts} +105 -12
  3. package/dist/{chunk-EYEDPD7Y.js → chunk-6RR6G35E.js} +92 -17
  4. package/dist/chunk-6RR6G35E.js.map +1 -0
  5. package/dist/{chunk-2ZSOGGPO.js → chunk-FRXHEHQA.js} +15 -2
  6. package/dist/chunk-FRXHEHQA.js.map +1 -0
  7. package/dist/{chunk-UHNLEBBO.js → chunk-I3RSC52R.js} +28 -3
  8. package/dist/chunk-I3RSC52R.js.map +1 -0
  9. package/dist/chunk-M76SAXXM.js +13 -0
  10. package/dist/chunk-M76SAXXM.js.map +1 -0
  11. package/dist/{chunk-7R6LOOAI.js → chunk-N4PLEYFJ.js} +4 -2
  12. package/dist/chunk-N4PLEYFJ.js.map +1 -0
  13. package/dist/element/index.d.ts +142 -6
  14. package/dist/element/index.js +138 -43
  15. package/dist/element/index.js.map +1 -1
  16. package/dist/{engine-DPft99Wj.d.ts → engine-CsNRD7B7.d.ts} +1 -1
  17. package/dist/{engine-CDdaSojx.d.cts → engine-CzOCrfY9.d.cts} +1 -1
  18. package/dist/index.cjs +579 -47
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +5 -5
  21. package/dist/index.d.ts +5 -5
  22. package/dist/index.js +447 -37
  23. package/dist/index.js.map +1 -1
  24. package/dist/react/index.cjs +26 -0
  25. package/dist/react/index.cjs.map +1 -1
  26. package/dist/react/index.d.cts +24 -5
  27. package/dist/react/index.d.ts +24 -5
  28. package/dist/react/index.js +2 -2
  29. package/dist/{softphone-CY5d2WWY.d.cts → softphone-By9sizlV.d.ts} +42 -3
  30. package/dist/{softphone-DXgmTyrX.d.ts → softphone-JZvJdzU3.d.cts} +42 -3
  31. package/dist/{sources-ONYQW733.js → sources-QL2LRZVH.js} +19 -2
  32. package/dist/sources-QL2LRZVH.js.map +1 -0
  33. package/dist/testing/index.cjs +13 -0
  34. package/dist/testing/index.cjs.map +1 -1
  35. package/dist/testing/index.d.cts +2 -2
  36. package/dist/testing/index.d.ts +2 -2
  37. package/dist/testing/index.js +1 -1
  38. package/dist/ui/index.cjs +92 -2
  39. package/dist/ui/index.cjs.map +1 -1
  40. package/dist/ui/index.d.cts +2 -2
  41. package/dist/ui/index.d.ts +2 -2
  42. package/dist/ui/index.js +69 -4
  43. package/dist/ui/index.js.map +1 -1
  44. package/dist/zadarma/index.cjs +111 -14
  45. package/dist/zadarma/index.cjs.map +1 -1
  46. package/dist/zadarma/index.d.cts +5 -6
  47. package/dist/zadarma/index.d.ts +5 -6
  48. package/dist/zadarma/index.js +3 -2
  49. package/dist/zadarma-engine-TIUE7PZ7.js +6 -0
  50. package/dist/{zadarma-engine-L5DEFXUS.js.map → zadarma-engine-TIUE7PZ7.js.map} +1 -1
  51. package/package.json +1 -1
  52. package/dist/chunk-2ZSOGGPO.js.map +0 -1
  53. package/dist/chunk-7R6LOOAI.js.map +0 -1
  54. package/dist/chunk-EYEDPD7Y.js.map +0 -1
  55. package/dist/chunk-UHNLEBBO.js.map +0 -1
  56. package/dist/sources-ONYQW733.js.map +0 -1
  57. package/dist/zadarma-engine-L5DEFXUS.js +0 -5
@@ -361,6 +361,19 @@ var ERROR_CATALOGUE = {
361
361
  message: "The provider does not support this operation.",
362
362
  solution: "Guard the UI on the operation before offering it."
363
363
  },
364
+ // ── Line owner (docs/specs/line-owner.md §5) ────────────────────────────────
365
+ // What `takeLine()` rejects with — and `call()` from `standby`, which takes the line first. Never
366
+ // emitted on `SoftphoneEvent.Error`: the promise IS the answer, and nothing about the phone is broken.
367
+ "line/claim-refused": {
368
+ fatal: false,
369
+ message: "The line was not moved here: the user is on a call on the softphone that has it.",
370
+ 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."
371
+ },
372
+ "line/unavailable": {
373
+ fatal: false,
374
+ message: "The line could not be taken: nothing decided where it is.",
375
+ 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."
376
+ },
364
377
  // ── Provider ────────────────────────────────────────────────────────────────
365
378
  "provider/provision-failed": {
366
379
  fatal: true,
@@ -416,9 +429,22 @@ var SipCallEngine = class {
416
429
  provider;
417
430
  factory;
418
431
  #state = "stopped";
432
+ /**
433
+ * Which `start()`…`stop()` cycle this is — bumped by `stop()`, which ends one. Everything that awaits
434
+ * and then acts (builds a UA, subscribes to the signal, sets the state) captures it first and gives up
435
+ * if a stop has happened since. Not bumped by `start()`: that would cancel work begun just before it,
436
+ * such as a call placed while the softphone is still starting.
437
+ *
438
+ * Without it a stopped phone came back to life: a provisioning or a signal connect still in flight
439
+ * when `stop()` ran finished afterwards and REGISTERed — and since the PBX routes a call to whichever
440
+ * client registered last, it quietly took calls from the user's other device while its UI said off.
441
+ */
442
+ #run = 0;
419
443
  #ua = null;
420
444
  /** In-flight `#createUA()`. THE concurrency guard — see `#ensureUA`. */
421
445
  #uaInFlight = null;
446
+ /** Calls waiting for the current UA to register (`#callWhenRegistered`), failed by `#teardownUA`. */
447
+ #awaitingRegistration = /* @__PURE__ */ new Set();
422
448
  #domain = "";
423
449
  #activeCall = null;
424
450
  /**
@@ -501,14 +527,25 @@ var SipCallEngine = class {
501
527
  for (const listener of [...this.#errorListeners]) listener(error);
502
528
  }
503
529
  async start() {
530
+ const run = this.#run;
504
531
  this.#setState("starting");
505
532
  if (this.provider.registrationMode === "persistent") {
506
- await this.#ensureUA();
533
+ try {
534
+ await this.#ensureUA();
535
+ } catch (cause) {
536
+ if (run === this.#run) throw cause;
537
+ }
538
+ if (run !== this.#run) return;
507
539
  this.#setState("online");
508
540
  } else {
509
541
  const signal = this.provider.incomingSignal;
510
542
  if (signal) {
511
- await signal.connect();
543
+ try {
544
+ await signal.connect();
545
+ } catch (cause) {
546
+ if (run === this.#run) throw cause;
547
+ }
548
+ if (run !== this.#run) return;
512
549
  this.#signalUnsub = signal.onSignal(() => void this.#onIncomingSignal());
513
550
  this.#signalConnectionUnsub = signal.onConnectionChange?.(
514
551
  (connected) => this.#setState(connected ? "online" : "offline")
@@ -518,6 +555,7 @@ var SipCallEngine = class {
518
555
  }
519
556
  }
520
557
  async stop() {
558
+ this.#run += 1;
521
559
  this.#clearNoInviteTimer();
522
560
  this.#signalUnsub?.();
523
561
  this.#signalUnsub = null;
@@ -536,11 +574,12 @@ var SipCallEngine = class {
536
574
  async #onIncomingSignal() {
537
575
  if (this.now() < this.#rejectGuardUntil) return;
538
576
  if (this.#activeCall) return;
577
+ const run = this.#run;
539
578
  try {
540
579
  await this.#ensureUA();
541
580
  this.#armNoInviteTeardown();
542
581
  } catch {
543
- this.#teardownUA();
582
+ if (run === this.#run) this.#teardownUA();
544
583
  }
545
584
  }
546
585
  #armNoInviteTeardown() {
@@ -602,12 +641,16 @@ var SipCallEngine = class {
602
641
  */
603
642
  async #ensureUA() {
604
643
  if (this.#ua) return this.#ua;
605
- this.#uaInFlight ??= this.#createUA().finally(() => {
606
- this.#uaInFlight = null;
607
- });
644
+ if (!this.#uaInFlight) {
645
+ const inFlight = this.#createUA().finally(() => {
646
+ if (this.#uaInFlight === inFlight) this.#uaInFlight = null;
647
+ });
648
+ this.#uaInFlight = inFlight;
649
+ }
608
650
  return this.#uaInFlight;
609
651
  }
610
652
  async #createUA() {
653
+ const run = this.#run;
611
654
  const factory = await this.#resolveFactory();
612
655
  let credentials;
613
656
  try {
@@ -617,17 +660,28 @@ var SipCallEngine = class {
617
660
  else this.#reportError("provider/provision-failed", cause);
618
661
  throw cause;
619
662
  }
663
+ if (run !== this.#run) throw createError("engine/not-started");
620
664
  this.#domain = domainOf(credentials);
621
665
  const config = this.#uaConfig(credentials, factory);
622
666
  const ua = factory.createUA(config);
623
- ua.on("newRTCSession", ({ session, originator }) => this.#onNewSession(session, originator));
667
+ ua.on("newRTCSession", ({ session, originator }) => {
668
+ if (ua !== this.#ua) {
669
+ if (originator === "remote") session.terminate({ status_code: 486, reason_phrase: "Busy Here" });
670
+ return;
671
+ }
672
+ this.#onNewSession(session, originator);
673
+ });
624
674
  ua.on("disconnected", ({ error }) => {
675
+ if (ua !== this.#ua) return;
625
676
  if (!error) return;
626
677
  this.#setState("reconnecting");
627
678
  this.#reportError("net/transport-failed", error);
628
679
  });
629
- ua.on("connected", () => this.#setState("online"));
680
+ ua.on("connected", () => {
681
+ if (ua === this.#ua) this.#setState("online");
682
+ });
630
683
  ua.on("registrationFailed", (e) => {
684
+ if (run !== this.#run) return;
631
685
  const detail = e;
632
686
  this.#reportError("net/registration-failed", {
633
687
  cause: detail?.cause,
@@ -635,7 +689,7 @@ var SipCallEngine = class {
635
689
  domain: this.#domain,
636
690
  hint: "A bare 401 with no auth challenge usually means the registrar is in the wrong datacenter (SPEC \xA73.1)."
637
691
  });
638
- this.#teardownUA();
692
+ if (ua === this.#ua) this.#teardownUA();
639
693
  void this.#refreshSignal();
640
694
  });
641
695
  this.#ua = ua;
@@ -662,6 +716,7 @@ var SipCallEngine = class {
662
716
  #teardownUA() {
663
717
  this.#uaInFlight = null;
664
718
  if (!this.#ua) return;
719
+ for (const fail of [...this.#awaitingRegistration]) fail();
665
720
  try {
666
721
  this.#ua.terminateSessions({ status_code: 487 });
667
722
  this.#ua.stop();
@@ -685,8 +740,27 @@ var SipCallEngine = class {
685
740
  #callWhenRegistered(ua, target) {
686
741
  const options = { mediaConstraints: buildAudioConstraints(this.#inputDeviceId), pcConfig: PC_CONFIG };
687
742
  if (ua.isRegistered()) return Promise.resolve(ua.call(target, options));
688
- return new Promise((resolve) => {
689
- ua.on("registered", () => resolve(ua.call(target, options)));
743
+ const run = this.#run;
744
+ return new Promise((resolve, reject) => {
745
+ let settled = false;
746
+ const fail = () => {
747
+ if (settled) return;
748
+ settled = true;
749
+ this.#awaitingRegistration.delete(fail);
750
+ reject(createError(run === this.#run ? "call/failed" : "engine/not-started"));
751
+ };
752
+ this.#awaitingRegistration.add(fail);
753
+ ua.on("registered", () => {
754
+ if (settled) return;
755
+ if (ua !== this.#ua) return fail();
756
+ settled = true;
757
+ this.#awaitingRegistration.delete(fail);
758
+ try {
759
+ resolve(ua.call(target, options));
760
+ } catch (cause) {
761
+ reject(cause);
762
+ }
763
+ });
690
764
  });
691
765
  }
692
766
  #wrapCall(session) {
@@ -826,6 +900,12 @@ var ZadarmaPushSignal = class {
826
900
  #retryTimer;
827
901
  #unwatchBackground;
828
902
  #closed = false;
903
+ /**
904
+ * Bumped by `disconnect()`. An open or a rebuild still awaiting its key when the signal is disconnected
905
+ * must build nothing afterwards — and `#closed` alone cannot say so once a later `connect()` (stop, then
906
+ * start again) has reset it: the late socket was built beside the new one, and nothing ever closed it.
907
+ */
908
+ #run = 0;
829
909
  #listeners = /* @__PURE__ */ new Set();
830
910
  #connectionListeners = /* @__PURE__ */ new Set();
831
911
  async connect() {
@@ -835,8 +915,9 @@ var ZadarmaPushSignal = class {
835
915
  await this.#open();
836
916
  }
837
917
  async #open() {
918
+ const run = this.#run;
838
919
  const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
839
- if (this.#closed) return;
920
+ if (run !== this.#run) return;
840
921
  const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
841
922
  this.#socket = socket;
842
923
  this.#attach(socket);
@@ -931,9 +1012,10 @@ var ZadarmaPushSignal = class {
931
1012
  return;
932
1013
  }
933
1014
  this.#rebuilding = true;
1015
+ const run = this.#run;
934
1016
  try {
935
1017
  const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
936
- if (this.#closed) return;
1018
+ if (run !== this.#run) return;
937
1019
  const swapped = await new Promise((resolve) => {
938
1020
  const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
939
1021
  this.#candidate = socket;
@@ -986,6 +1068,7 @@ var ZadarmaPushSignal = class {
986
1068
  }
987
1069
  disconnect() {
988
1070
  this.#closed = true;
1071
+ this.#run += 1;
989
1072
  this.#stopPings();
990
1073
  clearTimeout(this.#refreshTimer);
991
1074
  this.#refreshTimer = void 0;
@@ -1256,10 +1339,24 @@ function makeZadarmaBrowserDeps(config) {
1256
1339
  };
1257
1340
  }
1258
1341
 
1342
+ // src/core/api-base.ts
1343
+ function normalizeApiBase(url) {
1344
+ const trimmed = url.replace(/\/+$/, "");
1345
+ if (!trimmed.endsWith("/api/v1")) return trimmed;
1346
+ console.warn(
1347
+ "[softphone] apiBase should be Coventia's domain only, without /api/v1. Ignoring the /api/v1 suffix."
1348
+ );
1349
+ return trimmed.slice(0, -"/api/v1".length).replace(/\/+$/, "");
1350
+ }
1351
+
1259
1352
  // src/transports/zadarma-engine.ts
1260
1353
  function createZadarmaEngine(config) {
1261
1354
  const provider = new ZadarmaProvider(
1262
- makeZadarmaBrowserDeps({ ...config, tokenSource: cachedTokenSource(config.tokenSource) })
1355
+ makeZadarmaBrowserDeps({
1356
+ ...config,
1357
+ apiBase: normalizeApiBase(config.apiBase),
1358
+ tokenSource: cachedTokenSource(config.tokenSource)
1359
+ })
1263
1360
  );
1264
1361
  return new SipCallEngine(provider, loadJsSipFactory);
1265
1362
  }