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.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
|
|
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
|
-
|
|
996
|
-
|
|
1015
|
+
const currentAttempt = ++attempt;
|
|
1016
|
+
const promise = (async () => {
|
|
1017
|
+
setStatus("connecting");
|
|
997
1018
|
const first = await fetchToken();
|
|
998
|
-
|
|
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
|
|
1003
|
-
|
|
1004
|
-
|
|
1026
|
+
rt = next;
|
|
1027
|
+
next.on("connecting", () => {
|
|
1028
|
+
const status = store.getStatus() === "ready" ? "reconnecting" : "connecting";
|
|
1029
|
+
setStatus(status);
|
|
1005
1030
|
});
|
|
1006
|
-
|
|
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
|
-
|
|
1043
|
+
next.on("disconnected", () => {
|
|
1010
1044
|
if (!disposed) {
|
|
1011
1045
|
setStatus("offline");
|
|
1012
1046
|
}
|
|
1013
1047
|
});
|
|
1014
|
-
|
|
1048
|
+
next.on("error", (ctx) => {
|
|
1015
1049
|
emitter.emit("error", new Error(ctx.error.message));
|
|
1016
1050
|
});
|
|
1017
|
-
|
|
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
|
-
|
|
1028
|
-
await
|
|
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
|
-
|
|
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
|
|
1199
|
-
emit(`session:${
|
|
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:
|
|
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
|
-
|
|
1826
|
+
session,
|
|
1827
|
+
host,
|
|
1324
1828
|
connect: core.connect,
|
|
1325
1829
|
disconnect: core.realtime.disconnect,
|
|
1326
|
-
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
|
|
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
|