@coventia/softphone 0.1.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 (69) hide show
  1. package/dist/3x2-2HQTJDKZ.js +6583 -0
  2. package/dist/3x2-2HQTJDKZ.js.map +1 -0
  3. package/dist/3x2-INKWPNLA.js +6585 -0
  4. package/dist/3x2-INKWPNLA.js.map +1 -0
  5. package/dist/call-BTYlT2SU.d.cts +680 -0
  6. package/dist/call-BTYlT2SU.d.ts +680 -0
  7. package/dist/chunk-2ZSOGGPO.js +169 -0
  8. package/dist/chunk-2ZSOGGPO.js.map +1 -0
  9. package/dist/chunk-3V2L6UTK.js +129 -0
  10. package/dist/chunk-3V2L6UTK.js.map +1 -0
  11. package/dist/chunk-5BGEHMPP.js +945 -0
  12. package/dist/chunk-5BGEHMPP.js.map +1 -0
  13. package/dist/chunk-5WRI5ZAA.js +29 -0
  14. package/dist/chunk-5WRI5ZAA.js.map +1 -0
  15. package/dist/chunk-KAXEMJ7E.js +11 -0
  16. package/dist/chunk-KAXEMJ7E.js.map +1 -0
  17. package/dist/chunk-MHOJHG5E.js +39 -0
  18. package/dist/chunk-MHOJHG5E.js.map +1 -0
  19. package/dist/chunk-MP7CSA24.js +299 -0
  20. package/dist/chunk-MP7CSA24.js.map +1 -0
  21. package/dist/chunk-N2N2EQQJ.js +29 -0
  22. package/dist/chunk-N2N2EQQJ.js.map +1 -0
  23. package/dist/element/index.d.ts +853 -0
  24. package/dist/element/index.js +9635 -0
  25. package/dist/element/index.js.map +1 -0
  26. package/dist/engine-D9nbeY08.d.cts +115 -0
  27. package/dist/engine-Doa6Brkm.d.ts +115 -0
  28. package/dist/index.cjs +2133 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +62 -0
  31. package/dist/index.d.ts +62 -0
  32. package/dist/index.js +630 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/min-64GIMWWL.js +5456 -0
  35. package/dist/min-64GIMWWL.js.map +1 -0
  36. package/dist/min-ATXYR23R.js +5458 -0
  37. package/dist/min-ATXYR23R.js.map +1 -0
  38. package/dist/react/index.cjs +138 -0
  39. package/dist/react/index.cjs.map +1 -0
  40. package/dist/react/index.d.cts +68 -0
  41. package/dist/react/index.d.ts +68 -0
  42. package/dist/react/index.js +4 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/softphone-DHmhotbf.d.ts +128 -0
  45. package/dist/softphone-DhuhcKCy.d.cts +128 -0
  46. package/dist/softphone.css +1923 -0
  47. package/dist/sources-N4BOEDRL.js +226 -0
  48. package/dist/sources-N4BOEDRL.js.map +1 -0
  49. package/dist/testing/index.cjs +345 -0
  50. package/dist/testing/index.cjs.map +1 -0
  51. package/dist/testing/index.d.cts +66 -0
  52. package/dist/testing/index.d.ts +66 -0
  53. package/dist/testing/index.js +186 -0
  54. package/dist/testing/index.js.map +1 -0
  55. package/dist/ui/index.cjs +15428 -0
  56. package/dist/ui/index.cjs.map +1 -0
  57. package/dist/ui/index.d.cts +188 -0
  58. package/dist/ui/index.d.ts +188 -0
  59. package/dist/ui/index.js +2256 -0
  60. package/dist/ui/index.js.map +1 -0
  61. package/dist/zadarma/index.cjs +1144 -0
  62. package/dist/zadarma/index.cjs.map +1 -0
  63. package/dist/zadarma/index.d.cts +86 -0
  64. package/dist/zadarma/index.d.ts +86 -0
  65. package/dist/zadarma/index.js +5 -0
  66. package/dist/zadarma/index.js.map +1 -0
  67. package/dist/zadarma-engine-DN2SZSQ3.js +5 -0
  68. package/dist/zadarma-engine-DN2SZSQ3.js.map +1 -0
  69. package/package.json +123 -0
