@coventia/softphone 0.5.0 → 0.5.1

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 (37) hide show
  1. package/dist/{call-FqcvDg1t.d.cts → call-iGvr2ZnO.d.cts} +26 -2
  2. package/dist/{call-FqcvDg1t.d.ts → call-iGvr2ZnO.d.ts} +26 -2
  3. package/dist/chunk-3UTUWGVG.js +1891 -0
  4. package/dist/chunk-3UTUWGVG.js.map +1 -0
  5. package/dist/chunk-DZRSSZWB.js +39 -0
  6. package/dist/chunk-DZRSSZWB.js.map +1 -0
  7. package/dist/element/index.d.ts +25 -1
  8. package/dist/{engine-CzOCrfY9.d.cts → engine-BKoT_Pj7.d.cts} +10 -1
  9. package/dist/{engine-CsNRD7B7.d.ts → engine-CeGh5Yai.d.ts} +10 -1
  10. package/dist/index.cjs +1049 -130
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +5 -5
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +179 -13
  15. package/dist/index.js.map +1 -1
  16. package/dist/react/index.d.cts +2 -2
  17. package/dist/react/index.d.ts +2 -2
  18. package/dist/{softphone-By9sizlV.d.ts → softphone-D0ao5W4z.d.ts} +1 -1
  19. package/dist/{softphone-JZvJdzU3.d.cts → softphone-DRoS0jJR.d.cts} +1 -1
  20. package/dist/{sources-QL2LRZVH.js → sources-LGFJG7W2.js} +10 -38
  21. package/dist/sources-LGFJG7W2.js.map +1 -0
  22. package/dist/testing/index.d.cts +2 -2
  23. package/dist/testing/index.d.ts +2 -2
  24. package/dist/ui/index.d.cts +2 -2
  25. package/dist/ui/index.d.ts +2 -2
  26. package/dist/zadarma/index.cjs +865 -116
  27. package/dist/zadarma/index.cjs.map +1 -1
  28. package/dist/zadarma/index.d.cts +37 -10
  29. package/dist/zadarma/index.d.ts +37 -10
  30. package/dist/zadarma/index.js +2 -1
  31. package/dist/zadarma-engine-VYIXLBQG.js +7 -0
  32. package/dist/{zadarma-engine-TIUE7PZ7.js.map → zadarma-engine-VYIXLBQG.js.map} +1 -1
  33. package/package.json +1 -1
  34. package/dist/chunk-6RR6G35E.js +0 -1146
  35. package/dist/chunk-6RR6G35E.js.map +0 -1
  36. package/dist/sources-QL2LRZVH.js.map +0 -1
  37. package/dist/zadarma-engine-TIUE7PZ7.js +0 -6
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as Softphone } from './softphone-JZvJdzU3.cjs';
2
- export { A as AudioDevices } from './softphone-JZvJdzU3.cjs';
3
- import { S as SoftphoneConfig, O as OutboundNumberRefusal } from './call-FqcvDg1t.cjs';
4
- export { C as CALL_TRANSITIONS, a as Call, b as CallDirection, c as CallEvent, d as CallEventMap, e as CallOptions, f as CallState, g as Contact, E as ERROR_CATALOGUE, F as FatalErrorCode, h as FatalSoftphoneError, H as HangupReason, i as HoldOriginator, I as INITIAL_STATES, L as LineClaim, j as LineLocation, k as LineState, l as LogLevel, N as NonFatalErrorCode, m as NonFatalSoftphoneError, n as OutboundNumber, o as OutboundNumberKind, p as OutboundNumberSettings, q as OutboundNumberState, r as OutboundNumbersSource, R as RemoteParty, s as RingtoneOption, t as RingtoneSettings, u as RingtoneSource, v as SoftphoneError, w as SoftphoneErrorCode, x as SoftphoneEvent, y as SoftphoneEventMap, z as SoftphoneState, T as TokenEndpointOptions, A as TokenSource, B as TypedEmitter, V as VoiceToken, D as canTransition, G as isFatal, J as isTerminal } from './call-FqcvDg1t.cjs';
1
+ import { S as Softphone } from './softphone-DRoS0jJR.cjs';
2
+ export { A as AudioDevices } from './softphone-DRoS0jJR.cjs';
3
+ import { S as SoftphoneConfig, O as OutboundNumberRefusal } from './call-iGvr2ZnO.cjs';
4
+ export { C as CALL_TRANSITIONS, a as Call, b as CallDirection, c as CallEvent, d as CallEventMap, e as CallOptions, f as CallState, g as Contact, E as ERROR_CATALOGUE, F as FatalErrorCode, h as FatalSoftphoneError, H as HangupReason, i as HoldOriginator, I as INITIAL_STATES, L as LineClaim, j as LineLocation, k as LineState, l as LogLevel, N as NonFatalErrorCode, m as NonFatalSoftphoneError, n as OutboundNumber, o as OutboundNumberKind, p as OutboundNumberSettings, q as OutboundNumberState, r as OutboundNumbersSource, R as RemoteParty, s as RingtoneOption, t as RingtoneSettings, u as RingtoneSource, v as SoftphoneError, w as SoftphoneErrorCode, x as SoftphoneEvent, y as SoftphoneEventMap, z as SoftphoneState, T as TokenEndpointOptions, A as TokenSource, B as TypedEmitter, V as VoiceToken, D as canTransition, G as isFatal, J as isTerminal } from './call-iGvr2ZnO.cjs';
5
5
 
