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/CHANGELOG.md +13 -0
- package/README.md +150 -224
- package/dist/index.cjs +597 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +597 -57
- package/dist/index.js.map +1 -1
- package/dist/livekit/index.cjs +43 -1
- package/dist/livekit/index.cjs.map +1 -1
- package/dist/livekit/index.d.cts +18 -0
- package/dist/livekit/index.d.ts +18 -0
- package/dist/livekit/index.js +46 -0
- package/dist/livekit/index.js.map +1 -1
- package/dist/react/index.cjs +17 -2
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +7 -5
- package/dist/react/index.d.ts +7 -5
- package/dist/react/index.js +16 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{types-Dgwrb3rf.d.cts → types-BjVaSMZ6.d.cts} +68 -5
- package/dist/{types-Dgwrb3rf.d.ts → types-BjVaSMZ6.d.ts} +68 -5
- package/package.json +3 -2
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
|
|
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
|
-
|
|
998
|
-
|
|
1017
|
+
const currentAttempt = ++attempt;
|
|
1018
|
+
const promise = (async () => {
|
|
1019
|
+
setStatus("connecting");
|
|
999
1020
|
const first = await fetchToken();
|
|
1000
|
-
|
|
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
|
|
1005
|
-
|
|
1006
|
-
|
|
1028
|
+
rt = next;
|
|
1029
|
+
next.on("connecting", () => {
|
|
1030
|
+
const status = store.getStatus() === "ready" ? "reconnecting" : "connecting";
|
|
1031
|
+
setStatus(status);
|
|
1007
1032
|
});
|
|
1008
|
-
|
|
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
|
-
|
|
1045
|
+
next.on("disconnected", () => {
|
|
1012
1046
|
if (!disposed) {
|
|
1013
1047
|
setStatus("offline");
|
|
1014
1048
|
}
|
|
1015
1049
|
});
|
|
1016
|
-
|
|
1050
|
+
next.on("error", (ctx) => {
|
|
1017
1051
|
emitter.emit("error", new Error(ctx.error.message));
|
|
1018
1052
|
});
|
|
1019
|
-
|
|
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
|
-
|
|
1030
|
-
await
|
|
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
|
-
|
|
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
|
|
1201
|
-
emit(`session:${
|
|
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:
|
|
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
|
-
|
|
1828
|
+
session,
|
|
1829
|
+
host,
|
|
1326
1830
|
connect: core.connect,
|
|
1327
1831
|
disconnect: core.realtime.disconnect,
|
|
1328
|
-
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
|
|
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;
|