farvex 1.0.1 → 2.0.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.
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var libphonenumberJs = require('libphonenumber-js');
3
4
  var centrifuge = require('centrifuge');
4
5
 
5
6
  // src/api/generated/core/bodySerializer.gen.ts
@@ -804,7 +805,7 @@ var apiClient = (config) => {
804
805
  var unwrap = async (result, auth) => {
805
806
  const response = await result;
806
807
  if (response.error !== void 0) {
807
- if (response.response?.status === 401) {
808
+ if (response.response?.status === 401 || response.response?.status === 403) {
808
809
  await auth?.onUnauthorized?.();
809
810
  }
810
811
  throw errorFrom(response.error, response.response?.status);
@@ -943,9 +944,23 @@ var createEmitter = () => {
943
944
  emit
944
945
  };
945
946
  };
946
- var createRealtime = (client2, auth, store, emitter) => {
947
+ var normalizeE164Phone = (phone, defaultCountry = "NG") => {
948
+ const trimmed = phone.trim();
949
+ const parsed = libphonenumberJs.parsePhoneNumberFromString(trimmed);
950
+ if (parsed?.isValid() && parsed.country) {
951
+ return parsed.number;
952
+ }
953
+ const fallback = libphonenumberJs.parsePhoneNumberFromString(trimmed, defaultCountry);
954
+ if (fallback?.isValid()) {
955
+ return fallback.number;
956
+ }
957
+ throw new Error("Enter a valid phone number");
958
+ };
959
+ var createRealtime = (client2, auth, store, emitter, recover) => {
947
960
  let rt = null;
961
+ let connectPromise = null;
948
962
  let disposed = false;
963
+ let attempt = 0;
949
964
  const fetchToken = async () => unwrap(getRealtimeToken({ client: client2 }), auth);
950
965
  const refreshToken = async () => {
951
966
  try {
@@ -982,6 +997,8 @@ var createRealtime = (client2, auth, store, emitter) => {
982
997
  emitter.emit("status", status);
983
998
  };
984
999
  const disconnect = async () => {
1000
+ attempt += 1;
1001
+ connectPromise = null;
985
1002
  rt?.disconnect();
986
1003
  rt = null;
987
1004
  if (!disposed) {
@@ -990,33 +1007,50 @@ var createRealtime = (client2, auth, store, emitter) => {
990
1007
  };
991
1008
  return {
992
1009
  connect: async () => {
1010
+ if (connectPromise) {
1011
+ return connectPromise;
1012
+ }
993
1013
  if (rt) {
994
1014
  return;
995
1015
  }
996
1016
  disposed = false;
997
- setStatus("connecting");
998
- try {
1017
+ const currentAttempt = ++attempt;
1018
+ const promise = (async () => {
1019
+ setStatus("connecting");
999
1020
  const first = await fetchToken();
1000
- rt = new centrifuge.Centrifuge(first.url, {
1021
+ if (disposed || currentAttempt !== attempt) {
1022
+ return;
1023
+ }
1024
+ const next = new centrifuge.Centrifuge(first.url, {
1001
1025
  token: first.token,
1002
1026
  getToken: refreshToken
1003
1027
  });
1004
- rt.on("connecting", () => {
1005
- const next = store.getStatus() === "ready" ? "reconnecting" : "connecting";
1006
- setStatus(next);
1028
+ rt = next;
1029
+ next.on("connecting", () => {
1030
+ const status = store.getStatus() === "ready" ? "reconnecting" : "connecting";
1031
+ setStatus(status);
1007
1032
  });
1008
- rt.on("connected", () => {
1033
+ next.on("connected", () => {
1034
+ const shouldRecover = store.getStatus() === "reconnecting" || store.getStatus() === "offline";
1009
1035
  setStatus("ready");
1036
+ if (shouldRecover && recover) {
1037
+ void recover().catch((err) => {
1038
+ emitter.emit(
1039
+ "error",
1040
+ err instanceof Error ? err : new Error("Realtime recovery failed")
1041
+ );
1042
+ });
1043
+ }
1010
1044
  });
1011
- rt.on("disconnected", () => {
1045
+ next.on("disconnected", () => {
1012
1046
  if (!disposed) {
1013
1047
  setStatus("offline");
1014
1048
  }
1015
1049
  });
1016
- rt.on("error", (ctx) => {
1050
+ next.on("error", (ctx) => {
1017
1051
  emitter.emit("error", new Error(ctx.error.message));
1018
1052
  });
1019
- rt.on("publication", (ctx) => {
1053
+ next.on("publication", (ctx) => {
1020
1054
  try {
1021
1055
  const message = parseMessage(ctx.data);
1022
1056
  if (message) {
@@ -1026,16 +1060,34 @@ var createRealtime = (client2, auth, store, emitter) => {
1026
1060
  emitter.emit("error", err instanceof Error ? err : new Error("Realtime event failed"));
1027
1061
  }
1028
1062
  });
1029
- rt.connect();
1030
- await rt.ready();
1063
+ next.connect();
1064
+ await next.ready();
1065
+ if (disposed || currentAttempt !== attempt) {
1066
+ next.disconnect();
1067
+ if (rt === next) {
1068
+ rt = null;
1069
+ }
1070
+ }
1071
+ })();
1072
+ connectPromise = promise;
1073
+ try {
1074
+ await promise;
1031
1075
  } catch (err) {
1032
- await disconnect();
1076
+ if (currentAttempt === attempt) {
1077
+ await disconnect();
1078
+ }
1033
1079
  throw err;
1080
+ } finally {
1081
+ if (connectPromise === promise) {
1082
+ connectPromise = null;
1083
+ }
1034
1084
  }
1035
1085
  },
1036
1086
  disconnect,
1037
1087
  dispose: async () => {
1038
1088
  disposed = true;
1089
+ attempt += 1;
1090
+ connectPromise = null;
1039
1091
  rt?.disconnect();
1040
1092
  rt = null;
1041
1093
  setStatus("disposed");
@@ -1047,21 +1099,449 @@ var parseMessage = (value) => {
1047
1099
  return null;
1048
1100
  }
1049
1101
  const message = value;
1050
- if (message.event === "session.upsert" && message.data) {
1051
- return message;
1102
+ if (message.event === "session.upsert" && isVoiceSession(message.data)) {
1103
+ return { event: "session.upsert", data: message.data };
1052
1104
  }
1053
- if (message.event === "session.remove" && message.data) {
1054
- return message;
1105
+ if (message.event === "session.remove" && isRemovePayload(message.data)) {
1106
+ return { event: "session.remove", data: message.data };
1055
1107
  }
1056
1108
  return null;
1057
1109
  };
1110
+ var isVoiceSession = (value) => {
1111
+ if (!value || typeof value !== "object") {
1112
+ return false;
1113
+ }
1114
+ const session = value;
1115
+ return typeof session.id === "string" && typeof session.vendor === "string" && typeof session.version === "number" && (session.state === "ringing" || session.state === "active" || session.state === "ended") && Array.isArray(session.participants);
1116
+ };
1117
+ var isRemovePayload = (value) => {
1118
+ if (!value || typeof value !== "object") {
1119
+ return false;
1120
+ }
1121
+ const payload = value;
1122
+ return typeof payload.id === "string" && (payload.vendor === void 0 || typeof payload.vendor === "string") && (payload.version === void 0 || typeof payload.version === "number");
1123
+ };
1124
+
1125
+ // src/core/session.ts
1126
+ var createCurrentSessionController = (deps) => {
1127
+ const subscribers = /* @__PURE__ */ new Set();
1128
+ const cleanup = [];
1129
+ let current = null;
1130
+ let pending = null;
1131
+ let op = 0;
1132
+ let mediaConnected = false;
1133
+ let snapshot = {
1134
+ current,
1135
+ incoming: deps.store.invites(),
1136
+ busy: false
1137
+ };
1138
+ const publish = () => {
1139
+ const next = {
1140
+ current,
1141
+ incoming: deps.store.invites(),
1142
+ busy: pending !== null || isBusy(current)
1143
+ };
1144
+ if (snapshot.current === next.current && snapshot.incoming === next.incoming && snapshot.busy === next.busy) {
1145
+ return;
1146
+ }
1147
+ snapshot = next;
1148
+ for (const fn of subscribers) {
1149
+ fn();
1150
+ }
1151
+ deps.emitter.emit("session.current.changed", snapshot);
1152
+ };
1153
+ const commit = (next) => {
1154
+ const prev = current;
1155
+ current = next;
1156
+ publish();
1157
+ if (!next) {
1158
+ return;
1159
+ }
1160
+ if (!prev || prev.sessionId !== next.sessionId) {
1161
+ deps.emitter.emit("session.current.started", next);
1162
+ }
1163
+ if (prev?.phase !== "active" && next.phase === "active") {
1164
+ deps.emitter.emit("session.current.connected", next);
1165
+ }
1166
+ if (prev?.phase !== "ended" && next.phase === "ended") {
1167
+ deps.emitter.emit("session.current.ended", next);
1168
+ }
1169
+ if (prev?.phase !== "failed" && next.phase === "failed" && next.error) {
1170
+ deps.emitter.emit("session.current.failed", { session: next, error: next.error });
1171
+ }
1172
+ };
1173
+ const commitFailed = (session, err) => {
1174
+ const error = toCallpadError(err);
1175
+ if (session) {
1176
+ commit(
1177
+ fromSession(session, {
1178
+ phase: "failed",
1179
+ join: null,
1180
+ participantId: null,
1181
+ startedBy: current?.startedBy ?? "join",
1182
+ error
1183
+ })
1184
+ );
1185
+ } else {
1186
+ deps.emitter.emit("session.current.failed", { session: current, error });
1187
+ publish();
1188
+ }
1189
+ return error;
1190
+ };
1191
+ const nextOp = () => {
1192
+ op += 1;
1193
+ return op;
1194
+ };
1195
+ const run = (kind, task) => {
1196
+ if (pending) {
1197
+ if (kind === "end" && pending.kind === "end") {
1198
+ return pending.promise;
1199
+ }
1200
+ return Promise.reject(busyError());
1201
+ }
1202
+ const id = nextOp();
1203
+ const promise = task(id).finally(() => {
1204
+ if (pending?.op === id) {
1205
+ pending = null;
1206
+ publish();
1207
+ }
1208
+ });
1209
+ pending = { kind, op: id, promise };
1210
+ publish();
1211
+ return promise;
1212
+ };
1213
+ const assertFresh = (id) => {
1214
+ if (id !== op) {
1215
+ throw new CallpadError("session.command_stale", "Session command was superseded");
1216
+ }
1217
+ };
1218
+ const canReplaceCurrent = (replaceCurrent) => {
1219
+ return replaceCurrent === true || !isBusy(current);
1220
+ };
1221
+ const reconcile = () => {
1222
+ if (!current) {
1223
+ publish();
1224
+ return;
1225
+ }
1226
+ const session = deps.store.get(current.sessionId);
1227
+ if (!session) {
1228
+ mediaConnected = false;
1229
+ commit({
1230
+ ...current,
1231
+ phase: "ended",
1232
+ session: null,
1233
+ join: null,
1234
+ endedAt: current.endedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1235
+ });
1236
+ return;
1237
+ }
1238
+ const next = reconcileCurrent(current, session, mediaConnected);
1239
+ if (next !== current) {
1240
+ if (isTerminal(next.phase)) {
1241
+ mediaConnected = false;
1242
+ }
1243
+ commit(next);
1244
+ return;
1245
+ }
1246
+ publish();
1247
+ };
1248
+ cleanup.push(deps.store.subscribe("sessions", reconcile));
1249
+ cleanup.push(deps.store.subscribe("invites", publish));
1250
+ const start = (input, options = {}) => {
1251
+ const behavior = options.behavior ?? "rejectWhileBusy";
1252
+ if (behavior !== "allowParallel" && isBusy(current)) {
1253
+ if (behavior === "focusExisting") {
1254
+ const existing = findTargetSession(deps.store.list(), input.target);
1255
+ if (existing) {
1256
+ return join(existing.id, { replaceCurrent: true });
1257
+ }
1258
+ }
1259
+ return Promise.reject(busyError());
1260
+ }
1261
+ return run("start", async (id) => {
1262
+ try {
1263
+ const started = await deps.start(input);
1264
+ assertFresh(id);
1265
+ const next = fromStarted(started, "start", options.media);
1266
+ mediaConnected = false;
1267
+ commit(next);
1268
+ if (next.phase === "failed" && next.error) {
1269
+ throw next.error;
1270
+ }
1271
+ return next;
1272
+ } catch (err) {
1273
+ if (shouldRethrowCommandError(err)) {
1274
+ throw err;
1275
+ }
1276
+ throw commitFailed(null, err);
1277
+ }
1278
+ });
1279
+ };
1280
+ const accept = (input, options = {}) => {
1281
+ if (!canReplaceCurrent(options.replaceCurrent)) {
1282
+ return Promise.reject(busyError());
1283
+ }
1284
+ return run("accept", async (id) => {
1285
+ try {
1286
+ const started = await deps.accept(input);
1287
+ assertFresh(id);
1288
+ const next = fromStarted(started, "accept", options.media);
1289
+ mediaConnected = false;
1290
+ commit(next);
1291
+ if (next.phase === "failed" && next.error) {
1292
+ throw next.error;
1293
+ }
1294
+ return next;
1295
+ } catch (err) {
1296
+ if (shouldRethrowCommandError(err)) {
1297
+ throw err;
1298
+ }
1299
+ throw commitFailed(deps.store.get(input.sessionId), err);
1300
+ }
1301
+ });
1302
+ };
1303
+ const join = (sessionId, options = {}) => {
1304
+ if (!canReplaceCurrent(options.replaceCurrent)) {
1305
+ return Promise.reject(busyError());
1306
+ }
1307
+ const session = deps.store.get(sessionId);
1308
+ if (!session) {
1309
+ return Promise.reject(new CallpadError("session.not_found", "Session was not found"));
1310
+ }
1311
+ return run("join", async (id) => {
1312
+ try {
1313
+ const grant = await deps.join(sessionId);
1314
+ assertFresh(id);
1315
+ const latest = deps.store.get(sessionId) ?? session;
1316
+ const next = fromSession(latest, {
1317
+ phase: latest.state === "active" ? "active" : "connecting",
1318
+ join: grant,
1319
+ participantId: grant.participantId,
1320
+ startedBy: "join",
1321
+ error: null
1322
+ });
1323
+ mediaConnected = next.phase === "active";
1324
+ commit(next);
1325
+ return next;
1326
+ } catch (err) {
1327
+ if (shouldRethrowCommandError(err)) {
1328
+ throw err;
1329
+ }
1330
+ throw commitFailed(session, err);
1331
+ }
1332
+ });
1333
+ };
1334
+ const end = async () => {
1335
+ const target = current;
1336
+ if (!target || isTerminal(target.phase)) {
1337
+ return;
1338
+ }
1339
+ await run("end", async (id) => {
1340
+ const session = deps.store.get(target.sessionId);
1341
+ commit({ ...target, phase: "ending" });
1342
+ try {
1343
+ if (session && deps.end && deps.store.can(session, "end")) {
1344
+ await deps.end(target.sessionId);
1345
+ } else {
1346
+ await deps.leave(target.sessionId);
1347
+ }
1348
+ assertFresh(id);
1349
+ mediaConnected = false;
1350
+ const ended = deps.store.get(target.sessionId);
1351
+ commit({
1352
+ ...target,
1353
+ phase: "ended",
1354
+ session: ended,
1355
+ join: null,
1356
+ endedAt: ended?.endedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1357
+ });
1358
+ } catch (err) {
1359
+ if (shouldRethrowCommandError(err)) {
1360
+ throw err;
1361
+ }
1362
+ throw commitFailed(session, err);
1363
+ }
1364
+ });
1365
+ };
1366
+ const clear = () => {
1367
+ nextOp();
1368
+ pending = null;
1369
+ mediaConnected = false;
1370
+ commit(null);
1371
+ };
1372
+ const mediaConnectedFor = (sessionId) => {
1373
+ if (!current || sessionId && current.sessionId !== sessionId || isTerminal(current.phase)) {
1374
+ return;
1375
+ }
1376
+ mediaConnected = true;
1377
+ const session = deps.store.get(current.sessionId);
1378
+ commit({
1379
+ ...current,
1380
+ phase: "active",
1381
+ session,
1382
+ answeredAt: session?.answeredAt ?? current.answeredAt
1383
+ });
1384
+ };
1385
+ const mediaDisconnectedFor = (sessionId) => {
1386
+ if (!current || sessionId && current.sessionId !== sessionId || isTerminal(current.phase)) {
1387
+ return;
1388
+ }
1389
+ mediaConnected = false;
1390
+ const session = deps.store.get(current.sessionId);
1391
+ if (session?.state === "ended") {
1392
+ reconcile();
1393
+ return;
1394
+ }
1395
+ commit({
1396
+ ...current,
1397
+ phase: current.join ? "connecting" : current.direction === "inbound" ? "incoming" : "outgoing",
1398
+ session
1399
+ });
1400
+ };
1401
+ const mediaFailedFor = (error, sessionId) => {
1402
+ if (!current || sessionId && current.sessionId !== sessionId || isTerminal(current.phase)) {
1403
+ return;
1404
+ }
1405
+ mediaConnected = false;
1406
+ commit({
1407
+ ...current,
1408
+ phase: "failed",
1409
+ join: null,
1410
+ error: toCallpadError(error, "session.media_failed")
1411
+ });
1412
+ };
1413
+ return {
1414
+ get: () => snapshot,
1415
+ start,
1416
+ accept,
1417
+ join,
1418
+ end,
1419
+ clear,
1420
+ mediaConnected: mediaConnectedFor,
1421
+ mediaDisconnected: mediaDisconnectedFor,
1422
+ mediaFailed: mediaFailedFor,
1423
+ subscribe: (fn) => {
1424
+ subscribers.add(fn);
1425
+ return () => {
1426
+ subscribers.delete(fn);
1427
+ };
1428
+ },
1429
+ dispose: () => {
1430
+ for (const unsubscribe of cleanup) {
1431
+ unsubscribe();
1432
+ }
1433
+ subscribers.clear();
1434
+ }
1435
+ };
1436
+ };
1437
+ var fromStarted = (started, startedBy, media = "required") => {
1438
+ if (!started.join && media === "required") {
1439
+ return fromSession(started.session, {
1440
+ phase: "failed",
1441
+ join: null,
1442
+ participantId: null,
1443
+ startedBy,
1444
+ error: new CallpadError(
1445
+ "session.join_unavailable",
1446
+ "Session did not include a browser media join grant"
1447
+ )
1448
+ });
1449
+ }
1450
+ const phase = started.join ? started.session.state === "active" ? "active" : "connecting" : started.session.direction === "inbound" ? "incoming" : "outgoing";
1451
+ return fromSession(started.session, {
1452
+ phase,
1453
+ join: started.join,
1454
+ participantId: started.join?.participantId ?? null,
1455
+ startedBy,
1456
+ error: null
1457
+ });
1458
+ };
1459
+ var fromSession = (session, values) => ({
1460
+ phase: values.phase,
1461
+ direction: session.direction,
1462
+ sessionId: session.id,
1463
+ participantId: values.participantId,
1464
+ session,
1465
+ join: values.join,
1466
+ startedBy: values.startedBy,
1467
+ error: values.error,
1468
+ createdAt: session.createdAt,
1469
+ answeredAt: session.answeredAt,
1470
+ endedAt: session.endedAt
1471
+ });
1472
+ var reconcileCurrent = (current, session, mediaConnected) => {
1473
+ if (session.state === "ended") {
1474
+ return {
1475
+ ...current,
1476
+ phase: "ended",
1477
+ session,
1478
+ join: null,
1479
+ answeredAt: session.answeredAt,
1480
+ endedAt: session.endedAt ?? current.endedAt
1481
+ };
1482
+ }
1483
+ if (isTerminal(current.phase)) {
1484
+ return {
1485
+ ...current,
1486
+ session,
1487
+ answeredAt: session.answeredAt,
1488
+ endedAt: session.endedAt
1489
+ };
1490
+ }
1491
+ const phase = mediaConnected || session.state === "active" ? "active" : current.join ? "connecting" : current.phase === "ending" ? "ending" : session.direction === "inbound" ? "incoming" : "outgoing";
1492
+ if (current.phase === phase && current.session === session && current.answeredAt === session.answeredAt && current.endedAt === session.endedAt) {
1493
+ return current;
1494
+ }
1495
+ return {
1496
+ ...current,
1497
+ phase,
1498
+ session,
1499
+ answeredAt: session.answeredAt,
1500
+ endedAt: session.endedAt
1501
+ };
1502
+ };
1503
+ var findTargetSession = (sessions, target) => {
1504
+ const normalized = normalizeTarget(target);
1505
+ return sessions.find(
1506
+ (session) => session.state !== "ended" && session.participants.some((participant) => {
1507
+ if (normalized.type === "vendor_user") {
1508
+ return participant.kind === "vendor_user" && participant.userId === normalized.userId;
1509
+ }
1510
+ if (normalized.type === "customer") {
1511
+ return participant.kind === "customer" && participant.userId === normalized.userId;
1512
+ }
1513
+ if (normalized.type === "contact") {
1514
+ return participant.contactId === normalized.contactId;
1515
+ }
1516
+ return participant.phone === normalized.phone;
1517
+ })
1518
+ ) ?? null;
1519
+ };
1520
+ var normalizeTarget = (target) => {
1521
+ if (target.type === "phone") {
1522
+ return { ...target, phone: normalizeE164Phone(target.phone) };
1523
+ }
1524
+ return target;
1525
+ };
1526
+ var isBusy = (session) => session !== null && !isTerminal(session.phase);
1527
+ var isTerminal = (phase) => phase === "ended" || phase === "failed";
1528
+ var busyError = () => new CallpadError("session.busy", "A session is already active");
1529
+ var shouldRethrowCommandError = (err) => err instanceof CallpadError && (err.code === "session.join_unavailable" || err.code === "session.command_stale");
1530
+ var toCallpadError = (err, code = "session.failed") => {
1531
+ if (err instanceof CallpadError) {
1532
+ return err;
1533
+ }
1534
+ if (err instanceof Error) {
1535
+ return new CallpadError(code, err.message);
1536
+ }
1537
+ return new CallpadError(code, "Session command failed");
1538
+ };
1058
1539
 
1059
1540
  // src/core/store.ts
1060
1541
  var createSessionStore = (userId) => {
1061
1542
  const sessions = /* @__PURE__ */ new Map();
1062
1543
  const subscribers = /* @__PURE__ */ new Map();
1063
1544
  let status = "idle";
1064
- let activeId = null;
1065
1545
  let listCache = [];
1066
1546
  let invitesCache = [];
1067
1547
  let inviteKey = "";
@@ -1093,15 +1573,6 @@ var createSessionStore = (userId) => {
1093
1573
  listCache = Array.from(sessions.values()).sort(
1094
1574
  (a, b) => b.createdAt.localeCompare(a.createdAt)
1095
1575
  );
1096
- if (activeId) {
1097
- const active = sessions.get(activeId);
1098
- if (!active || active.state === "ended") {
1099
- activeId = null;
1100
- }
1101
- }
1102
- if (!activeId) {
1103
- activeId = listCache.find((session) => session.state !== "ended")?.id ?? null;
1104
- }
1105
1576
  };
1106
1577
  const refreshInvites = () => {
1107
1578
  const now = Date.now();
@@ -1141,6 +1612,7 @@ var createSessionStore = (userId) => {
1141
1612
  inviteExpiryTimer = null;
1142
1613
  if (refreshInvites()) {
1143
1614
  emit("invites");
1615
+ emit("sessions");
1144
1616
  }
1145
1617
  },
1146
1618
  Math.max(0, nextExpiry - now)
@@ -1155,15 +1627,7 @@ var createSessionStore = (userId) => {
1155
1627
  emit("invites");
1156
1628
  }
1157
1629
  };
1158
- const get = (sessionId) => {
1159
- if (sessionId) {
1160
- return sessions.get(sessionId) ?? null;
1161
- }
1162
- if (!activeId) {
1163
- return null;
1164
- }
1165
- return sessions.get(activeId) ?? null;
1166
- };
1630
+ const get = (sessionId) => sessions.get(sessionId) ?? null;
1167
1631
  const upsert = (session, mode = "event") => {
1168
1632
  const prev = sessions.get(session.id);
1169
1633
  const stale = prev ? mode === "command" ? session.version < prev.version : session.version <= prev.version : false;
@@ -1187,18 +1651,33 @@ var createSessionStore = (userId) => {
1187
1651
  get,
1188
1652
  invites: () => invitesCache,
1189
1653
  invite: (sessionId) => invitesCache.find((invite) => invite.session.id === sessionId) ?? null,
1190
- sync: (items) => {
1654
+ sync: (items, options) => {
1655
+ const changedIds = /* @__PURE__ */ new Set();
1191
1656
  for (const session of items) {
1192
1657
  const prev = sessions.get(session.id);
1193
1658
  if (!prev || session.version >= prev.version) {
1194
1659
  sessions.set(session.id, session);
1660
+ changedIds.add(session.id);
1661
+ }
1662
+ }
1663
+ const removedIds = [];
1664
+ if (options?.mode === "replaceVisible" && options.shouldReplace) {
1665
+ const visibleIds = new Set(items.map((session) => session.id));
1666
+ for (const session of sessions.values()) {
1667
+ if (!visibleIds.has(session.id) && options.shouldReplace(session)) {
1668
+ sessions.delete(session.id);
1669
+ removedIds.push(session.id);
1670
+ }
1195
1671
  }
1196
1672
  }
1197
1673
  refresh();
1198
1674
  const invitesChanged = refreshInvites();
1199
1675
  emit("sessions");
1200
- for (const session of items) {
1201
- emit(`session:${session.id}`);
1676
+ for (const sessionId of changedIds) {
1677
+ emit(`session:${sessionId}`);
1678
+ }
1679
+ for (const sessionId of removedIds) {
1680
+ emit(`session:${sessionId}`);
1202
1681
  }
1203
1682
  if (invitesChanged) {
1204
1683
  emit("invites");
@@ -1286,55 +1765,97 @@ var isHost = (participant) => participant.role === "host" && (participant.kind =
1286
1765
  // src/core/client.ts
1287
1766
  var createSessionClient = (config) => {
1288
1767
  const core = createCore(config);
1768
+ const sessions = {
1769
+ ...createCommonSessions(core),
1770
+ start: (input) => startSession(core, input.vendor, input.target)
1771
+ };
1772
+ const session = createCurrentSessionController({
1773
+ store: core.store,
1774
+ emitter: core.emitter,
1775
+ start: sessions.start,
1776
+ accept: sessions.accept,
1777
+ join: sessions.join,
1778
+ leave: sessions.leave
1779
+ });
1289
1780
  return {
1290
1781
  kind: "session",
1291
1782
  get status() {
1292
1783
  return core.store.getStatus();
1293
1784
  },
1294
- sessions: {
1295
- ...createCommonSessions(core),
1296
- start: (input) => startSession(core, input.vendor, input.target)
1297
- },
1785
+ sessions,
1298
1786
  invites: {
1299
1787
  list: core.store.invites,
1300
1788
  get: core.store.invite
1301
1789
  },
1790
+ session,
1302
1791
  connect: core.connect,
1303
1792
  disconnect: core.realtime.disconnect,
1304
- dispose: core.dispose,
1793
+ dispose: async () => {
1794
+ session.dispose();
1795
+ await core.dispose();
1796
+ },
1305
1797
  on: core.emitter.on,
1306
1798
  subscribe: core.store.subscribe
1307
1799
  };
1308
1800
  };
1309
1801
  var createHostClient = (config) => {
1310
1802
  const core = createCore(config);
1803
+ const sessions = {
1804
+ ...createCommonSessions(core),
1805
+ start: (input) => startSession(core, config.vendor, input.target)
1806
+ };
1807
+ const host = createHostControls(core);
1808
+ const session = createCurrentSessionController({
1809
+ store: core.store,
1810
+ emitter: core.emitter,
1811
+ start: sessions.start,
1812
+ accept: sessions.accept,
1813
+ join: sessions.join,
1814
+ leave: sessions.leave,
1815
+ end: host.end
1816
+ });
1311
1817
  return {
1312
1818
  kind: "host",
1313
1819
  vendor: config.vendor,
1314
1820
  get status() {
1315
1821
  return core.store.getStatus();
1316
1822
  },
1317
- sessions: {
1318
- ...createCommonSessions(core),
1319
- start: (input) => startSession(core, config.vendor, input.target)
1320
- },
1823
+ sessions,
1321
1824
  invites: {
1322
1825
  list: core.store.invites,
1323
1826
  get: core.store.invite
1324
1827
  },
1325
- host: createHostControls(core),
1828
+ session,
1829
+ host,
1326
1830
  connect: core.connect,
1327
1831
  disconnect: core.realtime.disconnect,
1328
- dispose: core.dispose,
1832
+ dispose: async () => {
1833
+ session.dispose();
1834
+ await core.dispose();
1835
+ },
1329
1836
  on: core.emitter.on,
1330
1837
  subscribe: core.store.subscribe
1331
1838
  };
1332
1839
  };
1840
+ var normalizePhoneTarget = (target) => {
1841
+ if (target.type === "phone") {
1842
+ return { ...target, phone: normalizeE164Phone(target.phone) };
1843
+ }
1844
+ return target;
1845
+ };
1333
1846
  var createCore = (config) => {
1334
1847
  const api = apiClient(config);
1335
1848
  const store = createSessionStore(config.userId);
1336
1849
  const emitter = createEmitter();
1337
- const realtime = createRealtime(api, config.auth, store, emitter);
1850
+ const syncActive = async () => {
1851
+ const query = { state: "active" };
1852
+ const response = await unwrap(listSessions({ client: api, query }), config.auth);
1853
+ store.sync(response.items, {
1854
+ mode: "replaceVisible",
1855
+ shouldReplace: (session) => matchesVisibleQuery(session, query)
1856
+ });
1857
+ };
1858
+ const realtime = createRealtime(api, config.auth, store, emitter, syncActive);
1338
1859
  const applySession = (session, mode = "event") => {
1339
1860
  const change = store.upsert(session, mode);
1340
1861
  if (change === "added") {
@@ -1384,9 +1905,12 @@ var createCore = (config) => {
1384
1905
  var createCommonSessions = (core) => ({
1385
1906
  list: core.store.list,
1386
1907
  get: core.store.get,
1387
- sync: async (query) => {
1908
+ sync: async (query, options) => {
1388
1909
  const response = await unwrap(listSessions({ client: core.api, query }), core.auth);
1389
- core.store.sync(response.items);
1910
+ core.store.sync(response.items, {
1911
+ mode: options?.mode,
1912
+ shouldReplace: options?.mode === "replaceVisible" && canReplaceVisibleQuery(query) ? (session) => matchesVisibleQuery(session, query) : void 0
1913
+ });
1390
1914
  return core.store.list();
1391
1915
  },
1392
1916
  join: (sessionId) => unwrap(createSessionToken({ client: core.api, path: { sessionId } }), core.auth),
@@ -1434,7 +1958,7 @@ var createHostControls = (core) => ({
1434
1958
  addSessionParticipant({
1435
1959
  client: core.api,
1436
1960
  path: { sessionId: input.sessionId },
1437
- body: { target: input.target }
1961
+ body: { target: normalizePhoneTarget(input.target) }
1438
1962
  }),
1439
1963
  core.auth
1440
1964
  ),
@@ -1465,12 +1989,28 @@ var startSession = async (core, vendor, target) => core.applyStarted(
1465
1989
  createSession({
1466
1990
  client: core.api,
1467
1991
  path: { vendor },
1468
- body: { target }
1992
+ body: { target: normalizePhoneTarget(target) }
1469
1993
  }),
1470
1994
  core.auth
1471
1995
  ),
1472
1996
  "command"
1473
1997
  );
1998
+ var matchesVisibleQuery = (session, query) => {
1999
+ if (query?.vendor && session.vendor !== query.vendor) {
2000
+ return false;
2001
+ }
2002
+ const state = query?.state ?? "active";
2003
+ if (state === "all") {
2004
+ return true;
2005
+ }
2006
+ if (state === "ended") {
2007
+ return session.state === "ended";
2008
+ }
2009
+ return session.state !== "ended";
2010
+ };
2011
+ var canReplaceVisibleQuery = (query) => {
2012
+ return !query?.before && query?.limit === void 0;
2013
+ };
1474
2014
 
1475
2015
  exports.CallpadError = CallpadError;
1476
2016
  exports.createHostClient = createHostClient;