@coventia/softphone 0.2.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-DUVnDxsz.d.cts → call-VKq1I8yP.d.cts} +6 -0
- package/dist/{call-DUVnDxsz.d.ts → call-VKq1I8yP.d.ts} +6 -0
- package/dist/{chunk-YWI6EQYS.js → chunk-EYEDPD7Y.js} +108 -15
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/element/index.d.ts +8 -2
- package/dist/element/index.js +334 -249
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-CDjkrXHY.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
- package/dist/{engine-EPCKf-Bl.d.ts → engine-C0BOkIcW.d.ts} +1 -1
- package/dist/index.cjs +199 -23
- 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 +90 -11
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/{softphone-BtdgZWkm.d.ts → softphone-Cyq3hiIM.d.ts} +1 -1
- package/dist/{softphone-DHG9MTNg.d.cts → softphone-DlGV12Xp.d.cts} +1 -1
- package/dist/softphone.css +82 -34
- package/dist/{sources-5L2R4HTV.js → sources-IRKNQPWW.js} +6 -2
- package/dist/sources-IRKNQPWW.js.map +1 -0
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/ui/index.cjs +332 -247
- 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 +332 -247
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +106 -13
- 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-4VOPIBXG.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-YWI6EQYS.js.map +0 -1
- package/dist/sources-5L2R4HTV.js.map +0 -1
- package/dist/zadarma-engine-4VOPIBXG.js +0 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-
|
|
1
|
+
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-VKq1I8yP.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-
|
|
1
|
+
import { e as CallOptions, b as CallDirection, H as HangupReason, i as HoldOriginator, o as SoftphoneState, k as SoftphoneError } from './call-VKq1I8yP.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The `CallEngine` seam — INTERNAL, not exported from src/index.ts.
|
package/dist/index.cjs
CHANGED
|
@@ -293,6 +293,10 @@ function coventiaSources(config) {
|
|
|
293
293
|
if (ws.readyState !== WS.OPEN) return;
|
|
294
294
|
ws.send(JSON.stringify({ type: "call-state", onCall }));
|
|
295
295
|
},
|
|
296
|
+
bye: () => {
|
|
297
|
+
if (ws.readyState !== WS.OPEN) return;
|
|
298
|
+
ws.send(JSON.stringify({ type: "bye" }));
|
|
299
|
+
},
|
|
296
300
|
close: () => {
|
|
297
301
|
ws.onclose = null;
|
|
298
302
|
ws.onerror = null;
|
|
@@ -1222,11 +1226,13 @@ function buildPushUrl(key, sip) {
|
|
|
1222
1226
|
const params = new URLSearchParams({ key, integrationType: "site", sipId: sip });
|
|
1223
1227
|
return `wss://ws.zadarma.com/webrtc_pushes?${params.toString()}`;
|
|
1224
1228
|
}
|
|
1225
|
-
var PING, KEY_REFRESH_BUFFER_MS, ZADARMA_QUIRKS, ZadarmaPushSignal, ZadarmaProvider;
|
|
1229
|
+
var PING, KEY_REFRESH_BUFFER_MS, REBUILD_CONNECT_TIMEOUT_MS, REBUILD_RETRY_MS, ZADARMA_QUIRKS, ZadarmaPushSignal, ZadarmaProvider;
|
|
1226
1230
|
var init_zadarma = __esm({
|
|
1227
1231
|
"src/transports/providers/zadarma.ts"() {
|
|
1228
1232
|
PING = { localMs: 3e4, globalMs: 6e4 };
|
|
1229
1233
|
KEY_REFRESH_BUFFER_MS = 2 * 60 * 60 * 1e3;
|
|
1234
|
+
REBUILD_CONNECT_TIMEOUT_MS = 1e4;
|
|
1235
|
+
REBUILD_RETRY_MS = 3e4;
|
|
1230
1236
|
ZADARMA_QUIRKS = {
|
|
1231
1237
|
// Zadarma's registrar REFUSES to register a WebRTC client with any other User-Agent — a clean 401
|
|
1232
1238
|
// after a valid digest challenge, indistinguishable from a wrong password (§3.1). The old CRM
|
|
@@ -1250,6 +1256,12 @@ var init_zadarma = __esm({
|
|
|
1250
1256
|
#connected = false;
|
|
1251
1257
|
#pingTimers = [];
|
|
1252
1258
|
#refreshTimer;
|
|
1259
|
+
/** The replacement socket of a rebuild in flight, not yet current. */
|
|
1260
|
+
#candidate = null;
|
|
1261
|
+
/** Abandons the rebuild in flight (closing its candidate). */
|
|
1262
|
+
#abortRebuild = null;
|
|
1263
|
+
#rebuilding = false;
|
|
1264
|
+
#retryTimer;
|
|
1253
1265
|
#unwatchBackground;
|
|
1254
1266
|
#closed = false;
|
|
1255
1267
|
#listeners = /* @__PURE__ */ new Set();
|
|
@@ -1265,20 +1277,35 @@ var init_zadarma = __esm({
|
|
|
1265
1277
|
if (this.#closed) return;
|
|
1266
1278
|
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
1267
1279
|
this.#socket = socket;
|
|
1280
|
+
this.#attach(socket);
|
|
1281
|
+
this.#armRefresh(keyExpiresAt);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Wires one socket's events. `this.#socket` is read when an event FIRES, not when it is wired: a socket that
|
|
1285
|
+
* has been replaced must stop driving connectivity (our own `close()` of it emits `disconnect`, which would
|
|
1286
|
+
* otherwise flip the state to offline right after a perfectly healthy swap). `update` is the exception — it is
|
|
1287
|
+
* forwarded from any socket not yet closed, because a push seen twice is harmless (the engine ignores a
|
|
1288
|
+
* repeated signal mid-call and reuses a UA that already exists) while one seen by neither is a missed call.
|
|
1289
|
+
*
|
|
1290
|
+
* With `hooks`, the socket is a rebuild's candidate: its first `connect` or failure goes to the rebuild instead
|
|
1291
|
+
* of changing the connection state (it is not current yet).
|
|
1292
|
+
*/
|
|
1293
|
+
#attach(socket, hooks) {
|
|
1268
1294
|
socket.on("connect", () => {
|
|
1295
|
+
if (hooks && socket === this.#candidate) return hooks.onConnect();
|
|
1296
|
+
if (socket !== this.#socket) return;
|
|
1269
1297
|
this.#setConnected(true);
|
|
1270
1298
|
this.#startPings(socket);
|
|
1271
1299
|
});
|
|
1272
1300
|
socket.on("update", () => this.#emit());
|
|
1273
|
-
|
|
1274
|
-
this.#
|
|
1275
|
-
this.#
|
|
1276
|
-
});
|
|
1277
|
-
socket.on("connect_error", () => {
|
|
1301
|
+
const lost = () => {
|
|
1302
|
+
if (hooks && socket === this.#candidate) return hooks.onFailure();
|
|
1303
|
+
if (socket !== this.#socket) return;
|
|
1278
1304
|
this.#stopPings();
|
|
1279
1305
|
this.#setConnected(false);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1306
|
+
};
|
|
1307
|
+
socket.on("disconnect", lost);
|
|
1308
|
+
socket.on("connect_error", lost);
|
|
1282
1309
|
}
|
|
1283
1310
|
/**
|
|
1284
1311
|
* Presence pings — **the reason an idle tab keeps receiving calls**.
|
|
@@ -1317,22 +1344,92 @@ var init_zadarma = __esm({
|
|
|
1317
1344
|
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
1318
1345
|
this.#refreshTimer = setTimeout(() => void this.#reopen(), ms);
|
|
1319
1346
|
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Replaces the push socket with a fresh one — MAKE-BEFORE-BREAK (presence-client-reporting.md §4.1, T8).
|
|
1349
|
+
*
|
|
1350
|
+
* Why it must happen at all: Zadarma's gateway decides the extension is alive from the raw Engine.IO ping, which
|
|
1351
|
+
* a hidden tab's throttled timers silently stop sending, so while the tab is hidden the socket is rebuilt every
|
|
1352
|
+
* 20 s (`push-socket-reliability.md` §5.3), and once before the key expires. Why it must not leave a gap: the
|
|
1353
|
+
* old close-then-reopen reported `connected = false` for the ~0.5 s in between, which took the softphone
|
|
1354
|
+
* `offline`, tore down the presence socket and left Zadarma with no socket to push an incoming call to.
|
|
1355
|
+
*
|
|
1356
|
+
* Now the OLD socket stays up and stays current until the NEW one has actually connected; only then do we swap,
|
|
1357
|
+
* move the pings and close the old one. A healthy rebuild never changes `connected`. A failed one — the key fetch
|
|
1358
|
+
* throws, the socket errors, or it has not connected within `REBUILD_CONNECT_TIMEOUT_MS` — keeps the old socket
|
|
1359
|
+
* and retries, instead of leaving the tab disconnected. Zadarma does not evict the older of two sockets for one
|
|
1360
|
+
* sip (multi-tab-leader-election.md §1), so the overlap is safe.
|
|
1361
|
+
*/
|
|
1320
1362
|
async #reopen() {
|
|
1321
|
-
if (this.#closed) return;
|
|
1322
|
-
this.#
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1363
|
+
if (this.#closed || this.#rebuilding) return;
|
|
1364
|
+
if (!this.#socket) {
|
|
1365
|
+
try {
|
|
1366
|
+
await this.#open();
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
this.#rebuilding = true;
|
|
1326
1372
|
try {
|
|
1327
|
-
await this
|
|
1373
|
+
const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
|
|
1374
|
+
if (this.#closed) return;
|
|
1375
|
+
const swapped = await new Promise((resolve) => {
|
|
1376
|
+
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
1377
|
+
this.#candidate = socket;
|
|
1378
|
+
let settled = false;
|
|
1379
|
+
const finish = (ok) => {
|
|
1380
|
+
if (settled) return;
|
|
1381
|
+
settled = true;
|
|
1382
|
+
clearTimeout(timer);
|
|
1383
|
+
this.#abortRebuild = null;
|
|
1384
|
+
if (ok && socket.connected) {
|
|
1385
|
+
this.#swapIn(socket, keyExpiresAt);
|
|
1386
|
+
} else {
|
|
1387
|
+
this.#candidate = null;
|
|
1388
|
+
socket.close();
|
|
1389
|
+
}
|
|
1390
|
+
resolve(ok && socket.connected);
|
|
1391
|
+
};
|
|
1392
|
+
const timer = setTimeout(() => finish(false), REBUILD_CONNECT_TIMEOUT_MS);
|
|
1393
|
+
this.#abortRebuild = () => finish(false);
|
|
1394
|
+
this.#attach(socket, { onConnect: () => finish(true), onFailure: () => finish(false) });
|
|
1395
|
+
});
|
|
1396
|
+
if (!swapped) this.#armRetry();
|
|
1328
1397
|
} catch {
|
|
1398
|
+
this.#armRetry();
|
|
1399
|
+
} finally {
|
|
1400
|
+
this.#rebuilding = false;
|
|
1329
1401
|
}
|
|
1330
1402
|
}
|
|
1403
|
+
/**
|
|
1404
|
+
* The moment of the swap — synchronous, called from the candidate's own `connect` event so nothing can run
|
|
1405
|
+
* between "the new socket is up" and "it is the current one". Order matters: `#socket` moves first, so the old
|
|
1406
|
+
* socket's `disconnect` (our own `close()` of it, just below) is ignored by `#attach`.
|
|
1407
|
+
*/
|
|
1408
|
+
#swapIn(socket, keyExpiresAt) {
|
|
1409
|
+
const previous = this.#socket;
|
|
1410
|
+
this.#socket = socket;
|
|
1411
|
+
this.#candidate = null;
|
|
1412
|
+
clearTimeout(this.#retryTimer);
|
|
1413
|
+
this.#retryTimer = void 0;
|
|
1414
|
+
this.#stopPings();
|
|
1415
|
+
this.#startPings(socket);
|
|
1416
|
+
this.#setConnected(true);
|
|
1417
|
+
previous?.close();
|
|
1418
|
+
this.#armRefresh(keyExpiresAt);
|
|
1419
|
+
}
|
|
1420
|
+
#armRetry() {
|
|
1421
|
+
if (this.#closed) return;
|
|
1422
|
+
clearTimeout(this.#retryTimer);
|
|
1423
|
+
this.#retryTimer = setTimeout(() => void this.#reopen(), REBUILD_RETRY_MS);
|
|
1424
|
+
}
|
|
1331
1425
|
disconnect() {
|
|
1332
1426
|
this.#closed = true;
|
|
1333
1427
|
this.#stopPings();
|
|
1334
1428
|
clearTimeout(this.#refreshTimer);
|
|
1335
1429
|
this.#refreshTimer = void 0;
|
|
1430
|
+
clearTimeout(this.#retryTimer);
|
|
1431
|
+
this.#retryTimer = void 0;
|
|
1432
|
+
this.#abortRebuild?.();
|
|
1336
1433
|
this.#unwatchBackground?.();
|
|
1337
1434
|
this.#unwatchBackground = void 0;
|
|
1338
1435
|
this.#socket?.close();
|
|
@@ -1689,6 +1786,9 @@ function canTransition(from, to) {
|
|
|
1689
1786
|
function isTerminal(state) {
|
|
1690
1787
|
return CALL_TRANSITIONS[state].length === 0;
|
|
1691
1788
|
}
|
|
1789
|
+
function isOnCallState(state) {
|
|
1790
|
+
return state !== "ringing" && !isTerminal(state);
|
|
1791
|
+
}
|
|
1692
1792
|
|
|
1693
1793
|
// src/core/call-impl.ts
|
|
1694
1794
|
var CallImpl = class _CallImpl extends Emitter {
|
|
@@ -2051,13 +2151,18 @@ function startPresenceSocket(params) {
|
|
|
2051
2151
|
let agents = /* @__PURE__ */ new Map();
|
|
2052
2152
|
let lastReportedOnCall = null;
|
|
2053
2153
|
let connection = null;
|
|
2054
|
-
|
|
2154
|
+
let byeOnArrival = false;
|
|
2155
|
+
const stop = (options) => {
|
|
2055
2156
|
if (stopped) return;
|
|
2056
2157
|
stopped = true;
|
|
2057
2158
|
if (reconnectTimer !== null) {
|
|
2058
2159
|
cancelTimeout(reconnectTimer);
|
|
2059
2160
|
reconnectTimer = null;
|
|
2060
2161
|
}
|
|
2162
|
+
if (options?.bye) {
|
|
2163
|
+
byeOnArrival = true;
|
|
2164
|
+
connection?.bye?.();
|
|
2165
|
+
}
|
|
2061
2166
|
connection?.close();
|
|
2062
2167
|
connection = null;
|
|
2063
2168
|
};
|
|
@@ -2096,6 +2201,7 @@ function startPresenceSocket(params) {
|
|
|
2096
2201
|
return;
|
|
2097
2202
|
}
|
|
2098
2203
|
if (stopped) {
|
|
2204
|
+
if (byeOnArrival) opened.bye?.();
|
|
2099
2205
|
opened.close();
|
|
2100
2206
|
return;
|
|
2101
2207
|
}
|
|
@@ -2117,6 +2223,7 @@ function startPresenceSocket(params) {
|
|
|
2117
2223
|
// src/core/softphone-impl.ts
|
|
2118
2224
|
init_errors();
|
|
2119
2225
|
var liveInstance = false;
|
|
2226
|
+
var PRESENCE_TEARDOWN_GRACE_MS = 1e4;
|
|
2120
2227
|
var BrowserAudioDevices = class {
|
|
2121
2228
|
inputs = [];
|
|
2122
2229
|
outputs = [];
|
|
@@ -2191,6 +2298,8 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2191
2298
|
* rather than by any one call.
|
|
2192
2299
|
*/
|
|
2193
2300
|
#presenceWatch = null;
|
|
2301
|
+
/** A pending state-driven teardown of the watch — see `PRESENCE_TEARDOWN_GRACE_MS`. */
|
|
2302
|
+
#presenceTeardownTimer = null;
|
|
2194
2303
|
#presenceAgents = [];
|
|
2195
2304
|
/**
|
|
2196
2305
|
* `tabLock` defaults to the real Web Locks implementation — not part of `SoftphoneConfig` (an
|
|
@@ -2250,6 +2359,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2250
2359
|
}
|
|
2251
2360
|
async stop() {
|
|
2252
2361
|
if (!this.#startedHere) return;
|
|
2362
|
+
this.#sayGoodbye();
|
|
2253
2363
|
this.#tabLock.release();
|
|
2254
2364
|
if (this.#isLeader) {
|
|
2255
2365
|
await this.#engine.stop();
|
|
@@ -2265,8 +2375,7 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2265
2375
|
this.#destroyed = true;
|
|
2266
2376
|
for (const stopWatch of this.#activeWatches) stopWatch();
|
|
2267
2377
|
this.#activeWatches.clear();
|
|
2268
|
-
this.#
|
|
2269
|
-
this.#presenceWatch = null;
|
|
2378
|
+
this.#sayGoodbye();
|
|
2270
2379
|
await this.stop().catch(() => {
|
|
2271
2380
|
});
|
|
2272
2381
|
this.#setState("destroyed");
|
|
@@ -2301,11 +2410,15 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2301
2410
|
(target, callerNumber) => this.#onTransfer(target, callerNumber)
|
|
2302
2411
|
);
|
|
2303
2412
|
this.#activeCall = call;
|
|
2413
|
+
const offState = call.on("stateChanged" /* StateChanged */, () => this.#syncCallState());
|
|
2414
|
+
this.#syncCallState();
|
|
2304
2415
|
const off = call.on("ended" /* Ended */, () => {
|
|
2305
2416
|
off();
|
|
2417
|
+
offState();
|
|
2306
2418
|
if (this.#activeCall === call) {
|
|
2307
2419
|
this.#activeCall = null;
|
|
2308
2420
|
this.emit("activeCallChanged" /* ActiveCallChanged */, { call: null });
|
|
2421
|
+
this.#syncCallState();
|
|
2309
2422
|
}
|
|
2310
2423
|
});
|
|
2311
2424
|
if (this.#resolveContact) {
|
|
@@ -2325,6 +2438,21 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2325
2438
|
this.emit("stateChanged" /* StateChanged */, { state, previous });
|
|
2326
2439
|
this.#syncPresenceWatch();
|
|
2327
2440
|
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Whether this softphone is on a call right now, as presence means it (`isOnCallState`). There is a single
|
|
2443
|
+
* active-call slot, and a consult leg is a child of that call, so the parent staying alive covers a consult.
|
|
2444
|
+
*/
|
|
2445
|
+
#isOnCall() {
|
|
2446
|
+
return this.#activeCall !== null && isOnCallState(this.#activeCall.state);
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Reports the current on-a-call value to the backend. The watch de-duplicates (it only sends on change) and
|
|
2450
|
+
* remembers the latest value to replay after a reconnect, so this can be called on every transition without
|
|
2451
|
+
* ceremony. A no-op while there is no presence watch (not online, not the leader, no backend configured).
|
|
2452
|
+
*/
|
|
2453
|
+
#syncCallState() {
|
|
2454
|
+
this.#presenceWatch?.reportCallState(this.#isOnCall());
|
|
2455
|
+
}
|
|
2328
2456
|
#setLeader(isLeader) {
|
|
2329
2457
|
if (isLeader === this.#isLeader) return;
|
|
2330
2458
|
this.#isLeader = isLeader;
|
|
@@ -2343,9 +2471,21 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2343
2471
|
* This still passes `getIsLeader: () => this.#isLeader` for that narrower, defensive recheck; the
|
|
2344
2472
|
* BROAD online/leader gate — whether to be connected AT ALL — is owned entirely here, the same
|
|
2345
2473
|
* split `#onTransfer` already has with `startTransferWatch`.
|
|
2474
|
+
*
|
|
2475
|
+
* One exception to "tear down the moment `online` is lost": a leader that is merely `offline` or
|
|
2476
|
+
* `reconnecting` keeps its watch for `PRESENCE_TEARDOWN_GRACE_MS` — a blip shorter than that never touches
|
|
2477
|
+
* the presence socket at all. Anything else (stopped, starting, destroyed, or no longer the leader) still
|
|
2478
|
+
* tears it down at once: those are not blips, and a follower must never hold a connection.
|
|
2346
2479
|
*/
|
|
2347
2480
|
#syncPresenceWatch() {
|
|
2348
|
-
const
|
|
2481
|
+
const eligible = Boolean(this.sources) && this.#isLeader;
|
|
2482
|
+
const shouldRun = eligible && this.#state === "online";
|
|
2483
|
+
const blip = eligible && (this.#state === "offline" || this.#state === "reconnecting");
|
|
2484
|
+
if (!blip) this.#cancelPresenceTeardown();
|
|
2485
|
+
if (blip && this.#presenceWatch !== null) {
|
|
2486
|
+
this.#schedulePresenceTeardown();
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2349
2489
|
if (shouldRun === (this.#presenceWatch !== null)) return;
|
|
2350
2490
|
if (shouldRun) {
|
|
2351
2491
|
this.#presenceWatch = startPresenceSocket({
|
|
@@ -2356,13 +2496,49 @@ var SoftphoneImpl = class extends Emitter {
|
|
|
2356
2496
|
this.emit("presenceChanged" /* PresenceChanged */, { agents });
|
|
2357
2497
|
}
|
|
2358
2498
|
});
|
|
2499
|
+
this.#syncCallState();
|
|
2359
2500
|
} else {
|
|
2360
|
-
this.#
|
|
2361
|
-
this.#presenceWatch = null;
|
|
2362
|
-
this.#presenceAgents = [];
|
|
2363
|
-
this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
|
|
2501
|
+
this.#stopPresence();
|
|
2364
2502
|
}
|
|
2365
2503
|
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Starts the grace period before an `offline` / `reconnecting` leader loses its presence socket. Idempotent
|
|
2506
|
+
* on purpose: a second notification inside the same blip must NOT push the deadline back.
|
|
2507
|
+
*/
|
|
2508
|
+
#schedulePresenceTeardown() {
|
|
2509
|
+
if (this.#presenceTeardownTimer !== null) return;
|
|
2510
|
+
this.#presenceTeardownTimer = setTimeout(() => {
|
|
2511
|
+
this.#presenceTeardownTimer = null;
|
|
2512
|
+
if (this.#state === "online" && this.#isLeader) return;
|
|
2513
|
+
this.#stopPresence();
|
|
2514
|
+
}, PRESENCE_TEARDOWN_GRACE_MS);
|
|
2515
|
+
}
|
|
2516
|
+
#cancelPresenceTeardown() {
|
|
2517
|
+
if (this.#presenceTeardownTimer === null) return;
|
|
2518
|
+
clearTimeout(this.#presenceTeardownTimer);
|
|
2519
|
+
this.#presenceTeardownTimer = null;
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Stops the presence watch and forgets its roster. A STATE-DRIVEN teardown (the softphone left `online`
|
|
2523
|
+
* for good, or lost leadership) passes nothing and says no goodbye; only a deliberate stop passes `bye`.
|
|
2524
|
+
* Any pending grace-period teardown is cancelled — there is nothing left for it to do.
|
|
2525
|
+
*/
|
|
2526
|
+
#stopPresence(options) {
|
|
2527
|
+
this.#cancelPresenceTeardown();
|
|
2528
|
+
if (!this.#presenceWatch) return;
|
|
2529
|
+
this.#presenceWatch.stop(options);
|
|
2530
|
+
this.#presenceWatch = null;
|
|
2531
|
+
this.#presenceAgents = [];
|
|
2532
|
+
this.emit("presenceChanged" /* PresenceChanged */, { agents: [] });
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* A DELIBERATE stop (`stop()` / `destroy()`): say any call is over, then say goodbye, so the backend drops
|
|
2536
|
+
* this phone at once instead of waiting out the close grace. Idempotent — a second call finds no watch.
|
|
2537
|
+
*/
|
|
2538
|
+
#sayGoodbye() {
|
|
2539
|
+
this.#presenceWatch?.reportCallState(false);
|
|
2540
|
+
this.#stopPresence({ bye: true });
|
|
2541
|
+
}
|
|
2366
2542
|
/**
|
|
2367
2543
|
* Phase A (transfer outcome relay, `docs/specs/transfer-outcome-relay.md`) — `CallImpl.transfer()`'s
|
|
2368
2544
|
* internal seam calls this. No-ops entirely when no Coventia backend is configured (`sources` is
|
|
@@ -2498,7 +2674,7 @@ function isCallEngine(value) {
|
|
|
2498
2674
|
|
|
2499
2675
|
// src/index.ts
|
|
2500
2676
|
init_errors();
|
|
2501
|
-
var VERSION = "0.2.
|
|
2677
|
+
var VERSION = "0.2.1";
|
|
2502
2678
|
|
|
2503
2679
|
exports.CALL_TRANSITIONS = CALL_TRANSITIONS;
|
|
2504
2680
|
exports.CallEvent = CallEvent;
|