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