6
6
  /**
7
7
  * Construct a softphone.
@@ -67,6 +67,6 @@ declare function isWebRtcSupported(): boolean;
67
67
  * - `MockProvider` — lives at @coventia/softphone/testing, so it never lands in a production bundle.
68
68
  */
69
69
 
70
- declare const VERSION = "0.5.0";
70
+ declare const VERSION = "0.5.1";
71
71
 
72
72
  export { OutboundNumberRefusal, type OutboundNumberRefusedError, Softphone, SoftphoneConfig, VERSION, createSoftphone, isBrowser, isOutboundNumberRefused, isWebRtcSupported };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as Softphone } from './softphone-By9sizlV.js';
2
- export { A as AudioDevices } from './softphone-By9sizlV.js';
3
- import { S as SoftphoneConfig, O as OutboundNumberRefusal } from './call-FqcvDg1t.js';
4
- export { C as CALL_TRANSITIONS, a as Call, b as CallDirection, c as CallEvent, d as CallEventMap, e as CallOptions, f as CallState, g as Contact, E as ERROR_CATALOGUE, F as FatalErrorCode, h as FatalSoftphoneError, H as HangupReason, i as HoldOriginator, I as INITIAL_STATES, L as LineClaim, j as LineLocation, k as LineState, l as LogLevel, N as NonFatalErrorCode, m as NonFatalSoftphoneError, n as OutboundNumber, o as OutboundNumberKind, p as OutboundNumberSettings, q as OutboundNumberState, r as OutboundNumbersSource, R as RemoteParty, s as RingtoneOption, t as RingtoneSettings, u as RingtoneSource, v as SoftphoneError, w as SoftphoneErrorCode, x as SoftphoneEvent, y as SoftphoneEventMap, z as SoftphoneState, T as TokenEndpointOptions, A as TokenSource, B as TypedEmitter, V as VoiceToken, D as canTransition, G as isFatal, J as isTerminal } from './call-FqcvDg1t.js';
1
+ import { S as Softphone } from './softphone-D0ao5W4z.js';
2
+ export { A as AudioDevices } from './softphone-D0ao5W4z.js';
3
+ import { S as SoftphoneConfig, O as OutboundNumberRefusal } from './call-iGvr2ZnO.js';
4
+ export { C as CALL_TRANSITIONS, a as Call, b as CallDirection, c as CallEvent, d as CallEventMap, e as CallOptions, f as CallState, g as Contact, E as ERROR_CATALOGUE, F as FatalErrorCode, h as FatalSoftphoneError, H as HangupReason, i as HoldOriginator, I as INITIAL_STATES, L as LineClaim, j as LineLocation, k as LineState, l as LogLevel, N as NonFatalErrorCode, m as NonFatalSoftphoneError, n as OutboundNumber, o as OutboundNumberKind, p as OutboundNumberSettings, q as OutboundNumberState, r as OutboundNumbersSource, R as RemoteParty, s as RingtoneOption, t as RingtoneSettings, u as RingtoneSource, v as SoftphoneError, w as SoftphoneErrorCode, x as SoftphoneEvent, y as SoftphoneEventMap, z as SoftphoneState, T as TokenEndpointOptions, A as TokenSource, B as TypedEmitter, V as VoiceToken, D as canTransition, G as isFatal, J as isTerminal } from './call-iGvr2ZnO.js';
5
5
 
