@coventia/softphone 0.1.0 → 0.2.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.
- package/dist/{call-BTYlT2SU.d.cts → call-VKq1I8yP.d.cts} +158 -5
- package/dist/{call-BTYlT2SU.d.ts → call-VKq1I8yP.d.ts} +158 -5
- package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
- package/dist/chunk-7R6LOOAI.js.map +1 -0
- package/dist/{chunk-5BGEHMPP.js → chunk-EYEDPD7Y.js} +146 -20
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
- package/dist/chunk-LMJXX4YM.js.map +1 -0
- package/dist/element/index.d.ts +162 -7
- package/dist/element/index.js +866 -421
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-D9nbeY08.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
- package/dist/{engine-Doa6Brkm.d.ts → engine-C0BOkIcW.d.ts} +1 -1
- package/dist/index.cjs +586 -28
- 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 +335 -8
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +15 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +14 -3
- package/dist/react/index.d.ts +14 -3
- package/dist/react/index.js +2 -2
- package/dist/{softphone-DhuhcKCy.d.cts → softphone-Cyq3hiIM.d.ts} +17 -2
- package/dist/{softphone-DHmhotbf.d.ts → softphone-DlGV12Xp.d.cts} +17 -2
- package/dist/softphone.css +477 -35
- package/dist/{sources-N4BOEDRL.js → sources-IRKNQPWW.js} +108 -5
- package/dist/sources-IRKNQPWW.js.map +1 -0
- package/dist/testing/index.cjs +41 -5
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +8 -2
- package/dist/testing/index.d.ts +8 -2
- package/dist/testing/index.js +41 -5
- package/dist/testing/index.js.map +1 -1
- package/dist/ui/index.cjs +846 -410
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +17 -6
- package/dist/ui/index.d.ts +17 -6
- package/dist/ui/index.js +878 -456
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +144 -18
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/dist/zadarma/index.js +1 -1
- package/dist/zadarma-engine-L5DEFXUS.js +5 -0
- package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3V2L6UTK.js.map +0 -1
- package/dist/chunk-5BGEHMPP.js.map +0 -1
- package/dist/chunk-N2N2EQQJ.js.map +0 -1
- package/dist/sources-N4BOEDRL.js.map +0 -1
- package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as Softphone } from './softphone-
|
|
2
|
-
export { A as AudioDevices } from './softphone-
|
|
3
|
-
import { S as SoftphoneConfig } from './call-
|
|
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 LogLevel, N as NonFatalErrorCode, j as NonFatalSoftphoneError, R as RemoteParty, k as SoftphoneError, l as SoftphoneErrorCode, m as SoftphoneEvent, n as SoftphoneEventMap, o as SoftphoneState, T as TokenEndpointOptions, p as TokenSource, q as TypedEmitter, V as VoiceToken, r as canTransition, s as isFatal, t as isTerminal } from './call-
|
|
1
|
+
import { S as Softphone } from './softphone-DlGV12Xp.cjs';
|
|
2
|
+
export { A as AudioDevices } from './softphone-DlGV12Xp.cjs';
|
|
3
|
+
import { S as SoftphoneConfig } from './call-VKq1I8yP.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 LogLevel, N as NonFatalErrorCode, j as NonFatalSoftphoneError, R as RemoteParty, k as SoftphoneError, l as SoftphoneErrorCode, m as SoftphoneEvent, n as SoftphoneEventMap, o as SoftphoneState, T as TokenEndpointOptions, p as TokenSource, q as TypedEmitter, V as VoiceToken, r as canTransition, s as isFatal, t as isTerminal } from './call-VKq1I8yP.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Construct a softphone.
|
|
@@ -57,6 +57,6 @@ declare function isWebRtcSupported(): boolean;
|
|
|
57
57
|
* - `MockProvider` — lives at @coventia/softphone/testing, so it never lands in a production bundle.
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
|
-
declare const VERSION = "0.
|
|
60
|
+
declare const VERSION = "0.2.1";
|
|
61
61
|
|
|
62
62
|
export { Softphone, SoftphoneConfig, VERSION, createSoftphone, isBrowser, isWebRtcSupported };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as Softphone } from './softphone-
|
|
2
|
-
export { A as AudioDevices } from './softphone-
|
|
3
|
-
import { S as SoftphoneConfig } from './call-
|
|
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 LogLevel, N as NonFatalErrorCode, j as NonFatalSoftphoneError, R as RemoteParty, k as SoftphoneError, l as SoftphoneErrorCode, m as SoftphoneEvent, n as SoftphoneEventMap, o as SoftphoneState, T as TokenEndpointOptions, p as TokenSource, q as TypedEmitter, V as VoiceToken, r as canTransition, s as isFatal, t as isTerminal } from './call-
|
|
1
|
+
import { S as Softphone } from './softphone-Cyq3hiIM.js';
|
|
2
|
+
export { A as AudioDevices } from './softphone-Cyq3hiIM.js';
|
|
3
|
+
import { S as SoftphoneConfig } from './call-VKq1I8yP.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 LogLevel, N as NonFatalErrorCode, j as NonFatalSoftphoneError, R as RemoteParty, k as SoftphoneError, l as SoftphoneErrorCode, m as SoftphoneEvent, n as SoftphoneEventMap, o as SoftphoneState, T as TokenEndpointOptions, p as TokenSource, q as TypedEmitter, V as VoiceToken, r as canTransition, s as isFatal, t as isTerminal } from './call-VKq1I8yP.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Construct a softphone.
|
|
@@ -57,6 +57,6 @@ declare function isWebRtcSupported(): boolean;
|
|
|
57
57
|
* - `MockProvider` — lives at @coventia/softphone/testing, so it never lands in a production bundle.
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
|
-
declare const VERSION = "0.
|
|
60
|
+
declare const VERSION = "0.2.1";
|
|
61
61
|
|
|
62
62
|
export { Softphone, SoftphoneConfig, VERSION, createSoftphone, isBrowser, isWebRtcSupported };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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-7R6LOOAI.js';
|
|
4
4
|
import { isFatal } from './chunk-2ZSOGGPO.js';
|
|
5
5
|
export { ERROR_CATALOGUE, isFatal } from './chunk-2ZSOGGPO.js';
|
|
6
6
|
import { cachedTokenSource } from './chunk-MHOJHG5E.js';
|
|
@@ -75,13 +75,17 @@ function canTransition(from, to) {
|
|
|
75
75
|
function isTerminal(state) {
|
|
76
76
|
return CALL_TRANSITIONS[state].length === 0;
|
|
77
77
|
}
|
|
78
|
+
function isOnCallState(state) {
|
|
79
|
+
return state !== "ringing" && !isTerminal(state);
|
|
80
|
+
}
|
|
78
81
|
|
|
79
82
|
// src/core/call-impl.ts
|
|
80
83
|
var CallImpl = class _CallImpl extends Emitter {
|
|
81
|
-
constructor(engineCall, onListenerError, onInternalError) {
|
|
84
|
+
constructor(engineCall, onListenerError, onInternalError, onTransfer) {
|
|
82
85
|
super(onListenerError);
|
|
83
86
|
this.engineCall = engineCall;
|
|
84
87
|
this.onInternalError = onInternalError;
|
|
88
|
+
this.onTransfer = onTransfer;
|
|
85
89
|
this.#onListenerError = onListenerError;
|
|
86
90
|
this.#state = INITIAL_STATES[engineCall.direction];
|
|
87
91
|
this.#remote = {
|
|
@@ -93,6 +97,7 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
93
97
|
}
|
|
94
98
|
engineCall;
|
|
95
99
|
onInternalError;
|
|
100
|
+
onTransfer;
|
|
96
101
|
#state;
|
|
97
102
|
#remote;
|
|
98
103
|
#answeredAt = null;
|
|
@@ -178,7 +183,10 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
178
183
|
}
|
|
179
184
|
async hangup() {
|
|
180
185
|
if (isTerminal(this.#state)) return;
|
|
181
|
-
|
|
186
|
+
const consult = this.#consultCall;
|
|
187
|
+
this.#consultCall = null;
|
|
188
|
+
this.#consultEngineCall = null;
|
|
189
|
+
await Promise.all([consult?.hangup() ?? Promise.resolve(), this.engineCall.hangup()]);
|
|
182
190
|
}
|
|
183
191
|
mute(muted) {
|
|
184
192
|
if (this.#muted === muted) return;
|
|
@@ -200,6 +208,7 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
200
208
|
}
|
|
201
209
|
async transfer(target) {
|
|
202
210
|
await this.engineCall.transfer(target);
|
|
211
|
+
this.onTransfer?.(target, this.#remote.number.trim() || null);
|
|
203
212
|
}
|
|
204
213
|
async startConsult(target) {
|
|
205
214
|
await this.hold();
|
|
@@ -235,6 +244,12 @@ var CallImpl = class _CallImpl extends Emitter {
|
|
|
235
244
|
}
|
|
236
245
|
#end(reason) {
|
|
237
246
|
if (isTerminal(this.#state)) return;
|
|
247
|
+
if (this.#consultCall) {
|
|
248
|
+
const consult = this.#consultCall;
|
|
249
|
+
this.#consultCall = null;
|
|
250
|
+
this.#consultEngineCall = null;
|
|
251
|
+
void consult.hangup();
|
|
252
|
+
}
|
|
238
253
|
const from = this.#state;
|
|
239
254
|
this.#state = "ended";
|
|
240
255
|
this.emit("stateChanged" /* StateChanged */, { state: "ended", previous: from });
|
|
@@ -269,7 +284,7 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
|
|
|
269
284
|
}
|
|
270
285
|
let pending = null;
|
|
271
286
|
const load = () => {
|
|
272
|
-
pending ??= import('./sources-
|
|
287
|
+
pending ??= import('./sources-IRKNQPWW.js').then(
|
|
273
288
|
(m) => m.coventiaSources({ ...config, baseUrl, tokenSource })
|
|
274
289
|
);
|
|
275
290
|
return pending;
|
|
@@ -278,7 +293,9 @@ function coventiaBackend(config, tokenSource, fallbackBaseUrl) {
|
|
|
278
293
|
contacts: async (params) => (await load()).contacts(params),
|
|
279
294
|
history: async (params) => (await load()).history(params),
|
|
280
295
|
resolveContact: async (number) => (await load()).resolveContact(number),
|
|
281
|
-
listAgents: async () => (await load()).listAgents()
|
|
296
|
+
listAgents: async () => (await load()).listAgents(),
|
|
297
|
+
transferOutcomes: async (params) => (await load()).transferOutcomes(params),
|
|
298
|
+
presence: async (callbacks) => (await load()).presence(callbacks)
|
|
282
299
|
};
|
|
283
300
|
}
|
|
284
301
|
|
|
@@ -315,8 +332,175 @@ function webLocksTabLock(lockName = LOCK_NAME) {
|
|
|
315
332
|
};
|
|
316
333
|
}
|
|
317
334
|
|
|
335
|
+
// src/core/transfer-watch.ts
|
|
336
|
+
var FAST_INTERVAL_MS = 2e3;
|
|
337
|
+
var FAST_WINDOW_MS = 1e4;
|
|
338
|
+
var SLOW_INTERVAL_MS = 5e3;
|
|
339
|
+
var DEADLINE_MS = 9e4;
|
|
340
|
+
var EXTENSION_LIKE = /^\d{1,6}$/;
|
|
341
|
+
function startTransferWatch(params) {
|
|
342
|
+
const {
|
|
343
|
+
target,
|
|
344
|
+
callerNumber,
|
|
345
|
+
source,
|
|
346
|
+
getIsLeader,
|
|
347
|
+
emit,
|
|
348
|
+
setTimeout: scheduleTimeout = globalThis.setTimeout,
|
|
349
|
+
clearTimeout: cancelTimeout = globalThis.clearTimeout,
|
|
350
|
+
now = () => Date.now()
|
|
351
|
+
} = params;
|
|
352
|
+
if (!EXTENSION_LIKE.test(target)) return () => {
|
|
353
|
+
};
|
|
354
|
+
if (!getIsLeader()) return () => {
|
|
355
|
+
};
|
|
356
|
+
let stopped = false;
|
|
357
|
+
let timer = null;
|
|
358
|
+
let cursor = null;
|
|
359
|
+
const startedAt = now();
|
|
360
|
+
const stop = () => {
|
|
361
|
+
stopped = true;
|
|
362
|
+
if (timer !== null) {
|
|
363
|
+
cancelTimeout(timer);
|
|
364
|
+
timer = null;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const finish = (status) => {
|
|
368
|
+
stop();
|
|
369
|
+
emit({ status, target, caller: callerNumber, at: new Date(now()) });
|
|
370
|
+
};
|
|
371
|
+
const scheduleNext = () => {
|
|
372
|
+
if (stopped) return;
|
|
373
|
+
const elapsed = now() - startedAt;
|
|
374
|
+
if (elapsed >= DEADLINE_MS) {
|
|
375
|
+
stop();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
timer = scheduleTimeout(() => void poll(), elapsed < FAST_WINDOW_MS ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS);
|
|
379
|
+
};
|
|
380
|
+
const poll = async () => {
|
|
381
|
+
if (stopped) return;
|
|
382
|
+
if (!getIsLeader()) {
|
|
383
|
+
stop();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
let page;
|
|
387
|
+
try {
|
|
388
|
+
page = await source(cursor === null ? {} : { since: cursor });
|
|
389
|
+
} catch {
|
|
390
|
+
if (!stopped) scheduleNext();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (stopped) return;
|
|
394
|
+
if (cursor === null) {
|
|
395
|
+
cursor = page.cursor;
|
|
396
|
+
} else {
|
|
397
|
+
cursor = page.cursor;
|
|
398
|
+
const match = page.outcomes.find((o) => o.target === target);
|
|
399
|
+
if (match) {
|
|
400
|
+
finish(match.status);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
scheduleNext();
|
|
405
|
+
};
|
|
406
|
+
void poll();
|
|
407
|
+
return stop;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/core/presence-socket.ts
|
|
411
|
+
var RECONNECT_BASE_MS = 1e3;
|
|
412
|
+
var RECONNECT_MAX_MS = 3e4;
|
|
413
|
+
var NOOP_WATCH = { reportCallState: () => {
|
|
414
|
+
}, stop: () => {
|
|
415
|
+
} };
|
|
416
|
+
function startPresenceSocket(params) {
|
|
417
|
+
const {
|
|
418
|
+
source,
|
|
419
|
+
getIsLeader,
|
|
420
|
+
onAgentsChanged,
|
|
421
|
+
setTimeout: scheduleTimeout = globalThis.setTimeout,
|
|
422
|
+
clearTimeout: cancelTimeout = globalThis.clearTimeout,
|
|
423
|
+
random = Math.random
|
|
424
|
+
} = params;
|
|
425
|
+
if (!getIsLeader()) return NOOP_WATCH;
|
|
426
|
+
let stopped = false;
|
|
427
|
+
let attempt = 0;
|
|
428
|
+
let reconnectTimer = null;
|
|
429
|
+
let agents = /* @__PURE__ */ new Map();
|
|
430
|
+
let lastReportedOnCall = null;
|
|
431
|
+
let connection = null;
|
|
432
|
+
let byeOnArrival = false;
|
|
433
|
+
const stop = (options) => {
|
|
434
|
+
if (stopped) return;
|
|
435
|
+
stopped = true;
|
|
436
|
+
if (reconnectTimer !== null) {
|
|
437
|
+
cancelTimeout(reconnectTimer);
|
|
438
|
+
reconnectTimer = null;
|
|
439
|
+
}
|
|
440
|
+
if (options?.bye) {
|
|
441
|
+
byeOnArrival = true;
|
|
442
|
+
connection?.bye?.();
|
|
443
|
+
}
|
|
444
|
+
connection?.close();
|
|
445
|
+
connection = null;
|
|
446
|
+
};
|
|
447
|
+
const scheduleReconnect = () => {
|
|
448
|
+
if (stopped) return;
|
|
449
|
+
attempt += 1;
|
|
450
|
+
const capped = Math.min(RECONNECT_BASE_MS * 2 ** (attempt - 1), RECONNECT_MAX_MS);
|
|
451
|
+
const delay = capped * random();
|
|
452
|
+
reconnectTimer = scheduleTimeout(() => void connect(), delay);
|
|
453
|
+
};
|
|
454
|
+
const connect = async () => {
|
|
455
|
+
if (stopped) return;
|
|
456
|
+
if (!getIsLeader()) {
|
|
457
|
+
stop();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
let opened;
|
|
461
|
+
try {
|
|
462
|
+
opened = await source({
|
|
463
|
+
onSnapshot: (snapshot) => {
|
|
464
|
+
agents = new Map(snapshot.map((agent) => [agent.userId, agent]));
|
|
465
|
+
onAgentsChanged([...agents.values()]);
|
|
466
|
+
},
|
|
467
|
+
onDelta: (agent) => {
|
|
468
|
+
agents.set(agent.userId, agent);
|
|
469
|
+
onAgentsChanged([...agents.values()]);
|
|
470
|
+
},
|
|
471
|
+
onClose: () => {
|
|
472
|
+
if (connection === null) return;
|
|
473
|
+
connection = null;
|
|
474
|
+
scheduleReconnect();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
} catch {
|
|
478
|
+
if (!stopped) scheduleReconnect();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (stopped) {
|
|
482
|
+
if (byeOnArrival) opened.bye?.();
|
|
483
|
+
opened.close();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
connection = opened;
|
|
487
|
+
attempt = 0;
|
|
488
|
+
if (lastReportedOnCall !== null) connection.reportCallState(lastReportedOnCall);
|
|
489
|
+
};
|
|
490
|
+
void connect();
|
|
491
|
+
return {
|
|
492
|
+
reportCallState: (onCall) => {
|
|
493
|
+
if (onCall === lastReportedOnCall) return;
|
|
494
|
+
lastReportedOnCall = onCall;
|
|
495
|
+
connection?.reportCallState(onCall);
|
|
496
|
+
},
|
|
497
|
+
stop
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
318
501
|
// src/core/softphone-impl.ts
|
|
319
502
|
var liveInstance = false;
|
|
503
|
+
var PRESENCE_TEARDOWN_GRACE_MS = 1e4;
|
|
320
504
|
var BrowserAudioDevices = class {
|
|
321
505
|
inputs = [];
|
|
322
506
|
outputs = [];
|
|
@@ -377,6 +561,23 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
377
561
|
/** Contacts + history, whenever there is a Coventia backend to reach. The widget reads these. */
|
|
378
562
|
sources;
|
|
379
563
|
audio;
|
|
564
|
+
/**
|
|
565
|
+
* Every in-flight transfer-outcome watch (Phase A), so `destroy()` can stop all of them. Never
|
|
566
|
+
* pruned as individual watches finish on their own — `stop()` is idempotent and cheap to call again,
|
|
567
|
+
* and a session accumulating a handful of closures for however many transfers it makes is not worth
|
|
568
|
+
* the bookkeeping a self-removing set would add.
|
|
569
|
+
*/
|
|
570
|
+
#activeWatches = /* @__PURE__ */ new Set();
|
|
571
|
+
/**
|
|
572
|
+
* Phase B (presence-gated transfer picker). ONE long-lived watch, not a member of `#activeWatches`
|
|
573
|
+
* — that set is per-transfer instances a live call spawns and forgets; this is a single background
|
|
574
|
+
* connection tied to the softphone's own lifecycle, started and stopped by `#syncPresenceWatch`
|
|
575
|
+
* rather than by any one call.
|
|
576
|
+
*/
|
|
577
|
+
#presenceWatch = null;
|
|
578
|
+
/** A pending state-driven teardown of the watch — see `PRESENCE_TEARDOWN_GRACE_MS`. */
|
|
579
|
+
#presenceTeardownTimer = null;
|
|
580
|
+
#presenceAgents = [];
|
|
380
581
|
/**
|
|
381
582
|
* `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
|
|
382
583
|
* internal seam, not something an integrator tunes). Tests construct this class directly with a
|
|
@@ -409,6 +610,9 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
409
610
|
get activeCall() {
|
|
410
611
|
return this.#activeCall;
|
|
411
612
|
}
|
|
613
|
+
get presence() {
|
|
614
|
+
return this.#presenceAgents;
|
|
615
|
+
}
|
|
412
616
|
get canPlayAudio() {
|
|
413
617
|
return true;
|
|
414
618
|
}
|
|
@@ -432,6 +636,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
432
636
|
}
|
|
433
637
|
async stop() {
|
|
434
638
|
if (!this.#startedHere) return;
|
|
639
|
+
this.#sayGoodbye();
|
|
435
640
|
this.#tabLock.release();
|
|
436
641
|
if (this.#isLeader) {
|
|
437
642
|
await this.#engine.stop();
|
|
@@ -445,6 +650,9 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
445
650
|
async destroy() {
|
|
446
651
|
if (this.#destroyed) return;
|
|
447
652
|
this.#destroyed = true;
|
|
653
|
+
for (const stopWatch of this.#activeWatches) stopWatch();
|
|
654
|
+
this.#activeWatches.clear();
|
|
655
|
+
this.#sayGoodbye();
|
|
448
656
|
await this.stop().catch(() => {
|
|
449
657
|
});
|
|
450
658
|
this.#setState("destroyed");
|
|
@@ -475,14 +683,19 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
475
683
|
const call = new CallImpl(
|
|
476
684
|
engineCall,
|
|
477
685
|
(event, error) => this.#reportListenerError("call", event, error),
|
|
478
|
-
(message) => this.#reportInternalError(message)
|
|
686
|
+
(message) => this.#reportInternalError(message),
|
|
687
|
+
(target, callerNumber) => this.#onTransfer(target, callerNumber)
|
|
479
688
|
);
|
|
480
689
|
this.#activeCall = call;
|
|
690
|
+
const offState = call.on("stateChanged" /* StateChanged */, () => this.#syncCallState());
|
|
691
|
+
this.#syncCallState();
|
|
481
692
|
const off = call.on("ended" /* Ended */, () => {
|
|
482
693
|
off();
|
|
694
|
+
offState();
|
|
483
695
|
if (this.#activeCall === call) {
|
|
484
696
|
this.#activeCall = null;
|
|
485
697
|
this.emit("activeCallChanged" /* ActiveCallChanged */, { call: null });
|
|
698
|
+
this.#syncCallState();
|
|
486
699
|
}
|
|
487
700
|
});
|
|
488
701
|
if (this.#resolveContact) {
|
|
@@ -500,11 +713,125 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
500
713
|
this.#state = state;
|
|
501
714
|
if (state === "online") this.#lastError = null;
|
|
502
715
|
this.emit("stateChanged" /* StateChanged */, { state, previous });
|
|
716
|
+
this.#syncPresenceWatch();
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Whether this softphone is on a call right now, as presence means it (`isOnCallState`). There is a single
|
|
720
|
+
* active-call slot, and a consult leg is a child of that call, so the parent staying alive covers a consult.
|
|
721
|
+
*/
|
|
722
|
+
#isOnCall() {
|
|
723
|
+
return this.#activeCall !== null && isOnCallState(this.#activeCall.state);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Reports the current on-a-call value to the backend. The watch de-duplicates (it only sends on change) and
|
|
727
|
+
* remembers the latest value to replay after a reconnect, so this can be called on every transition without
|
|
728
|
+
* ceremony. A no-op while there is no presence watch (not online, not the leader, no backend configured).
|
|
729
|
+
*/
|
|
730
|
+
#syncCallState() {
|
|
731
|
+
this.#presenceWatch?.reportCallState(this.#isOnCall());
|
|
503
732
|
}
|
|
504
733
|
#setLeader(isLeader) {
|
|
505
734
|
if (isLeader === this.#isLeader) return;
|
|
506
735
|
this.#isLeader = isLeader;
|
|
507
736
|
this.emit("leaderChanged" /* LeaderChanged */, { isLeader });
|
|
737
|
+
this.#syncPresenceWatch();
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Phase B (presence-gated transfer picker, `docs/specs/transfer-presence-picker.md` plan T6/T7).
|
|
741
|
+
* The ONE place that decides whether the presence connection should be running right now — called
|
|
742
|
+
* after every `#setState`/`#setLeader` transition rather than left to either alone, since either
|
|
743
|
+
* one flipping can change the answer. No-ops entirely when no Coventia backend is configured
|
|
744
|
+
* (`sources` is null) — same guard `#onTransfer` uses, and for the same reason: nothing to open.
|
|
745
|
+
*
|
|
746
|
+
* `presence-socket.ts`'s own `getIsLeader` is checked at start and before each reconnect attempt,
|
|
747
|
+
* not on a poll tick the way `startTransferWatch`'s is — see that module's own doc comment for why.
|
|
748
|
+
* This still passes `getIsLeader: () => this.#isLeader` for that narrower, defensive recheck; the
|
|
749
|
+
* BROAD online/leader gate — whether to be connected AT ALL — is owned entirely here, the same
|
|
750
|
+
* split `#onTransfer` already has with `startTransferWatch`.
|
|
751
|
+
*
|
|
752
|
+
* One exception to "tear down the moment `online` is lost": a leader that is merely `offline` or
|
|
753
|
+
* `reconnecting` keeps its watch for `PRESENCE_TEARDOWN_GRACE_MS` — a blip shorter than that never touches
|
|
754
|
+
* the presence socket at all. Anything else (stopped, starting, destroyed, or no longer the leader) still
|
|
755
|
+
* tears it down at once: those are not blips, and a follower must never hold a connection.
|
|
756
|
+
*/
|
|
757
|
+
#syncPresenceWatch() {
|
|
758
|
+
const eligible = Boolean(this.sources) && this.#isLeader;
|
|
759
|
+
const shouldRun = eligible && this.#state === "online";
|
|
760
|
+
const blip = eligible && (this.#state === "offline" || this.#state === "reconnecting");
|
|
761
|
+
if (!blip) this.#cancelPresenceTeardown();
|
|
762
|
+
if (blip && this.#presenceWatch !== null) {
|
|
763
|
+
this.#schedulePresenceTeardown();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (shouldRun === (this.#presenceWatch !== null)) return;
|
|
767
|
+
if (shouldRun) {
|
|
768
|
+
this.#presenceWatch = startPresenceSocket({
|
|
769
|
+
source: this.sources.presence,
|
|
770
|
+
getIsLeader: () => this.#isLeader,
|
|
771
|
+
onAgentsChanged: (agents) => {
|
|
772
|
+
this.#presenceAgents = agents;
|
|
773
|
+
this.emit("presenceChanged" /* PresenceChanged */, { agents });
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
this.#syncCallState();
|
|
777
|
+
} else {
|
|
778
|
+
this.#stopPresence();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Starts the grace period before an `offline` / `reconnecting` leader loses its presence socket. Idempotent
|
|
783
|
+
* on purpose: a second notification inside the same blip must NOT push the deadline back.
|
|
784
|
+
*/
|
|
785
|
+
#schedulePresenceTeardown() {
|
|
786
|
+
if (this.#presenceTeardownTimer !== null) return;
|
|
787
|
+
this.#presenceTeardownTimer = setTimeout(() => {
|
|
788
|
+
this.#presenceTeardownTimer = null;
|
|
789
|
+
if (this.#state === "online" && this.#isLeader) return;
|
|
790
|
+
this.#stopPresence();
|
|
791
|
+
}, PRESENCE_TEARDOWN_GRACE_MS);
|
|
792
|
+
}
|
|
793
|
+
#cancelPresenceTeardown() {
|
|
794
|
+
if (this.#presenceTeardownTimer === null) return;
|
|
795
|
+
clearTimeout(this.#presenceTeardownTimer);
|
|
796
|
+
this.#presenceTeardownTimer = null;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Stops the presence watch and forgets its roster. A STATE-DRIVEN teardown (the softphone left `online`
|
|
800
|
+
* for good, or lost leadership) passes nothing and says no goodbye; only a deliberate stop passes `bye`.
|
|
801
|
+
* Any pending grace-period teardown is cancelled — there is nothing left for it to do.
|
|
802
|
+
*/
|
|
803
|
+
#stopPresence(options) {
|
|
804
|
+
this.#cancelPresenceTeardown();
|
|
805
|
+
if (!this.#presenceWatch) return;
|
|
806
|
+
this.#presenceWatch.stop(options);
|
|
807
|
+
this.#presenceWatch = null;
|
|
808
|
+
this.#presenceAgents = [];
|
|
809
|
+
this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* A DELIBERATE stop (`stop()` / `destroy()`): say any call is over, then say goodbye, so the backend drops
|
|
813
|
+
* this phone at once instead of waiting out the close grace. Idempotent — a second call finds no watch.
|
|
814
|
+
*/
|
|
815
|
+
#sayGoodbye() {
|
|
816
|
+
this.#presenceWatch?.reportCallState(false);
|
|
817
|
+
this.#stopPresence({ bye: true });
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Phase A (transfer outcome relay, `docs/specs/transfer-outcome-relay.md`) — `CallImpl.transfer()`'s
|
|
821
|
+
* internal seam calls this. No-ops entirely when no Coventia backend is configured (`sources` is
|
|
822
|
+
* null): there is then nothing to poll, and behaviour is exactly what it was before this feature
|
|
823
|
+
* existed (spec §5.4).
|
|
824
|
+
*/
|
|
825
|
+
#onTransfer(target, callerNumber) {
|
|
826
|
+
if (!this.sources) return;
|
|
827
|
+
const stopWatch = startTransferWatch({
|
|
828
|
+
target,
|
|
829
|
+
callerNumber,
|
|
830
|
+
source: this.sources.transferOutcomes,
|
|
831
|
+
getIsLeader: () => this.#isLeader,
|
|
832
|
+
emit: (payload) => this.emit("transferOutcome" /* TransferOutcome */, payload)
|
|
833
|
+
});
|
|
834
|
+
this.#activeWatches.add(stopWatch);
|
|
508
835
|
}
|
|
509
836
|
/**
|
|
510
837
|
* Re-emit an engine failure to the host, and remember it if it is fatal.
|
|
@@ -563,7 +890,7 @@ function defaultZadarmaEngine(config) {
|
|
|
563
890
|
const stateChange = new LazyRelay();
|
|
564
891
|
const error = new LazyRelay();
|
|
565
892
|
function load() {
|
|
566
|
-
pending ??= import('./zadarma-engine-
|
|
893
|
+
pending ??= import('./zadarma-engine-L5DEFXUS.js').then(({ createZadarmaEngine }) => {
|
|
567
894
|
const engine = createZadarmaEngine(config);
|
|
568
895
|
incoming.attach({ on: (l) => engine.onIncoming(l) });
|
|
569
896
|
stateChange.attach({ on: (l) => engine.onStateChange(l) });
|
|
@@ -623,7 +950,7 @@ function isCallEngine(value) {
|
|
|
623
950
|
}
|
|
624
951
|
|
|
625
952
|
// src/index.ts
|
|
626
|
-
var VERSION = "0.
|
|
953
|
+
var VERSION = "0.2.1";
|
|
627
954
|
|
|
628
955
|
export { CALL_TRANSITIONS, INITIAL_STATES, VERSION, canTransition, createSoftphone, isTerminal };
|
|
629
956
|
//# sourceMappingURL=index.js.map
|