package/dist/index.js ADDED
@@ -0,0 +1,630 @@
1
+ import { isBrowser } from './chunk-KAXEMJ7E.js';
2
+ export { isBrowser, isWebRtcSupported } from './chunk-KAXEMJ7E.js';
3
+ export { CallEvent, SoftphoneEvent } from './chunk-N2N2EQQJ.js';
4
+ import { isFatal } from './chunk-2ZSOGGPO.js';
5
+ export { ERROR_CATALOGUE, isFatal } from './chunk-2ZSOGGPO.js';
6
+ import { cachedTokenSource } from './chunk-MHOJHG5E.js';
7
+
8
+ // src/core/emitter.ts
9
+ var Emitter = class {
10
+ constructor(onListenerError) {
11
+ this.onListenerError = onListenerError;
12
+ }
13
+ onListenerError;
14
+ // A copy of the listener set is iterated on emit, so a listener that unsubscribes (or subscribes)
15
+ // during dispatch does not mutate the set mid-iteration.
16
+ #listeners = /* @__PURE__ */ new Map();
17
+ on(event, listener) {
18
+ let set = this.#listeners.get(event);
19
+ if (!set) {
20
+ set = /* @__PURE__ */ new Set();
21
+ this.#listeners.set(event, set);
22
+ }
23
+ set.add(listener);
24
+ return () => this.off(event, listener);
25
+ }
26
+ once(event, listener) {
27
+ const wrapped = (payload) => {
28
+ off();
29
+ listener(payload);
30
+ };
31
+ const off = this.on(event, wrapped);
32
+ return off;
33
+ }
34
+ off(event, listener) {
35
+ const set = this.#listeners.get(event);
36
+ if (!set) return;
37
+ set.delete(listener);
38
+ if (set.size === 0) this.#listeners.delete(event);
39
+ }
40
+ emit(event, payload) {
41
+ const set = this.#listeners.get(event);
42
+ if (!set) return;
43
+ for (const listener of [...set]) {
44
+ try {
45
+ listener(payload);
46
+ } catch (error) {
47
+ this.onListenerError?.(event, error);
48
+ }
49
+ }
50
+ }
51
+ /** Drop every listener. Called on teardown so a destroyed object leaks nothing. */
52
+ clear() {
53
+ this.#listeners.clear();
54
+ }
55
+ };
56
+
57
+ // src/core/call.ts
58
+ var CALL_TRANSITIONS = {
59
+ ringing: ["answering", "ended"],
60
+ dialing: ["early", "active", "ended"],
61
+ early: ["active", "ended"],
62
+ answering: ["active", "ended"],
63
+ active: ["held", "reconnecting", "ended"],
64
+ held: ["active", "reconnecting", "ended"],
65
+ reconnecting: ["active", "held", "ended"],
66
+ ended: []
67
+ };
68
+ var INITIAL_STATES = {
69
+ inbound: "ringing",
70
+ outbound: "dialing"
71
+ };
72
+ function canTransition(from, to) {
73
+ return CALL_TRANSITIONS[from].includes(to);
74
+ }
75
+ function isTerminal(state) {
76
+ return CALL_TRANSITIONS[state].length === 0;
77
+ }
78
+
79
+ // src/core/call-impl.ts
80
+ var CallImpl = class _CallImpl extends Emitter {
81
+ constructor(engineCall, onListenerError, onInternalError) {
82
+ super(onListenerError);
83
+ this.engineCall = engineCall;
84
+ this.onInternalError = onInternalError;
85
+ this.#onListenerError = onListenerError;
86
+ this.#state = INITIAL_STATES[engineCall.direction];
87
+ this.#remote = {
88
+ number: engineCall.remoteNumber,
89
+ displayName: engineCall.remoteNumber,
90
+ contact: null
91
+ };
92
+ this.#unsubscribe = engineCall.onEvent((event) => this.#onEngineEvent(event));
93
+ }
94
+ engineCall;
95
+ onInternalError;
96
+ #state;
97
+ #remote;
98
+ #answeredAt = null;
99
+ #muted = false;
100
+ #onHold = false;
101
+ #remoteStream = null;
102
+ #consultEngineCall = null;
103
+ #consultCall = null;
104
+ #unsubscribe;
105
+ #onListenerError;
106
+ get id() {
107
+ return this.engineCall.id;
108
+ }
109
+ get direction() {
110
+ return this.engineCall.direction;
111
+ }
112
+ get state() {
113
+ return this.#state;
114
+ }
115
+ get remote() {
116
+ return this.#remote;
117
+ }
118
+ get answeredAt() {
119
+ return this.#answeredAt;
120
+ }
121
+ get isMuted() {
122
+ return this.#muted;
123
+ }
124
+ get isOnHold() {
125
+ return this.#onHold;
126
+ }
127
+ get remoteStream() {
128
+ return this.#remoteStream;
129
+ }
130
+ // ── Raw engine signals → state machine ──────────────────────────────────────
131
+ #onEngineEvent(event) {
132
+ switch (event.type) {
133
+ case "progress":
134
+ this.#transition("early");
135
+ break;
136
+ case "accepted":
137
+ if (this.#answeredAt === null) this.#answeredAt = this.#now();
138
+ this.#transition("active");
139
+ this.emit("accepted" /* Accepted */, void 0);
140
+ break;
141
+ case "held":
142
+ this.#onHold = true;
143
+ if (this.#transition("held")) this.emit("held" /* Held */, { originator: event.originator });
144
+ break;
145
+ case "unheld":
146
+ this.#onHold = false;
147
+ if (this.#transition("active")) this.emit("unheld" /* Unheld */, { originator: event.originator });
148
+ break;
149
+ case "reconnecting":
150
+ if (this.#transition("reconnecting")) this.emit("reconnecting" /* Reconnecting */, void 0);
151
+ break;
152
+ case "reconnected":
153
+ if (this.#transition(this.#onHold ? "held" : "active"))
154
+ this.emit("reconnected" /* Reconnected */, void 0);
155
+ break;
156
+ case "ended":
157
+ this.#end(event.reason);
158
+ break;
159
+ case "stream":
160
+ this.#remoteStream = event.stream;
161
+ this.emit("remoteStream" /* RemoteStream */, { stream: event.stream });
162
+ break;
163
+ }
164
+ }
165
+ // ── Commands ────────────────────────────────────────────────────────────────
166
+ async answer() {
167
+ if (this.#state !== "ringing") {
168
+ throw this.#stateError("answer", "call/not-answerable");
169
+ }
170
+ this.#transition("answering");
171
+ await this.engineCall.answer();
172
+ }
173
+ async reject() {
174
+ if (this.#state !== "ringing") {
175
+ throw this.#stateError("reject", "call/not-answerable");
176
+ }
177
+ await this.engineCall.reject();
178
+ }
179
+ async hangup() {
180
+ if (isTerminal(this.#state)) return;
181
+ await this.engineCall.hangup();
182
+ }
183
+ mute(muted) {
184
+ if (this.#muted === muted) return;
185
+ this.#muted = muted;
186
+ this.engineCall.mute(muted);
187
+ this.emit("muted" /* Muted */, { muted });
188
+ }
189
+ async hold() {
190
+ if (this.#onHold || this.#state !== "active" && this.#state !== "reconnecting") return;
191
+ await this.engineCall.hold();
192
+ }
193
+ async unhold() {
194
+ if (!this.#onHold) return;
195
+ await this.engineCall.unhold();
196
+ }
197
+ sendDigits(digits) {
198
+ this.engineCall.sendDigits(digits);
199
+ this.emit("digitsSent" /* DigitsSent */, { digits });
200
+ }
201
+ async transfer(target) {
202
+ await this.engineCall.transfer(target);
203
+ }
204
+ async startConsult(target) {
205
+ await this.hold();
206
+ const consultEngineCall = await this.engineCall.startConsult(target);
207
+ this.#consultEngineCall = consultEngineCall;
208
+ const consult = new _CallImpl(consultEngineCall, this.#onListenerError, this.onInternalError);
209
+ this.#consultCall = consult;
210
+ return consult;
211
+ }
212
+ async completeTransfer() {
213
+ if (!this.#consultEngineCall) throw this.#stateError("completeTransfer", "call/unsupported");
214
+ await this.engineCall.completeTransfer(this.#consultEngineCall);
215
+ }
216
+ async cancelConsult() {
217
+ if (!this.#consultCall) return;
218
+ await this.#consultCall.hangup();
219
+ this.#consultCall = null;
220
+ this.#consultEngineCall = null;
221
+ await this.unhold();
222
+ }
223
+ // ── Internals ───────────────────────────────────────────────────────────────
224
+ #transition(to) {
225
+ const from = this.#state;
226
+ if (from === to) return false;
227
+ if (isTerminal(from)) return false;
228
+ if (!canTransition(from, to)) {
229
+ this.onInternalError?.(`illegal call transition ${from} \u2192 ${to} (SPEC \xA75.3)`);
230
+ return false;
231
+ }
232
+ this.#state = to;
233
+ this.emit("stateChanged" /* StateChanged */, { state: to, previous: from });
234
+ return true;
235
+ }
236
+ #end(reason) {
237
+ if (isTerminal(this.#state)) return;
238
+ const from = this.#state;
239
+ this.#state = "ended";
240
+ this.emit("stateChanged" /* StateChanged */, { state: "ended", previous: from });
241
+ this.emit("ended" /* Ended */, { reason });
242
+ this.#unsubscribe();
243
+ this.clear();
244
+ }
245
+ #stateError(op, _code) {
246
+ return new Error(`${op}() is not valid while call is '${this.#state}'`);
247
+ }
248
+ #now() {
249
+ return /* @__PURE__ */ new Date();
250
+ }
251
+ /** @internal — the Softphone updates caller identity once `resolveContact` resolves. */
252
+ setRemoteContact(contact) {
253
+ this.#remote = {
254
+ number: this.#remote.number,
255
+ displayName: contact.name ?? this.#remote.number,
256
+ contact
257
+ };
258
+ this.emit("remotePartyUpdated" /* RemotePartyUpdated */, void 0);
259
+ }
260
+ };
261
+
262
+ // src/core/backend.ts
263
+ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
264
+ const baseUrl = config.baseUrl ?? fallbackBaseUrl;
265
+ if (!baseUrl) {
266
+ throw new Error(
267
+ "No base URL for the Coventia backend. Pass `coventia.baseUrl`, or a top-level `apiBase` on `createSoftphone` for it to fall back to."
268
+ );
269
+ }
270
+ let pending = null;
271
+ const load = () => {
272
+ pending ??= import('./sources-N4BOEDRL.js').then(
273
+ (m) => m.coventiaSources({ ...config, baseUrl, tokenSource })
274
+ );
275
+ return pending;
276
+ };
277
+ return {
278
+ contacts: async (params) => (await load()).contacts(params),
279
+ history: async (params) => (await load()).history(params),
280
+ resolveContact: async (number) => (await load()).resolveContact(number),
281
+ listAgents: async () => (await load()).listAgents()
282
+ };
283
+ }
284
+
285
+ // src/core/tabs.ts
286
+ var LOCK_NAME = "coventia-softphone:leader";
287
+ function isWebLocksSupported() {
288
+ return isBrowser() && typeof navigator.locks?.request === "function";
289
+ }
290
+ function webLocksTabLock(lockName = LOCK_NAME) {
291
+ const controller = new AbortController();
292
+ let releaseHold;
293
+ return {
294
+ acquireLeadership() {
295
+ if (!isWebLocksSupported()) return Promise.resolve();
296
+ return new Promise((resolveAcquired) => {
297
+ navigator.locks.request(lockName, { signal: controller.signal }, () => {
298
+ resolveAcquired();
299
+ return new Promise((resolveHold) => {
300
+ releaseHold = resolveHold;
301
+ });
302
+ }).catch(() => {
303
+ });
304
+ });
305
+ },
306
+ release() {
307
+ if (releaseHold) {
308
+ const resolve = releaseHold;
309
+ releaseHold = void 0;
310
+ resolve();
311
+ } else {
312
+ controller.abort();
313
+ }
314
+ }
315
+ };
316
+ }
317
+
318
+ // src/core/softphone-impl.ts
319
+ var liveInstance = false;
320
+ var BrowserAudioDevices = class {
321
+ inputs = [];
322
+ outputs = [];
323
+ currentInputId = null;
324
+ currentOutputId = null;
325
+ canSelectOutput;
326
+ #engine;
327
+ #onChange;
328
+ constructor(engine, onChange) {
329
+ this.#engine = engine;
330
+ this.#onChange = onChange;
331
+ this.canSelectOutput = isBrowser() && typeof HTMLMediaElement !== "undefined" && "setSinkId" in HTMLMediaElement.prototype;
332
+ if (isBrowser() && navigator.mediaDevices) {
333
+ void this.refresh();
334
+ navigator.mediaDevices.addEventListener?.("devicechange", () => void this.refresh());
335
+ }
336
+ }
337
+ async refresh() {
338
+ if (!isBrowser() || typeof navigator.mediaDevices?.enumerateDevices !== "function") return;
339
+ let all;
340
+ try {
341
+ all = await navigator.mediaDevices.enumerateDevices();
342
+ } catch {
343
+ return;
344
+ }
345
+ this.inputs = all.filter((d) => d.kind === "audioinput");
346
+ this.outputs = all.filter((d) => d.kind === "audiooutput");
347
+ if (this.currentInputId && !this.inputs.some((d) => d.deviceId === this.currentInputId)) {
348
+ this.currentInputId = null;
349
+ }
350
+ if (this.currentOutputId && !this.outputs.some((d) => d.deviceId === this.currentOutputId)) {
351
+ this.currentOutputId = null;
352
+ }
353
+ this.#onChange();
354
+ }
355
+ async setInput(deviceId) {
356
+ this.currentInputId = deviceId;
357
+ this.#onChange();
358
+ await this.#engine.setInputDevice?.(deviceId);
359
+ }
360
+ async setOutput(deviceId) {
361
+ this.currentOutputId = deviceId;
362
+ this.#onChange();
363
+ }
364
+ async setRingtoneOutput() {
365
+ }
366
+ };
367
+ var SoftphoneImpl = class extends Emitter {
368
+ #state = "stopped";
369
+ #lastError = null;
370
+ #activeCall = null;
371
+ #destroyed = false;
372
+ #startedHere = false;
373
+ #isLeader = false;
374
+ #engine;
375
+ #tabLock;
376
+ #resolveContact;
377
+ /** Contacts + history, whenever there is a Coventia backend to reach. The widget reads these. */
378
+ sources;
379
+ audio;
380
+ /**
381
+ * `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
382
+ * internal seam, not something an integrator tunes). Tests construct this class directly with a
383
+ * fake for deterministic control; `createSoftphone()` never passes a third argument, so every
384
+ * existing test that goes through it is unaffected (`multi-tab-leader-election.md` §3.8).
385
+ */
386
+ constructor(engine, config, tabLock = webLocksTabLock()) {
387
+ super();
388
+ this.#engine = engine;
389
+ this.#tabLock = tabLock;
390
+ this.audio = new BrowserAudioDevices(
391
+ this.#engine,
392
+ () => this.emit("deviceChange" /* DeviceChange */, void 0)
393
+ );
394
+ this.sources = config.coventia || config.apiBase ? coventiaBackend(config.coventia ?? {}, cachedTokenSource(config.tokenSource), config.apiBase) : null;
395
+ this.#resolveContact = config.resolveContact ?? this.sources?.resolveContact;
396
+ this.#engine.onIncoming((engineCall) => this.#onIncoming(engineCall));
397
+ this.#engine.onStateChange((state) => this.#setState(state));
398
+ this.#engine.onError?.((error) => this.#onError(error));
399
+ }
400
+ get state() {
401
+ return this.#state;
402
+ }
403
+ get lastError() {
404
+ return this.#lastError;
405
+ }
406
+ get isLeader() {
407
+ return this.#isLeader;
408
+ }
409
+ get activeCall() {
410
+ return this.#activeCall;
411
+ }
412
+ get canPlayAudio() {
413
+ return true;
414
+ }
415
+ async startAudio() {
416
+ }
417
+ async start() {
418
+ this.#assertUsable();
419
+ if (this.#startedHere) return;
420
+ if (liveInstance) {
421
+ throw new Error(
422
+ "A Softphone is already running on this page. Construct one and share it; call destroy() before starting another. (SPEC \xA75.3 / error engine/duplicate-instance)"
423
+ );
424
+ }
425
+ liveInstance = true;
426
+ this.#startedHere = true;
427
+ this.#setState("starting");
428
+ await this.#tabLock.acquireLeadership();
429
+ if (this.#destroyed) return;
430
+ this.#setLeader(true);
431
+ await this.#engine.start();
432
+ }
433
+ async stop() {
434
+ if (!this.#startedHere) return;
435
+ this.#tabLock.release();
436
+ if (this.#isLeader) {
437
+ await this.#engine.stop();
438
+ if (this.#activeCall) await this.#activeCall.hangup().catch(() => {
439
+ });
440
+ }
441
+ this.#setLeader(false);
442
+ this.#releaseInstance();
443
+ this.#setState("stopped");
444
+ }
445
+ async destroy() {
446
+ if (this.#destroyed) return;
447
+ this.#destroyed = true;
448
+ await this.stop().catch(() => {
449
+ });
450
+ this.#setState("destroyed");
451
+ this.clear();
452
+ }
453
+ async call(destination, options) {
454
+ this.#assertUsable();
455
+ if (this.#state !== "online" && this.#state !== "offline" && this.#state !== "starting") {
456
+ throw new Error(`Cannot place a call while softphone is '${this.#state}'.`);
457
+ }
458
+ if (this.#activeCall) {
459
+ throw new Error("A call is already in progress. Hang up before placing another.");
460
+ }
461
+ const engineCall = await this.#engine.placeCall(destination, options);
462
+ return this.#adopt(engineCall);
463
+ }
464
+ // ── internals ───────────────────────────────────────────────────────────────
465
+ #onIncoming(engineCall) {
466
+ if (this.#activeCall) {
467
+ void engineCall.reject().catch(() => {
468
+ });
469
+ return;
470
+ }
471
+ const call = this.#adopt(engineCall);
472
+ this.emit("incomingCall" /* IncomingCall */, call);
473
+ }
474
+ #adopt(engineCall) {
475
+ const call = new CallImpl(
476
+ engineCall,
477
+ (event, error) => this.#reportListenerError("call", event, error),
478
+ (message) => this.#reportInternalError(message)
479
+ );
480
+ this.#activeCall = call;
481
+ const off = call.on("ended" /* Ended */, () => {
482
+ off();
483
+ if (this.#activeCall === call) {
484
+ this.#activeCall = null;
485
+ this.emit("activeCallChanged" /* ActiveCallChanged */, { call: null });
486
+ }
487
+ });
488
+ if (this.#resolveContact) {
489
+ void this.#resolveContact(engineCall.remoteNumber).then((contact) => {
490
+ if (contact && this.#activeCall === call) call.setRemoteContact(contact);
491
+ }).catch(() => {
492
+ });
493
+ }
494
+ this.emit("activeCallChanged" /* ActiveCallChanged */, { call });
495
+ return call;
496
+ }
497
+ #setState(state) {
498
+ if (state === this.#state) return;
499
+ const previous = this.#state;
500
+ this.#state = state;
501
+ if (state === "online") this.#lastError = null;
502
+ this.emit("stateChanged" /* StateChanged */, { state, previous });
503
+ }
504
+ #setLeader(isLeader) {
505
+ if (isLeader === this.#isLeader) return;
506
+ this.#isLeader = isLeader;
507
+ this.emit("leaderChanged" /* LeaderChanged */, { isLeader });
508
+ }
509
+ /**
510
+ * Re-emit an engine failure to the host, and remember it if it is fatal.
511
+ *
512
+ * Every error is emitted; only fatal ones are retained. A non-fatal error is a fact about one call
513
+ * — the remote party was busy — and keeping it as softphone state would make a working phone look
514
+ * broken to anything that renders `lastError`.
515
+ */
516
+ #onError(error) {
517
+ if (isFatal(error)) this.#lastError = error;
518
+ this.emit("error" /* Error */, error);
519
+ }
520
+ #releaseInstance() {
521
+ if (this.#startedHere) {
522
+ liveInstance = false;
523
+ this.#startedHere = false;
524
+ }
525
+ }
526
+ #assertUsable() {
527
+ if (this.#destroyed) {
528
+ throw new Error("This Softphone has been destroyed. Construct a new one. (engine/destroyed)");
529
+ }
530
+ }
531
+ #reportListenerError(source, event, error) {
532
+ console.error(`[softphone] a ${source} '${String(event)}' listener threw:`, error);
533
+ }
534
+ #reportInternalError(message) {
535
+ console.error(`[softphone] ${message}`);
536
+ }
537
+ };
538
+
539
+ // src/core/default-provider.ts
540
+ var LazyRelay = class {
541
+ #listeners = /* @__PURE__ */ new Set();
542
+ #realOff = /* @__PURE__ */ new Map();
543
+ #target = null;
544
+ subscribe(listener) {
545
+ this.#listeners.add(listener);
546
+ if (this.#target) this.#realOff.set(listener, this.#target.on(listener));
547
+ return () => {
548
+ this.#listeners.delete(listener);
549
+ this.#realOff.get(listener)?.();
550
+ this.#realOff.delete(listener);
551
+ };
552
+ }
553
+ /** Called once the real engine exists — replays every listener registered before now onto it. */
554
+ attach(target) {
555
+ this.#target = target;
556
+ for (const listener of this.#listeners) this.#realOff.set(listener, target.on(listener));
557
+ }
558
+ };
559
+ function defaultZadarmaEngine(config) {
560
+ let real = null;
561
+ let pending = null;
562
+ const incoming = new LazyRelay();
563
+ const stateChange = new LazyRelay();
564
+ const error = new LazyRelay();
565
+ function load() {
566
+ pending ??= import('./zadarma-engine-DN2SZSQ3.js').then(({ createZadarmaEngine }) => {
567
+ const engine = createZadarmaEngine(config);
568
+ incoming.attach({ on: (l) => engine.onIncoming(l) });
569
+ stateChange.attach({ on: (l) => engine.onStateChange(l) });
570
+ error.attach({ on: (l) => engine.onError?.(l) ?? (() => {
571
+ }) });
572
+ real = engine;
573
+ return engine;
574
+ }).catch((cause) => {
575
+ pending = null;
576
+ throw cause;
577
+ });
578
+ return pending;
579
+ }
580
+ return {
581
+ get state() {
582
+ return real?.state ?? "stopped";
583
+ },
584
+ async start() {
585
+ await (await load()).start();
586
+ },
587
+ async stop() {
588
+ if (real) await real.stop();
589
+ },
590
+ async placeCall(destination, options) {
591
+ return (await load()).placeCall(destination, options);
592
+ },
593
+ onIncoming: (listener) => incoming.subscribe(listener),
594
+ onStateChange: (listener) => stateChange.subscribe(listener),
595
+ onError: (listener) => error.subscribe(listener)
596
+ };
597
+ }
598
+
599
+ // src/core/create.ts
600
+ function createSoftphone(config) {
601
+ return new SoftphoneImpl(resolveEngine(config), config);
602
+ }
603
+ function resolveEngine(config) {
604
+ if (config.provider !== void 0) {
605
+ if (isCallEngine(config.provider)) return config.provider;
606
+ throw new Error(
607
+ '`config.provider` was set but is not a valid CallEngine \u2014 it needs at least `placeCall` and `onIncoming`. Pass `createZadarmaEngine(...)` from "@coventia/softphone/zadarma", a `MockProvider` from "@coventia/softphone/testing", or omit it entirely to use the default.'
608
+ );
609
+ }
610
+ if (config.apiBase) {
611
+ return defaultZadarmaEngine({
612
+ apiBase: config.apiBase,
613
+ tokenSource: config.tokenSource,
614
+ organizationId: config.coventia?.organizationId
615
+ });
616
+ }
617
+ throw new Error(
618
+ 'No call engine configured. Pass `config.provider` explicitly \u2014 `createZadarmaEngine({ apiBase, tokenSource })` from "@coventia/softphone/zadarma", or a `MockProvider` from "@coventia/softphone/testing" \u2014 or set `config.apiBase` so a default Zadarma engine can be built.'
619
+ );
620
+ }
621
+ function isCallEngine(value) {
622
+ return typeof value === "object" && value !== null && typeof value.placeCall === "function" && typeof value.onIncoming === "function";
623
+ }
624
+
625
+ // src/index.ts
626
+ var VERSION = "0.0.0";
627
+
628
+ export { CALL_TRANSITIONS, INITIAL_STATES, VERSION, canTransition, createSoftphone, isTerminal };
629
+ //# sourceMappingURL=index.js.map
630
+ //# sourceMappingURL=index.js.map