6
6
  /**
7
7
  * Construct a softphone.
@@ -67,6 +67,6 @@ declare function isWebRtcSupported(): boolean;
67
67
  * - `MockProvider` — lives at @coventia/softphone/testing, so it never lands in a production bundle.
68
68
  */
69
69
 
70
- declare const VERSION = "0.5.0";
70
+ declare const VERSION = "0.5.1";
71
71
 
72
72
  export { OutboundNumberRefusal, type OutboundNumberRefusedError, Softphone, SoftphoneConfig, VERSION, createSoftphone, isBrowser, isOutboundNumberRefused, isWebRtcSupported };
package/dist/index.js CHANGED
@@ -286,7 +286,7 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
286
286
  }
287
287
  let pending = null;
288
288
  const load = () => {
289
- pending ??= import('./sources-QL2LRZVH.js').then(
289
+ pending ??= import('./sources-LGFJG7W2.js').then(
290
290
  (m) => m.coventiaSources({ ...config, baseUrl, tokenSource })
291
291
  );
292
292
  return pending;
@@ -421,9 +421,55 @@ function startTransferWatch(params) {
421
421
  return stop;
422
422
  }
423
423
 
424
+ // src/core/start-retry.ts
425
+ var BACKOFF_BASE_MS = 1e3;
426
+ var BACKOFF_MAX_MS = 3e4;
427
+ function backoffDelayMs(attempt, random = Math.random) {
428
+ const capped = Math.min(BACKOFF_BASE_MS * 2 ** (Math.max(1, attempt) - 1), BACKOFF_MAX_MS);
429
+ return capped * random();
430
+ }
431
+ function isTransientStatus(status) {
432
+ return status === 0 || status === 408 || status === 425 || status === 429 || status >= 500;
433
+ }
434
+ function isNetworkFailure(error) {
435
+ const name = typeof error.name === "string" ? error.name : "";
436
+ return error instanceof TypeError || name === "AbortError" || name === "TimeoutError" || name === "NetworkError";
437
+ }
438
+ function transientStartFailure(error) {
439
+ let retryAfterMs = 0;
440
+ let viaTokenEndpoint = false;
441
+ let current = error;
442
+ for (let depth = 0; depth < 6 && current !== null && typeof current === "object"; depth += 1) {
443
+ const layer = current;
444
+ if (typeof layer.retryAfterMs === "number" && Number.isFinite(layer.retryAfterMs)) {
445
+ retryAfterMs = Math.max(retryAfterMs, layer.retryAfterMs);
446
+ }
447
+ if (typeof layer.status === "number") return isTransientStatus(layer.status) ? { retryAfterMs } : null;
448
+ if (isNetworkFailure(layer)) return { retryAfterMs };
449
+ if (isSoftphoneError(current)) {
450
+ if (layer.code === "auth/token-rejected") return null;
451
+ if (layer.code === "auth/token-fetch-failed") viaTokenEndpoint = true;
452
+ }
453
+ current = layer.cause;
454
+ }
455
+ return viaTokenEndpoint ? { retryAfterMs } : null;
456
+ }
457
+ function watchWake(wake) {
458
+ if (!isBrowser()) return () => {
459
+ };
460
+ const onOnline = () => wake();
461
+ const onVisibility = () => {
462
+ if (document.visibilityState === "visible") wake();
463
+ };
464
+ window.addEventListener("online", onOnline);
465
+ document.addEventListener("visibilitychange", onVisibility);
466
+ return () => {
467
+ window.removeEventListener("online", onOnline);
468
+ document.removeEventListener("visibilitychange", onVisibility);
469
+ };
470
+ }
471
+
424
472
  // src/core/presence-socket.ts
425
- var RECONNECT_BASE_MS = 1e3;
426
- var RECONNECT_MAX_MS = 3e4;
427
473
  var NOOP_WATCH = { reportCallState: () => {
428
474
  }, claimLine: () => false, stop: () => {
429
475
  } };
@@ -435,6 +481,7 @@ function startPresenceSocket(params) {
435
481
  clientId,
436
482
  onLine,
437
483
  onClaimResult,
484
+ onOffer,
438
485
  onLineUnsupported,
439
486
  onConnectFailed,
440
487
  onDisconnect,
@@ -475,8 +522,7 @@ function startPresenceSocket(params) {
475
522
  const scheduleReconnect = () => {
476
523
  if (stopped) return;
477
524
  attempt += 1;
478
- const capped = Math.min(RECONNECT_BASE_MS * 2 ** (attempt - 1), RECONNECT_MAX_MS);
479
- const delay = capped * random();
525
+ const delay = backoffDelayMs(attempt, random);
480
526
  reconnectTimer = scheduleTimeout(() => void connect(), delay);
481
527
  };
482
528
  const connect = async () => {
@@ -511,6 +557,9 @@ function startPresenceSocket(params) {
511
557
  },
512
558
  onClaimResult: (result) => {
513
559
  if (!stopped) onClaimResult?.(result);
560
+ },
561
+ onOffer: (offer) => {
562
+ if (!stopped) onOffer?.(offer);
514
563
  }
515
564
  });
516
565
  } catch {
@@ -762,6 +811,17 @@ var SoftphoneImpl = class extends Emitter {
762
811
  #claim = null;
763
812
  /** Pending: what the server said about the line stops counting (`LINE_LAPSE_MS`). */
764
813
  #lineLapseTimer = null;
814
+ // ── The start retry (incoming-call-reliability.md §4.6) ─────────────────────────────────────────────────
815
+ // An engine start that failed for a passing reason (a 5xx, the network, a timeout) is attempted again on its
816
+ // own, with backoff, instead of leaving the phone `offline` until someone presses "Reintentar".
817
+ /** Failed attempts in the current streak; back to 0 when the engine starts, or the softphone stops. */
818
+ #startRetries = 0;
819
+ /** The wait before the next attempt, or `null` when none is pending. */
820
+ #startRetryTimer = null;
821
+ /** No attempt before this (epoch ms): a `Retry-After` the failure carried, which a wake-up must honour too. */
822
+ #startRetryNotBefore = 0;
823
+ /** Unsubscribes the `online` / tab-visible wake-ups, armed only while an attempt is pending. */
824
+ #unwatchWake = null;
765
825
  /**
766
826
  * `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
767
827
  * internal seam, not something an integrator tunes). Tests construct this class directly with a
@@ -844,6 +904,7 @@ var SoftphoneImpl = class extends Emitter {
844
904
  async stop() {
845
905
  if (!this.#startedHere) return;
846
906
  this.#run += 1;
907
+ this.#cancelStartRetry();
847
908
  this.#lineMode = "off";
848
909
  this.#sayGoodbye();
849
910
  this.#startDecision?.end();
@@ -887,6 +948,7 @@ var SoftphoneImpl = class extends Emitter {
887
948
  async call(destination, options) {
888
949
  this.#assertUsable();
889
950
  const run = this.#run;
951
+ if (this.#startRetryTimer !== null) this.#wakeStartRetry();
890
952
  if (this.#startDecision) {
891
953
  let timer;
892
954
  const decided = await Promise.race([
@@ -1030,6 +1092,9 @@ var SoftphoneImpl = class extends Emitter {
1030
1092
  clientId: this.#clientId,
1031
1093
  onLine: (update) => this.#onLine(update),
1032
1094
  onClaimResult: (result) => this.#onClaimResult(result),
1095
+ // The calls offered to this line, straight to the engine: after a decline, they say when the declined
1096
+ // call's re-offers stop (incoming-call-reliability.md §4.4). Not public — nothing a host acts on.
1097
+ onOffer: (offer) => this.#engine.noteOffer?.(offer),
1033
1098
  // A source that cannot take part will never answer, and one that cannot connect cannot answer yet:
1034
1099
  // `start()` need not wait out its 4 s for either (R7). A later answer still applies.
1035
1100
  onLineUnsupported: () => this.#releaseFirstLine?.(),
@@ -1076,6 +1141,7 @@ var SoftphoneImpl = class extends Emitter {
1076
1141
  this.#presenceAgents = [];
1077
1142
  this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
1078
1143
  this.#forgetLine();
1144
+ this.#engine.noteOffer?.(null);
1079
1145
  }
1080
1146
  /**
1081
1147
  * A DELIBERATE stop (`stop()` / `destroy()`): say any call is over, then say goodbye, so the backend drops
@@ -1135,12 +1201,14 @@ var SoftphoneImpl = class extends Emitter {
1135
1201
  });
1136
1202
  return step;
1137
1203
  }
1204
+ /** Resolves when the engine started — or when its failure is being retried on its own (§4.6). */
1138
1205
  async #startEngine(run) {
1139
1206
  const reported = this.#lastError;
1140
1207
  try {
1141
1208
  await this.#engine.start();
1209
+ if (run === this.#run) this.#startRetries = 0;
1142
1210
  } catch (cause) {
1143
- if (run === this.#run) this.#engineStartFailed(cause, reported);
1211
+ if (run === this.#run && this.#engineStartFailed(cause, reported)) return;
1144
1212
  throw cause;
1145
1213
  }
1146
1214
  }
@@ -1148,19 +1216,99 @@ var SoftphoneImpl = class extends Emitter {
1148
1216
  * An engine start the line asked for failed. Kept as it was, this softphone would stay the owner on the
1149
1217
  * server — `starting`, its presence socket renewing the line — with no push socket anywhere: nobody would
1150
1218
  * 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.
1219
+ * phone was not switched off): the server hands the line on after its grace (R4).
1220
+ *
1221
+ * Then it depends on why (incoming-call-reliability.md §4.6, F5):
1222
+ * - **a passing failure** (a 5xx, the network, a timeout — `transientStartFailure`) is attempted again on its
1223
+ * own, after a backoff, as `reconnecting`, with no error gate. Found live: a voice token answered 500 after
1224
+ * 10 s left a background tab `offline` for good, and every call was lost until someone pressed "Reintentar".
1225
+ * Returns `true`.
1226
+ * - **anything else** (a refused credential, no extension) is reported like any engine failure —
1227
+ * `SoftphoneEvent.Error` and `lastError`, which is the widget's error gate and its "Reintentar" (`stop()`
1228
+ * then `start()`, which asks for the line again) — and nothing retries on its own. Returns `false`.
1154
1229
  */
1155
1230
  #engineStartFailed(cause, reportedBefore) {
1156
1231
  this.#engineOn = false;
1157
1232
  this.#engineRunning = false;
1158
1233
  this.#lineMode = "off";
1234
+ const transient = this.#destroyed ? null : transientStartFailure(cause);
1235
+ this.#stopPresence();
1236
+ if (transient) {
1237
+ this.#scheduleStartRetry(transient.retryAfterMs, cause);
1238
+ this.#setState("reconnecting");
1239
+ return true;
1240
+ }
1159
1241
  if (this.#lastError === reportedBefore) {
1160
1242
  this.#onError(isSoftphoneError(cause) && isFatal(cause) ? cause : createError("net/transport-failed", cause));
1161
1243
  }
1162
- this.#stopPresence();
1163
1244
  this.#setState("offline");
1245
+ return false;
1246
+ }
1247
+ /**
1248
+ * The next attempt, after the shared backoff (1 s doubling to 30 s, jittered — the presence socket's own) and
1249
+ * never before a `Retry-After` the failure carried. A plain timer: in a hidden tab it can stretch to about a
1250
+ * minute, which is why the browser coming back `online`, or the tab becoming visible, attempts at once
1251
+ * (`watchWake`). One warning per streak, so a console says what is going on without filling up.
1252
+ */
1253
+ #scheduleStartRetry(retryAfterMs, cause) {
1254
+ this.#startRetries += 1;
1255
+ const wait = Math.max(backoffDelayMs(this.#startRetries), retryAfterMs);
1256
+ if (this.#startRetries === 1) {
1257
+ console.warn("[softphone] the phone could not start; it keeps trying on its own", cause);
1258
+ }
1259
+ this.#startRetryNotBefore = Date.now() + retryAfterMs;
1260
+ if (this.#startRetryTimer !== null) clearTimeout(this.#startRetryTimer);
1261
+ const run = this.#run;
1262
+ this.#startRetryTimer = setTimeout(() => void this.#retryStart(run), wait);
1263
+ this.#unwatchWake ??= watchWake(() => this.#wakeStartRetry());
1264
+ }
1265
+ /** The pending attempt, now — unless a `Retry-After` says not yet, in which case it moves up to that moment. */
1266
+ #wakeStartRetry() {
1267
+ if (this.#startRetryTimer === null) return;
1268
+ const run = this.#run;
1269
+ clearTimeout(this.#startRetryTimer);
1270
+ const wait = this.#startRetryNotBefore - Date.now();
1271
+ if (wait > 0) {
1272
+ this.#startRetryTimer = setTimeout(() => void this.#retryStart(run), wait);
1273
+ return;
1274
+ }
1275
+ void this.#retryStart(run);
1276
+ }
1277
+ /** The wait and the wake-ups go; the streak's count stays (the next failure doubles on from it). */
1278
+ #clearStartRetryWait() {
1279
+ if (this.#startRetryTimer !== null) clearTimeout(this.#startRetryTimer);
1280
+ this.#startRetryTimer = null;
1281
+ this.#unwatchWake?.();
1282
+ this.#unwatchWake = null;
1283
+ }
1284
+ #cancelStartRetry() {
1285
+ this.#clearStartRetryWait();
1286
+ this.#startRetries = 0;
1287
+ }
1288
+ /**
1289
+ * One attempt: what "Reintentar" does after its `stop()`, minus the goodbye and the leadership (both kept) —
1290
+ * the presence socket opens again and its `hello` asks for the line, the line decides (`standby` if it went
1291
+ * elsewhere meanwhile, as at any start), and the engine starts. Without the goodbye, an attempt inside the
1292
+ * server's 10 s grace gets the line straight back. Never for a softphone stopped since it was scheduled.
1293
+ */
1294
+ async #retryStart(run) {
1295
+ this.#clearStartRetryWait();
1296
+ if (this.#destroyed || run !== this.#run || !this.#startedHere || !this.#isLeader) return;
1297
+ const decision = this.#openStartDecision();
1298
+ try {
1299
+ this.#lineMode = "deciding";
1300
+ this.#setState("starting");
1301
+ if (this.#presenceWatch && this.#line === "unknown") await this.#waitForFirstLine();
1302
+ if (this.#destroyed || run !== this.#run) return;
1303
+ this.#lineMode = "on";
1304
+ const engineStart = this.#applyLine();
1305
+ if (!engineStart) this.#startRetries = 0;
1306
+ decision.end();
1307
+ await engineStart?.catch(() => {
1308
+ });
1309
+ } finally {
1310
+ decision.end();
1311
+ }
1164
1312
  }
1165
1313
  /** `start()`'s decision (`#startDecision`): ended once, by whichever comes first — the decision or its end. */
1166
1314
  #openStartDecision() {
@@ -1214,6 +1362,7 @@ var SoftphoneImpl = class extends Emitter {
1214
1362
  * waiting softphone answers by listening again.
1215
1363
  */
1216
1364
  #onPresenceDisconnect() {
1365
+ this.#engine.noteOffer?.(null);
1217
1366
  this.#settleClaim(createError("line/unavailable"));
1218
1367
  if (this.#line === "unknown" || this.#lineLapseTimer !== null) return;
1219
1368
  this.#lineLapseTimer = setTimeout(() => {
@@ -1322,6 +1471,7 @@ var SoftphoneImpl = class extends Emitter {
1322
1471
  };
1323
1472
 
1324
1473
  // src/core/default-provider.ts
1474
+ var MAX_PENDING_OFFERS = 20;
1325
1475
  var LazyRelay = class {
1326
1476
  #listeners = /* @__PURE__ */ new Set();
1327
1477
  #realOff = /* @__PURE__ */ new Map();
@@ -1348,13 +1498,16 @@ function defaultZadarmaEngine(config) {
1348
1498
  const incoming = new LazyRelay();
1349
1499
  const stateChange = new LazyRelay();
1350
1500
  const error = new LazyRelay();
1501
+ let pendingOffers = [];
1351
1502
  function load() {
1352
- pending ??= import('./zadarma-engine-TIUE7PZ7.js').then(({ createZadarmaEngine }) => {
1503
+ pending ??= import('./zadarma-engine-VYIXLBQG.js').then(({ createZadarmaEngine }) => {
1353
1504
  const engine = createZadarmaEngine(config);
1354
1505
  incoming.attach({ on: (l) => engine.onIncoming(l) });
1355
1506
  stateChange.attach({ on: (l) => engine.onStateChange(l) });
1356
1507
  error.attach({ on: (l) => engine.onError?.(l) ?? (() => {
1357
1508
  }) });
1509
+ for (const offer of pendingOffers) engine.noteOffer?.(offer);
1510
+ pendingOffers = [];
1358
1511
  real = engine;
1359
1512
  return engine;
1360
1513
  }).catch((cause) => {
@@ -1385,7 +1538,20 @@ function defaultZadarmaEngine(config) {
1385
1538
  },
1386
1539
  onIncoming: (listener) => incoming.subscribe(listener),
1387
1540
  onStateChange: (listener) => stateChange.subscribe(listener),
1388
- onError: (listener) => error.subscribe(listener)
1541
+ onError: (listener) => error.subscribe(listener),
1542
+ /*
1543
+ * Relayed like the listeners above, or the decline guard on this — the default — path would never hear the
1544
+ * backend (incoming-call-reliability.md §4.4). Before the engine loads, kept (a few) rather than loading it:
1545
+ * a softphone in `standby` hears offers without ever starting an engine.
1546
+ */
1547
+ noteOffer: (offer) => {
1548
+ if (real) {
1549
+ real.noteOffer?.(offer);
1550
+ return;
1551
+ }
1552
+ pendingOffers.push(offer);
1553
+ if (pendingOffers.length > MAX_PENDING_OFFERS) pendingOffers.shift();
1554
+ }
1389
1555
  };
1390
1556
  }
1391
1557
 
@@ -1425,7 +1591,7 @@ function isCallEngine(value) {
1425
1591
  }
1426
1592
 
1427
1593
  // src/index.ts
1428
- var VERSION = "0.5.0";
1594
+ var VERSION = "0.5.1";
1429
1595
 
1430
1596
  export { CALL_TRANSITIONS, INITIAL_STATES, VERSION, canTransition, createSoftphone, isTerminal };
1431
1597
  //# sourceMappingURL=index.js.map