@glyphteck/veyl 0.72.0 → 0.73.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/account.js +3231 -213
- package/dist/accountprofiles.js +2 -0
- package/dist/cli.js +4305 -730
- package/dist/index.js +4304 -729
- package/examples/bot-fleet/policy.js +18 -0
- package/examples/bot-fleet/readme.md +1 -0
- package/examples/bot-fleet/runtime.js +8 -1
- package/examples/bot-fleet/voice.js +157 -0
- package/package.json +2 -1
package/dist/account.js
CHANGED
|
@@ -16676,17 +16676,6 @@ function canonicalBytes(value, label = "value") {
|
|
|
16676
16676
|
return encoder.encode(canonicalJson(value, label));
|
|
16677
16677
|
}
|
|
16678
16678
|
|
|
16679
|
-
// ../../core/utils/text.js
|
|
16680
|
-
function cleanText(value) {
|
|
16681
|
-
return typeof value === "string" ? value.trim() : "";
|
|
16682
|
-
}
|
|
16683
|
-
function lowerText(value) {
|
|
16684
|
-
return cleanText(value).toLowerCase();
|
|
16685
|
-
}
|
|
16686
|
-
function sameText(left, right) {
|
|
16687
|
-
return lowerText(left) === lowerText(right);
|
|
16688
|
-
}
|
|
16689
|
-
|
|
16690
16679
|
// ../../core/config.js
|
|
16691
16680
|
var MS_PER_SECOND = 1000;
|
|
16692
16681
|
var MINUTE_MS = 60 * MS_PER_SECOND;
|
|
@@ -16708,6 +16697,8 @@ var AUTOLOCK_MAX_MINUTES = 60;
|
|
|
16708
16697
|
var BTC_PRICE_FALLBACK = 80000;
|
|
16709
16698
|
var CHAT_MESSAGE_EDIT_WINDOW_MS = 10 * MINUTE_MS;
|
|
16710
16699
|
var CHAT_SEND_CONNECTION_WAIT_MS = 30 * MS_PER_SECOND;
|
|
16700
|
+
var CALL_SPEAKING_THRESHOLD = 0.02;
|
|
16701
|
+
var CALL_SPEAKING_HOLD_MS = 320;
|
|
16711
16702
|
var LOCAL_MEDIA_CACHE_MAX_BYTES = 512 * MIB_BYTES;
|
|
16712
16703
|
var LOCAL_PROFILE_CACHE_MAX_ITEMS = 500;
|
|
16713
16704
|
var LOCAL_PROFILE_CACHE_MAX_AGE_MS = 30 * DAY_MS;
|
|
@@ -16828,6 +16819,340 @@ var WALLET_PENDING_TRANSFER_STUCK_RETRY_MS = 10 * MINUTE_MS;
|
|
|
16828
16819
|
var WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS = HOUR_MS;
|
|
16829
16820
|
var WALLET_TRANSFER_CACHE_WRITE_DELAY_MS = 3 * MS_PER_SECOND;
|
|
16830
16821
|
|
|
16822
|
+
// ../../core/calls/wire.js
|
|
16823
|
+
var CALL_REQUEST_MAX_BYTES = 192 * 1024;
|
|
16824
|
+
var CALL_HEAD_MAX_BYTES = 64 * 1024;
|
|
16825
|
+
var CALL_HEAD_PROOF_MAX_BYTES = 2048;
|
|
16826
|
+
var CALL_EVENT_MAX_BYTES = 48 * 1024;
|
|
16827
|
+
var CALL_LOG_MAX_BYTES = 512 * 1024;
|
|
16828
|
+
var CALL_LOG_MAX_EVENTS = 128;
|
|
16829
|
+
var CALL_RESPONSE_MAX_BYTES = Math.ceil((CALL_HEAD_MAX_BYTES + CALL_HEAD_PROOF_MAX_BYTES + CALL_LOG_MAX_BYTES) / 3) * 4 + CALL_LOG_MAX_EVENTS * 128 + 4096;
|
|
16830
|
+
var CALL_SCOPE_KINDS = ["ownership", "discovery", "signaling"];
|
|
16831
|
+
var SCOPE_SOCKETS = Object.freeze({ ownership: 8, discovery: CHAT_MAX_MEMBERS * 4, signaling: 32 });
|
|
16832
|
+
var SCOPE_LIMITS = Object.freeze(Object.fromEntries(Object.entries(SCOPE_SOCKETS).map(([kind, sockets]) => [
|
|
16833
|
+
kind,
|
|
16834
|
+
Object.freeze({ sockets, readsPerMinute: (kind === "signaling" ? SCOPE_SOCKETS.discovery : sockets) * 4, writesPerMinute: 600 })
|
|
16835
|
+
])));
|
|
16836
|
+
var HEX_KEY = /^[a-f0-9]{64}$/u;
|
|
16837
|
+
function callWireError(code = "invalid-request") {
|
|
16838
|
+
const error = new Error(`call ${code}`);
|
|
16839
|
+
error.code = `calls/${code}`;
|
|
16840
|
+
return error;
|
|
16841
|
+
}
|
|
16842
|
+
function encodeCallBytes(value) {
|
|
16843
|
+
if (!(value instanceof Uint8Array))
|
|
16844
|
+
throw callWireError();
|
|
16845
|
+
let text = "";
|
|
16846
|
+
for (const byte of value)
|
|
16847
|
+
text += String.fromCharCode(byte);
|
|
16848
|
+
return btoa(text);
|
|
16849
|
+
}
|
|
16850
|
+
function callHeadDigest(value) {
|
|
16851
|
+
if (!(value instanceof Uint8Array) || !value.length || value.length > CALL_HEAD_MAX_BYTES)
|
|
16852
|
+
throw callWireError();
|
|
16853
|
+
return toHex(sha256(value));
|
|
16854
|
+
}
|
|
16855
|
+
function decodeCallBytes(value, maximum = CALL_HEAD_MAX_BYTES) {
|
|
16856
|
+
if (typeof value !== "string" || value.length > Math.ceil(maximum / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value))
|
|
16857
|
+
throw callWireError();
|
|
16858
|
+
const bytes = Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
16859
|
+
if (bytes.length > maximum || encodeCallBytes(bytes) !== value)
|
|
16860
|
+
throw callWireError();
|
|
16861
|
+
return bytes;
|
|
16862
|
+
}
|
|
16863
|
+
function callScope(realm, kind, publicKey) {
|
|
16864
|
+
if (!["prod", "dev"].includes(realm) || !CALL_SCOPE_KINDS.includes(kind) || !HEX_KEY.test(publicKey))
|
|
16865
|
+
throw callWireError();
|
|
16866
|
+
return toHex(sha256(canonicalBytes(["veyl.call.scope.v1", realm, kind, publicKey])));
|
|
16867
|
+
}
|
|
16868
|
+
function decodeCallRecord(record) {
|
|
16869
|
+
return {
|
|
16870
|
+
...record,
|
|
16871
|
+
active: record.active ? { ...record.active, payload: decodeCallBytes(record.active.payload, 4096) } : null,
|
|
16872
|
+
pending: record.pending ? { ...record.pending, payload: decodeCallBytes(record.pending.payload, 4096) } : null
|
|
16873
|
+
};
|
|
16874
|
+
}
|
|
16875
|
+
|
|
16876
|
+
// ../../core/calls/identity.js
|
|
16877
|
+
var REALMS = new Set(["prod", "dev"]);
|
|
16878
|
+
var KINDS = new Set(["ownership", "discovery", "signaling"]);
|
|
16879
|
+
var MAX_SEALED_BYTES = 64 * 1024;
|
|
16880
|
+
function createCallCapability(seed, realm, kind, parts = []) {
|
|
16881
|
+
if (!REALMS.has(realm) || !KINDS.has(kind))
|
|
16882
|
+
throw new Error("invalid call capability");
|
|
16883
|
+
const signing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.signing.v1", [realm, kind, ...parts]);
|
|
16884
|
+
const sealing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.sealing.v1", [realm, kind, ...parts]);
|
|
16885
|
+
const publicKey = toHex(ed25519.getPublicKey(signing));
|
|
16886
|
+
const scope = callScope(realm, kind, publicKey);
|
|
16887
|
+
const aad = canonicalBytes(["veyl.call.sealed.v1", realm, kind, scope]);
|
|
16888
|
+
let owners = 0;
|
|
16889
|
+
function retain() {
|
|
16890
|
+
owners += 1;
|
|
16891
|
+
let closed = false;
|
|
16892
|
+
function current() {
|
|
16893
|
+
if (closed)
|
|
16894
|
+
throw new Error("call capability closed");
|
|
16895
|
+
}
|
|
16896
|
+
return Object.freeze({
|
|
16897
|
+
realm,
|
|
16898
|
+
kind,
|
|
16899
|
+
scope,
|
|
16900
|
+
publicKey,
|
|
16901
|
+
sign(bytes) {
|
|
16902
|
+
current();
|
|
16903
|
+
return toHex(ed25519.sign(toBytes(bytes), signing));
|
|
16904
|
+
},
|
|
16905
|
+
async seal(value) {
|
|
16906
|
+
current();
|
|
16907
|
+
const plain = canonicalBytes(value, "call payload");
|
|
16908
|
+
if (plain.length + 28 > MAX_SEALED_BYTES)
|
|
16909
|
+
throw new Error("call payload too large");
|
|
16910
|
+
try {
|
|
16911
|
+
const { iv, ct } = await sealAes(sealing, plain, aad);
|
|
16912
|
+
current();
|
|
16913
|
+
const result = new Uint8Array(iv.length + ct.length);
|
|
16914
|
+
result.set(iv);
|
|
16915
|
+
result.set(ct, iv.length);
|
|
16916
|
+
return result;
|
|
16917
|
+
} finally {
|
|
16918
|
+
cleanBytes(plain);
|
|
16919
|
+
}
|
|
16920
|
+
},
|
|
16921
|
+
async open(value) {
|
|
16922
|
+
current();
|
|
16923
|
+
const bytes = toBytes(value);
|
|
16924
|
+
if (bytes.length < 28 || bytes.length > MAX_SEALED_BYTES)
|
|
16925
|
+
throw new Error("invalid call payload");
|
|
16926
|
+
const plain = await openAes(sealing, bytes.subarray(0, 12), bytes.subarray(12), aad);
|
|
16927
|
+
try {
|
|
16928
|
+
current();
|
|
16929
|
+
return JSON.parse(decoder.decode(plain));
|
|
16930
|
+
} finally {
|
|
16931
|
+
cleanBytes(plain);
|
|
16932
|
+
}
|
|
16933
|
+
},
|
|
16934
|
+
retain() {
|
|
16935
|
+
current();
|
|
16936
|
+
return retain();
|
|
16937
|
+
},
|
|
16938
|
+
close() {
|
|
16939
|
+
if (closed)
|
|
16940
|
+
return;
|
|
16941
|
+
closed = true;
|
|
16942
|
+
if (--owners === 0)
|
|
16943
|
+
cleanBytes(signing, sealing);
|
|
16944
|
+
}
|
|
16945
|
+
});
|
|
16946
|
+
}
|
|
16947
|
+
return retain();
|
|
16948
|
+
}
|
|
16949
|
+
function createCallIdentity(masterSeed, realm) {
|
|
16950
|
+
const root = deriveKey(toBytes32(masterSeed, "vault root"), "veyl.call.account.v1", [realm]);
|
|
16951
|
+
try {
|
|
16952
|
+
return createCallCapability(root, realm, "ownership");
|
|
16953
|
+
} finally {
|
|
16954
|
+
cleanBytes(root);
|
|
16955
|
+
}
|
|
16956
|
+
}
|
|
16957
|
+
function createCallEndpoint() {
|
|
16958
|
+
const secret = randomBytes3(32);
|
|
16959
|
+
const publicKey = toHex(ed25519.getPublicKey(secret));
|
|
16960
|
+
let closed = false;
|
|
16961
|
+
return Object.freeze({
|
|
16962
|
+
publicKey,
|
|
16963
|
+
holder: toHex(randomBytes3(16)),
|
|
16964
|
+
sign(bytes) {
|
|
16965
|
+
if (closed)
|
|
16966
|
+
throw new Error("call endpoint closed");
|
|
16967
|
+
return toHex(ed25519.sign(toBytes(bytes), secret));
|
|
16968
|
+
},
|
|
16969
|
+
close() {
|
|
16970
|
+
closed = true;
|
|
16971
|
+
cleanBytes(secret);
|
|
16972
|
+
}
|
|
16973
|
+
});
|
|
16974
|
+
}
|
|
16975
|
+
function createCallDiscovery(epochState, realm) {
|
|
16976
|
+
const manifest = epochState?.manifest;
|
|
16977
|
+
if (!manifest?.chatId || !manifest?.epochId || !epochState?.epochSecret)
|
|
16978
|
+
throw new Error("current chat epoch required");
|
|
16979
|
+
return createCallCapability(toBytes32(epochState.epochSecret), realm, "discovery", [manifest.chatId, manifest.epochId]);
|
|
16980
|
+
}
|
|
16981
|
+
|
|
16982
|
+
// ../../core/utils/text.js
|
|
16983
|
+
function cleanText(value) {
|
|
16984
|
+
return typeof value === "string" ? value.trim() : "";
|
|
16985
|
+
}
|
|
16986
|
+
function lowerText(value) {
|
|
16987
|
+
return cleanText(value).toLowerCase();
|
|
16988
|
+
}
|
|
16989
|
+
function sameText(left, right) {
|
|
16990
|
+
return lowerText(left) === lowerText(right);
|
|
16991
|
+
}
|
|
16992
|
+
|
|
16993
|
+
// ../../core/utils/time.js
|
|
16994
|
+
function timestampMs(value, fallback = null, options = {}) {
|
|
16995
|
+
let ms = null;
|
|
16996
|
+
if (typeof value?.toMillis === "function") {
|
|
16997
|
+
ms = value.toMillis();
|
|
16998
|
+
} else if (value instanceof Date) {
|
|
16999
|
+
ms = value.getTime();
|
|
17000
|
+
} else if (typeof value?.seconds === "number") {
|
|
17001
|
+
ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
|
|
17002
|
+
} else if (typeof value?._seconds === "number") {
|
|
17003
|
+
ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
|
|
17004
|
+
} else if (Number.isFinite(value)) {
|
|
17005
|
+
ms = value;
|
|
17006
|
+
} else if (options.parseString && typeof value === "string") {
|
|
17007
|
+
const numberMs = Number(value);
|
|
17008
|
+
ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
|
|
17009
|
+
}
|
|
17010
|
+
if (!Number.isFinite(ms) || options.positive && ms <= 0) {
|
|
17011
|
+
return fallback;
|
|
17012
|
+
}
|
|
17013
|
+
return ms;
|
|
17014
|
+
}
|
|
17015
|
+
function timestampKey(value) {
|
|
17016
|
+
if (value == null) {
|
|
17017
|
+
return null;
|
|
17018
|
+
}
|
|
17019
|
+
return timestampMs(value, null) ?? String(value);
|
|
17020
|
+
}
|
|
17021
|
+
function makeTimestamp(ms) {
|
|
17022
|
+
return {
|
|
17023
|
+
toMillis() {
|
|
17024
|
+
return ms;
|
|
17025
|
+
},
|
|
17026
|
+
toDate() {
|
|
17027
|
+
return new Date(ms);
|
|
17028
|
+
}
|
|
17029
|
+
};
|
|
17030
|
+
}
|
|
17031
|
+
function twoDigits(value) {
|
|
17032
|
+
return String(value).padStart(2, "0");
|
|
17033
|
+
}
|
|
17034
|
+
function dayKey(date) {
|
|
17035
|
+
return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
|
|
17036
|
+
}
|
|
17037
|
+
function localDayKey(value) {
|
|
17038
|
+
const ms = timestampMs(value, null, { parseString: true });
|
|
17039
|
+
if (!Number.isFinite(ms))
|
|
17040
|
+
return "";
|
|
17041
|
+
return dayKey(new Date(ms));
|
|
17042
|
+
}
|
|
17043
|
+
function hourKey(dateOrHour) {
|
|
17044
|
+
const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
|
|
17045
|
+
return twoDigits(hour);
|
|
17046
|
+
}
|
|
17047
|
+
function dayHourKey(date) {
|
|
17048
|
+
return `${dayKey(date)}-${hourKey(date)}`;
|
|
17049
|
+
}
|
|
17050
|
+
var MINUTE_MS2 = 60000;
|
|
17051
|
+
var HOUR_MS2 = 60 * MINUTE_MS2;
|
|
17052
|
+
function nextLocalDayStartMs(ms) {
|
|
17053
|
+
const date = new Date(ms);
|
|
17054
|
+
date.setDate(date.getDate() + 1);
|
|
17055
|
+
date.setHours(0, 0, 0, 0);
|
|
17056
|
+
return date.getTime();
|
|
17057
|
+
}
|
|
17058
|
+
function nextRowDateTimeRefreshMs(value, now = Date.now()) {
|
|
17059
|
+
const ms = timestampMs(value, null, { parseString: true });
|
|
17060
|
+
const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
|
|
17061
|
+
if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
|
|
17062
|
+
return null;
|
|
17063
|
+
const age = nowMs2 - ms;
|
|
17064
|
+
if (age < -MINUTE_MS2)
|
|
17065
|
+
return ms - MINUTE_MS2;
|
|
17066
|
+
if (age < MINUTE_MS2)
|
|
17067
|
+
return ms + MINUTE_MS2;
|
|
17068
|
+
if (age < HOUR_MS2)
|
|
17069
|
+
return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
|
|
17070
|
+
if (localDayKey(ms) === localDayKey(nowMs2))
|
|
17071
|
+
return nextLocalDayStartMs(nowMs2);
|
|
17072
|
+
return null;
|
|
17073
|
+
}
|
|
17074
|
+
|
|
17075
|
+
// ../../core/chat/state.js
|
|
17076
|
+
function makeCid() {
|
|
17077
|
+
return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
|
|
17078
|
+
}
|
|
17079
|
+
function getMessageKey(message) {
|
|
17080
|
+
return message?.cid || message?.id || null;
|
|
17081
|
+
}
|
|
17082
|
+
function getCidMs(cid) {
|
|
17083
|
+
if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
|
|
17084
|
+
return null;
|
|
17085
|
+
}
|
|
17086
|
+
const base = cid.slice(0, -6);
|
|
17087
|
+
const ms = Number.parseInt(base, 36);
|
|
17088
|
+
return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
|
|
17089
|
+
}
|
|
17090
|
+
function getMessageOrderMs(message) {
|
|
17091
|
+
return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
|
|
17092
|
+
}
|
|
17093
|
+
function sortMessages(messages) {
|
|
17094
|
+
return [...messages].sort((a, b) => {
|
|
17095
|
+
const aMs = getMessageOrderMs(a);
|
|
17096
|
+
const bMs = getMessageOrderMs(b);
|
|
17097
|
+
if (aMs !== bMs) {
|
|
17098
|
+
return aMs - bMs;
|
|
17099
|
+
}
|
|
17100
|
+
return String(a?.id || "").localeCompare(String(b?.id || ""));
|
|
17101
|
+
});
|
|
17102
|
+
}
|
|
17103
|
+
function mergeMessages(...groups) {
|
|
17104
|
+
const merged = new Map;
|
|
17105
|
+
for (const group of groups) {
|
|
17106
|
+
for (const message of group || []) {
|
|
17107
|
+
const key = getMessageKey(message);
|
|
17108
|
+
if (!key) {
|
|
17109
|
+
continue;
|
|
17110
|
+
}
|
|
17111
|
+
merged.set(key, message);
|
|
17112
|
+
}
|
|
17113
|
+
}
|
|
17114
|
+
return sortMessages([...merged.values()]);
|
|
17115
|
+
}
|
|
17116
|
+
|
|
17117
|
+
// ../../core/chat/messages/call.js
|
|
17118
|
+
var CALL_MSG_TYPE = "call";
|
|
17119
|
+
function readCallMessage(message) {
|
|
17120
|
+
if (message?.t !== CALL_MSG_TYPE || typeof message.callId !== "string" || !/^[0-9a-f]{64}$/u.test(message.callId) || !["started", "ended"].includes(message.event))
|
|
17121
|
+
return null;
|
|
17122
|
+
return { callId: message.callId, event: message.event };
|
|
17123
|
+
}
|
|
17124
|
+
function makeCallMessage(callId, event) {
|
|
17125
|
+
const message = { t: CALL_MSG_TYPE, callId, event };
|
|
17126
|
+
if (!readCallMessage(message))
|
|
17127
|
+
throw new Error("invalid call message");
|
|
17128
|
+
return message;
|
|
17129
|
+
}
|
|
17130
|
+
function callMessageText(message) {
|
|
17131
|
+
const call = readCallMessage(message);
|
|
17132
|
+
return call ? `call ${call.event}` : "";
|
|
17133
|
+
}
|
|
17134
|
+
function latestCallStartKey(messages) {
|
|
17135
|
+
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
17136
|
+
if (readCallMessage(messages[index])?.event === "started")
|
|
17137
|
+
return getMessageKey(messages[index]);
|
|
17138
|
+
}
|
|
17139
|
+
return "";
|
|
17140
|
+
}
|
|
17141
|
+
|
|
17142
|
+
// ../../core/chat/messages/types.js
|
|
17143
|
+
var ATTACHMENT_MSG_TYPES = ["img", "gif", "m4a", "mp4", "file"];
|
|
17144
|
+
var MAX_TXT_CHARS = CHAT_MAX_TEXT_CHARS;
|
|
17145
|
+
var REACTION_MSG_TYPE = "rxn";
|
|
17146
|
+
var DELETE_MSG_TYPE = "del";
|
|
17147
|
+
var SYSTEM_MSG_TYPE = "sys";
|
|
17148
|
+
var EPOCH_TRANSITION_MSG_TYPE = "epoch";
|
|
17149
|
+
var EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal";
|
|
17150
|
+
var SYSTEM_SETTINGS_KIND = "settings";
|
|
17151
|
+
var DEFAULT_REACTION_EMOJI = "❤️";
|
|
17152
|
+
var MAX_REACTIONS = CHAT_MAX_REACTIONS;
|
|
17153
|
+
var HOLD_VISIBLE_KEY = "__holdVisible";
|
|
17154
|
+
var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
|
|
17155
|
+
|
|
16831
17156
|
// ../../core/notifications.js
|
|
16832
17157
|
"use client";
|
|
16833
17158
|
var NOTIFICATION_DESCRIPTOR_VERSION = 1;
|
|
@@ -16843,10 +17168,47 @@ var CHAT_ATTENTION_KINDS = Object.freeze({
|
|
|
16843
17168
|
ROTATION: "rotation",
|
|
16844
17169
|
SILENT: "silent"
|
|
16845
17170
|
});
|
|
17171
|
+
var NOTIFICATION_PRESENTATION_KINDS = Object.freeze({
|
|
17172
|
+
MESSAGE: 0,
|
|
17173
|
+
REACTION: 1,
|
|
17174
|
+
ACTIVITY: 2,
|
|
17175
|
+
PHOTO: 3,
|
|
17176
|
+
VIDEO: 4,
|
|
17177
|
+
AUDIO: 5,
|
|
17178
|
+
FILE: 6,
|
|
17179
|
+
CALL_STARTED: 7,
|
|
17180
|
+
CALL_ENDED: 8
|
|
17181
|
+
});
|
|
17182
|
+
function notificationMessageKind(message) {
|
|
17183
|
+
switch (message?.t) {
|
|
17184
|
+
case SYSTEM_MSG_TYPE:
|
|
17185
|
+
case EPOCH_TRANSITION_MSG_TYPE:
|
|
17186
|
+
return NOTIFICATION_PRESENTATION_KINDS.ACTIVITY;
|
|
17187
|
+
case "img":
|
|
17188
|
+
case "gif":
|
|
17189
|
+
return NOTIFICATION_PRESENTATION_KINDS.PHOTO;
|
|
17190
|
+
case "mp4":
|
|
17191
|
+
return NOTIFICATION_PRESENTATION_KINDS.VIDEO;
|
|
17192
|
+
case "m4a":
|
|
17193
|
+
return NOTIFICATION_PRESENTATION_KINDS.AUDIO;
|
|
17194
|
+
case "file":
|
|
17195
|
+
return NOTIFICATION_PRESENTATION_KINDS.FILE;
|
|
17196
|
+
case "call": {
|
|
17197
|
+
const call = readCallMessage(message);
|
|
17198
|
+
return !call ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : call.event === "started" ? NOTIFICATION_PRESENTATION_KINDS.CALL_STARTED : NOTIFICATION_PRESENTATION_KINDS.CALL_ENDED;
|
|
17199
|
+
}
|
|
17200
|
+
default:
|
|
17201
|
+
return NOTIFICATION_PRESENTATION_KINDS.MESSAGE;
|
|
17202
|
+
}
|
|
17203
|
+
}
|
|
17204
|
+
function notificationMessageIsSilent(message) {
|
|
17205
|
+
return message?.t === SYSTEM_MSG_TYPE && message.sys === SYSTEM_SETTINGS_KIND || readCallMessage(message)?.event === "ended";
|
|
17206
|
+
}
|
|
16846
17207
|
var HEX_32_RE = /^[0-9a-f]{64}$/u;
|
|
16847
17208
|
var PEER_TAG_RE = /^[0-9a-f]{32}$/u;
|
|
16848
17209
|
var ATTENTION_KINDS = new Set(Object.values(CHAT_ATTENTION_KINDS));
|
|
16849
17210
|
var NOTIFICATION_MODES = new Set(Object.values(CHAT_NOTIFICATION_MODES));
|
|
17211
|
+
var PRESENTATION_KINDS = new Set(Object.values(NOTIFICATION_PRESENTATION_KINDS));
|
|
16850
17212
|
function hasCurrentChatAttentionRegistration(value) {
|
|
16851
17213
|
return value?.deliveryRegistered === true && value?.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION;
|
|
16852
17214
|
}
|
|
@@ -17002,13 +17364,30 @@ function notificationRouteTag(envelope) {
|
|
|
17002
17364
|
cleanBytes(input);
|
|
17003
17365
|
}
|
|
17004
17366
|
}
|
|
17005
|
-
|
|
17006
|
-
const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
|
|
17367
|
+
function notificationDescriptorClaims(descriptor) {
|
|
17007
17368
|
const peerTag = cleanText(descriptor.peerTag).toLowerCase();
|
|
17008
17369
|
const eventId = cleanText(descriptor.eventId);
|
|
17009
|
-
|
|
17370
|
+
const senderChatPK = cleanHex32(descriptor.senderChatPK, "notification sender key");
|
|
17371
|
+
const kind = descriptor.kind;
|
|
17372
|
+
if (!PEER_TAG_RE.test(peerTag) || !eventId || eventId.length > 256 || !PRESENTATION_KINDS.has(kind)) {
|
|
17010
17373
|
throw new Error("notification descriptor required");
|
|
17011
17374
|
}
|
|
17375
|
+
return { v: NOTIFICATION_DESCRIPTOR_VERSION, peerTag, eventId, senderChatPK, kind };
|
|
17376
|
+
}
|
|
17377
|
+
function notificationDescriptorSignatureInput(recipientPK, descriptor) {
|
|
17378
|
+
return encoder.encode([
|
|
17379
|
+
"veyl-notification-presentation-v1",
|
|
17380
|
+
recipientPK,
|
|
17381
|
+
descriptor.peerTag,
|
|
17382
|
+
descriptor.senderChatPK,
|
|
17383
|
+
descriptor.kind,
|
|
17384
|
+
descriptor.eventId
|
|
17385
|
+
].join("\x00"));
|
|
17386
|
+
}
|
|
17387
|
+
async function sealNotificationDescriptor(recipientNotificationPK, descriptor, sender) {
|
|
17388
|
+
const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
|
|
17389
|
+
const claims = notificationDescriptorClaims({ ...descriptor, senderChatPK: sender?.chatPK });
|
|
17390
|
+
const signature = signChatBytes(sender?.signingKey, notificationDescriptorSignatureInput(recipientPK, claims));
|
|
17012
17391
|
let eph = null;
|
|
17013
17392
|
let shared = null;
|
|
17014
17393
|
let key = null;
|
|
@@ -17019,9 +17398,8 @@ async function sealNotificationDescriptor(recipientNotificationPK, descriptor =
|
|
|
17019
17398
|
const epk = toHex(eph.pub);
|
|
17020
17399
|
key = deriveKey(shared.subarray(0, 32), "notification-descriptor-v1", [epk, recipientPK]);
|
|
17021
17400
|
plaintext = encoder.encode(JSON.stringify({
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
eventId
|
|
17401
|
+
...claims,
|
|
17402
|
+
signature
|
|
17025
17403
|
}));
|
|
17026
17404
|
const { iv, ct } = await sealAes(key, plaintext, descriptorAad(epk, recipientPK));
|
|
17027
17405
|
return {
|
|
@@ -18060,7 +18438,7 @@ function packRegistryData(registry) {
|
|
|
18060
18438
|
}
|
|
18061
18439
|
return concatBytes4(new Uint8Array([SECRET_REGISTRY_ENVELOPE_VERSION]), registry.iv, registry.ct);
|
|
18062
18440
|
}
|
|
18063
|
-
var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
|
|
18441
|
+
var packSeedData = ({ crypto: crypto2 = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
|
|
18064
18442
|
const body = ciphertext || ct;
|
|
18065
18443
|
if (!salt || !iv || !body || !registry) {
|
|
18066
18444
|
throw new Error("seed data missing");
|
|
@@ -18068,7 +18446,7 @@ var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry,
|
|
|
18068
18446
|
if (toBytes(salt, "seed salt").byteLength !== VAULT_SALT_BYTES || toBytes(iv, "seed iv").byteLength !== VAULT_IV_BYTES || toBytes(body, "seed ciphertext").byteLength !== VAULT_SEED_CIPHERTEXT_BYTES) {
|
|
18069
18447
|
throw new Error("invalid seed data");
|
|
18070
18448
|
}
|
|
18071
|
-
const magic = encoder.encode(
|
|
18449
|
+
const magic = encoder.encode(crypto2);
|
|
18072
18450
|
const header = new Uint8Array(1 + magic.length + 8);
|
|
18073
18451
|
header[0] = magic.length;
|
|
18074
18452
|
header.set(magic, 1);
|
|
@@ -18532,7 +18910,7 @@ function walletPubkey(value) {
|
|
|
18532
18910
|
}
|
|
18533
18911
|
return key;
|
|
18534
18912
|
}
|
|
18535
|
-
function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner } = {}) {
|
|
18913
|
+
function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity } = {}) {
|
|
18536
18914
|
const pending = [];
|
|
18537
18915
|
const close = (owner) => {
|
|
18538
18916
|
try {
|
|
@@ -18543,6 +18921,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
|
|
|
18543
18921
|
};
|
|
18544
18922
|
close(vaultAccess);
|
|
18545
18923
|
close(vaultSigner);
|
|
18924
|
+
close(callIdentity);
|
|
18546
18925
|
close(localCache);
|
|
18547
18926
|
lockWallet(wallet);
|
|
18548
18927
|
lockChat(chatPrivateKey, chatPK);
|
|
@@ -18598,6 +18977,7 @@ function createAccountSession(resources = {}) {
|
|
|
18598
18977
|
session.localCache = null;
|
|
18599
18978
|
session.vaultAccess = null;
|
|
18600
18979
|
session.vaultSigner = null;
|
|
18980
|
+
session.callIdentity = null;
|
|
18601
18981
|
closePromise = Promise.allSettled(pending).then(() => {
|
|
18602
18982
|
return;
|
|
18603
18983
|
});
|
|
@@ -18737,6 +19117,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18737
19117
|
let cacheKey = null;
|
|
18738
19118
|
let settingsKey = null;
|
|
18739
19119
|
let vaultSigner = null;
|
|
19120
|
+
let callIdentity = null;
|
|
18740
19121
|
let vaultAccess = null;
|
|
18741
19122
|
let walletBoot = null;
|
|
18742
19123
|
let network;
|
|
@@ -18777,6 +19158,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18777
19158
|
notificationPK = toHex(notificationPublicKey);
|
|
18778
19159
|
cacheKey = getCacheSeed(registry);
|
|
18779
19160
|
vaultSigner = createVaultSigner(masterSeed);
|
|
19161
|
+
callIdentity = createCallIdentity(masterSeed, cloud.environment);
|
|
18780
19162
|
settingsKey = deriveSettingsKey(cacheKey, uid);
|
|
18781
19163
|
mark(diag, "vault.unlock.derive.done", { elapsedMs: Date.now() - deriveStartedAt, source });
|
|
18782
19164
|
cleanBytes(masterSeed);
|
|
@@ -18825,6 +19207,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18825
19207
|
notificationPublicKey,
|
|
18826
19208
|
localCache,
|
|
18827
19209
|
vaultSigner,
|
|
19210
|
+
callIdentity,
|
|
18828
19211
|
vaultAccess,
|
|
18829
19212
|
vaultPK: vaultSigner.publicKey
|
|
18830
19213
|
});
|
|
@@ -18925,7 +19308,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18925
19308
|
return session;
|
|
18926
19309
|
} catch (error) {
|
|
18927
19310
|
walletBoot?.close();
|
|
18928
|
-
await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner });
|
|
19311
|
+
await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity });
|
|
18929
19312
|
throw error;
|
|
18930
19313
|
} finally {
|
|
18931
19314
|
cleanBytes(masterSeed, walletEntropy, chatSeed, cacheKey, settingsKey);
|
|
@@ -18962,88 +19345,6 @@ function hasAgreement(user, contract = CURRENT_AGREEMENT) {
|
|
|
18962
19345
|
return isAgreementAccepted(user?.agreement, contract);
|
|
18963
19346
|
}
|
|
18964
19347
|
|
|
18965
|
-
// ../../core/utils/time.js
|
|
18966
|
-
function timestampMs(value, fallback = null, options = {}) {
|
|
18967
|
-
let ms = null;
|
|
18968
|
-
if (typeof value?.toMillis === "function") {
|
|
18969
|
-
ms = value.toMillis();
|
|
18970
|
-
} else if (value instanceof Date) {
|
|
18971
|
-
ms = value.getTime();
|
|
18972
|
-
} else if (typeof value?.seconds === "number") {
|
|
18973
|
-
ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
|
|
18974
|
-
} else if (typeof value?._seconds === "number") {
|
|
18975
|
-
ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
|
|
18976
|
-
} else if (Number.isFinite(value)) {
|
|
18977
|
-
ms = value;
|
|
18978
|
-
} else if (options.parseString && typeof value === "string") {
|
|
18979
|
-
const numberMs = Number(value);
|
|
18980
|
-
ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
|
|
18981
|
-
}
|
|
18982
|
-
if (!Number.isFinite(ms) || options.positive && ms <= 0) {
|
|
18983
|
-
return fallback;
|
|
18984
|
-
}
|
|
18985
|
-
return ms;
|
|
18986
|
-
}
|
|
18987
|
-
function timestampKey(value) {
|
|
18988
|
-
if (value == null) {
|
|
18989
|
-
return null;
|
|
18990
|
-
}
|
|
18991
|
-
return timestampMs(value, null) ?? String(value);
|
|
18992
|
-
}
|
|
18993
|
-
function makeTimestamp(ms) {
|
|
18994
|
-
return {
|
|
18995
|
-
toMillis() {
|
|
18996
|
-
return ms;
|
|
18997
|
-
},
|
|
18998
|
-
toDate() {
|
|
18999
|
-
return new Date(ms);
|
|
19000
|
-
}
|
|
19001
|
-
};
|
|
19002
|
-
}
|
|
19003
|
-
function twoDigits(value) {
|
|
19004
|
-
return String(value).padStart(2, "0");
|
|
19005
|
-
}
|
|
19006
|
-
function dayKey(date) {
|
|
19007
|
-
return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
|
|
19008
|
-
}
|
|
19009
|
-
function localDayKey(value) {
|
|
19010
|
-
const ms = timestampMs(value, null, { parseString: true });
|
|
19011
|
-
if (!Number.isFinite(ms))
|
|
19012
|
-
return "";
|
|
19013
|
-
return dayKey(new Date(ms));
|
|
19014
|
-
}
|
|
19015
|
-
function hourKey(dateOrHour) {
|
|
19016
|
-
const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
|
|
19017
|
-
return twoDigits(hour);
|
|
19018
|
-
}
|
|
19019
|
-
function dayHourKey(date) {
|
|
19020
|
-
return `${dayKey(date)}-${hourKey(date)}`;
|
|
19021
|
-
}
|
|
19022
|
-
var MINUTE_MS2 = 60000;
|
|
19023
|
-
var HOUR_MS2 = 60 * MINUTE_MS2;
|
|
19024
|
-
function nextLocalDayStartMs(ms) {
|
|
19025
|
-
const date = new Date(ms);
|
|
19026
|
-
date.setDate(date.getDate() + 1);
|
|
19027
|
-
date.setHours(0, 0, 0, 0);
|
|
19028
|
-
return date.getTime();
|
|
19029
|
-
}
|
|
19030
|
-
function nextRowDateTimeRefreshMs(value, now = Date.now()) {
|
|
19031
|
-
const ms = timestampMs(value, null, { parseString: true });
|
|
19032
|
-
const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
|
|
19033
|
-
if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
|
|
19034
|
-
return null;
|
|
19035
|
-
const age = nowMs2 - ms;
|
|
19036
|
-
if (age < -MINUTE_MS2)
|
|
19037
|
-
return ms - MINUTE_MS2;
|
|
19038
|
-
if (age < MINUTE_MS2)
|
|
19039
|
-
return ms + MINUTE_MS2;
|
|
19040
|
-
if (age < HOUR_MS2)
|
|
19041
|
-
return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
|
|
19042
|
-
if (localDayKey(ms) === localDayKey(nowMs2))
|
|
19043
|
-
return nextLocalDayStartMs(nowMs2);
|
|
19044
|
-
return null;
|
|
19045
|
-
}
|
|
19046
|
-
|
|
19047
19348
|
// ../../core/moderation.js
|
|
19048
19349
|
function banUntilMs(ban) {
|
|
19049
19350
|
if (!ban || typeof ban !== "object" || Array.isArray(ban) || ban.until == null) {
|
|
@@ -20954,48 +21255,6 @@ function readAccountBootstrap(value, uid) {
|
|
|
20954
21255
|
};
|
|
20955
21256
|
}
|
|
20956
21257
|
|
|
20957
|
-
// ../../core/chat/state.js
|
|
20958
|
-
function makeCid() {
|
|
20959
|
-
return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
|
|
20960
|
-
}
|
|
20961
|
-
function getMessageKey(message) {
|
|
20962
|
-
return message?.cid || message?.id || null;
|
|
20963
|
-
}
|
|
20964
|
-
function getCidMs(cid) {
|
|
20965
|
-
if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
|
|
20966
|
-
return null;
|
|
20967
|
-
}
|
|
20968
|
-
const base = cid.slice(0, -6);
|
|
20969
|
-
const ms = Number.parseInt(base, 36);
|
|
20970
|
-
return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
|
|
20971
|
-
}
|
|
20972
|
-
function getMessageOrderMs(message) {
|
|
20973
|
-
return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
|
|
20974
|
-
}
|
|
20975
|
-
function sortMessages(messages) {
|
|
20976
|
-
return [...messages].sort((a, b) => {
|
|
20977
|
-
const aMs = getMessageOrderMs(a);
|
|
20978
|
-
const bMs = getMessageOrderMs(b);
|
|
20979
|
-
if (aMs !== bMs) {
|
|
20980
|
-
return aMs - bMs;
|
|
20981
|
-
}
|
|
20982
|
-
return String(a?.id || "").localeCompare(String(b?.id || ""));
|
|
20983
|
-
});
|
|
20984
|
-
}
|
|
20985
|
-
function mergeMessages(...groups) {
|
|
20986
|
-
const merged = new Map;
|
|
20987
|
-
for (const group of groups) {
|
|
20988
|
-
for (const message of group || []) {
|
|
20989
|
-
const key = getMessageKey(message);
|
|
20990
|
-
if (!key) {
|
|
20991
|
-
continue;
|
|
20992
|
-
}
|
|
20993
|
-
merged.set(key, message);
|
|
20994
|
-
}
|
|
20995
|
-
}
|
|
20996
|
-
return sortMessages([...merged.values()]);
|
|
20997
|
-
}
|
|
20998
|
-
|
|
20999
21258
|
// ../../core/chat/ids.js
|
|
21000
21259
|
function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
|
|
21001
21260
|
const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
|
|
@@ -21381,16 +21640,16 @@ function normalizeOpenedAction(epoch, head, action) {
|
|
|
21381
21640
|
};
|
|
21382
21641
|
}
|
|
21383
21642
|
async function openMsgPlaintext(epoch, nonce, ct, aad, options) {
|
|
21384
|
-
const
|
|
21385
|
-
if (typeof
|
|
21386
|
-
return
|
|
21643
|
+
const crypto2 = options?.crypto;
|
|
21644
|
+
if (typeof crypto2?.openBox === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
|
|
21645
|
+
return crypto2.openBox(epoch.bodyKey, nonce, ct, aad);
|
|
21387
21646
|
}
|
|
21388
21647
|
return openBox(epoch.bodyKey, nonce, ct, aad);
|
|
21389
21648
|
}
|
|
21390
21649
|
async function verifyMsgSignature(publicKey, sig, bytes, options) {
|
|
21391
|
-
const
|
|
21392
|
-
if (typeof
|
|
21393
|
-
return
|
|
21650
|
+
const crypto2 = options?.crypto;
|
|
21651
|
+
if (typeof crypto2?.verifyChatBytes === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
|
|
21652
|
+
return crypto2.verifyChatBytes(publicKey, sig, bytes);
|
|
21394
21653
|
}
|
|
21395
21654
|
return verifyChatBytes(publicKey, toHex(sig), bytes);
|
|
21396
21655
|
}
|
|
@@ -21473,9 +21732,9 @@ async function openMessageBatchV3(epoch, records, options = {}) {
|
|
|
21473
21732
|
if (!epoch?.chatId || !epoch?.epochId || !epoch?.bodyKey || !epoch?.manifest) {
|
|
21474
21733
|
throw new Error("chat batch epoch required");
|
|
21475
21734
|
}
|
|
21476
|
-
const
|
|
21477
|
-
if (typeof
|
|
21478
|
-
const opened = await
|
|
21735
|
+
const crypto2 = options.crypto;
|
|
21736
|
+
if (typeof crypto2?.openMessageBatchV3 === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
|
|
21737
|
+
const opened = await crypto2.openMessageBatchV3(makeBatchOpenRequest(epoch, source));
|
|
21479
21738
|
if (!Array.isArray(opened) || opened.length !== source.length) {
|
|
21480
21739
|
throw new Error("invalid chat message batch result");
|
|
21481
21740
|
}
|
|
@@ -22831,20 +23090,6 @@ async function readChatFile(readChatMedia, file) {
|
|
|
22831
23090
|
}
|
|
22832
23091
|
}
|
|
22833
23092
|
|
|
22834
|
-
// ../../core/chat/messages/types.js
|
|
22835
|
-
var ATTACHMENT_MSG_TYPES = ["img", "gif", "m4a", "mp4", "file"];
|
|
22836
|
-
var MAX_TXT_CHARS = CHAT_MAX_TEXT_CHARS;
|
|
22837
|
-
var REACTION_MSG_TYPE = "rxn";
|
|
22838
|
-
var DELETE_MSG_TYPE = "del";
|
|
22839
|
-
var SYSTEM_MSG_TYPE = "sys";
|
|
22840
|
-
var EPOCH_TRANSITION_MSG_TYPE = "epoch";
|
|
22841
|
-
var EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal";
|
|
22842
|
-
var SYSTEM_SETTINGS_KIND = "settings";
|
|
22843
|
-
var DEFAULT_REACTION_EMOJI = "❤️";
|
|
22844
|
-
var MAX_REACTIONS = CHAT_MAX_REACTIONS;
|
|
22845
|
-
var HOLD_VISIBLE_KEY = "__holdVisible";
|
|
22846
|
-
var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
|
|
22847
|
-
|
|
22848
23093
|
// ../../core/username.js
|
|
22849
23094
|
var MAX_USERNAME = USERNAME_MAX_CHARS;
|
|
22850
23095
|
var usernameKeyRegex = /^[a-z0-9]$/i;
|
|
@@ -24493,7 +24738,7 @@ function getChatSettingsEventAvatar(msg) {
|
|
|
24493
24738
|
return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
|
|
24494
24739
|
}
|
|
24495
24740
|
function isSystemMsg(msg) {
|
|
24496
|
-
return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
|
|
24741
|
+
return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || msg?.t === CALL_MSG_TYPE || isMembershipEventMsg(msg));
|
|
24497
24742
|
}
|
|
24498
24743
|
function makeChatInviteNotice(profiles) {
|
|
24499
24744
|
const names = [...new Map(profiles.map((profile) => [profile.chatPK, formatUserDisplay(profile, true)])).values()];
|
|
@@ -24528,6 +24773,8 @@ function isMembershipEventMsg(msg) {
|
|
|
24528
24773
|
return !!getMembershipEvent(msg);
|
|
24529
24774
|
}
|
|
24530
24775
|
function getSystemMsgText(msg) {
|
|
24776
|
+
if (msg?.t === CALL_MSG_TYPE)
|
|
24777
|
+
return callMessageText(msg);
|
|
24531
24778
|
if (isChatInviteNotice(msg)) {
|
|
24532
24779
|
const names = msg.names.length < 2 ? msg.names[0] : `${msg.names.slice(0, -1).join(", ")} and ${msg.names.at(-1)}`;
|
|
24533
24780
|
return `${names} ${msg.names.length === 1 ? "is" : "are"} not accepting chat invitations right now`;
|
|
@@ -24625,6 +24872,8 @@ function canShowMsg(msg) {
|
|
|
24625
24872
|
return hasText(msg.a);
|
|
24626
24873
|
case "pay":
|
|
24627
24874
|
return isChatPayment(msg) || isPendingChatPayment(msg);
|
|
24875
|
+
case CALL_MSG_TYPE:
|
|
24876
|
+
return !!readCallMessage(msg);
|
|
24628
24877
|
case SYSTEM_MSG_TYPE:
|
|
24629
24878
|
return !!getSystemMsgText(msg);
|
|
24630
24879
|
default:
|
|
@@ -24837,8 +25086,12 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
|
|
|
24837
25086
|
const heldExpired = [];
|
|
24838
25087
|
const shorten = [];
|
|
24839
25088
|
let nextExpiryAt = null;
|
|
25089
|
+
const latestStart = latestCallStartKey(projectedMessages.filter(isServerConfirmedMsg));
|
|
24840
25090
|
for (const message of projectedMessages) {
|
|
24841
|
-
if (!isServerConfirmedMsg(message) ||
|
|
25091
|
+
if (!isServerConfirmedMsg(message) || !canShowMsg(message) || message.ttl == null)
|
|
25092
|
+
continue;
|
|
25093
|
+
const call = readCallMessage(message);
|
|
25094
|
+
if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined || options.activeCallId === call.callId))
|
|
24842
25095
|
continue;
|
|
24843
25096
|
const messageMs = messageOrderMs(message);
|
|
24844
25097
|
const epochId = messageEpochId(message);
|
|
@@ -24875,7 +25128,7 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
|
|
|
24875
25128
|
if (expiresAt == null)
|
|
24876
25129
|
continue;
|
|
24877
25130
|
const currentTtlMs = Number.isFinite(message.ttl) ? message.ttl : Number(message.ttl?.toMillis?.());
|
|
24878
|
-
if (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt)
|
|
25131
|
+
if (!isControlMsg(message) && (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt))
|
|
24879
25132
|
shorten.push({ message, expiresAt });
|
|
24880
25133
|
if (expiresAt > now) {
|
|
24881
25134
|
nextExpiryAt = nextExpiryAt == null ? expiresAt : Math.min(nextExpiryAt, expiresAt);
|
|
@@ -25028,6 +25281,8 @@ async function compactMessages({ chatId, maintenance, messages, deletedKeys, pro
|
|
|
25028
25281
|
function sendableMsgPayload(message) {
|
|
25029
25282
|
if (isChatInviteNotice(message))
|
|
25030
25283
|
throw new Error("local chat notices cannot be sent");
|
|
25284
|
+
if (message?.t === CALL_MSG_TYPE && !readCallMessage(message))
|
|
25285
|
+
throw new Error("invalid call message");
|
|
25031
25286
|
const { localData, localUri, pending, failed, peerChatPK, id, from, ts, chatId, chatDraft, protocol, editedAt, editId, editStatus, editOriginal, ...payload } = message || {};
|
|
25032
25287
|
return payload;
|
|
25033
25288
|
}
|
|
@@ -25074,6 +25329,8 @@ function canRenderPreviewContent(preview) {
|
|
|
25074
25329
|
if (preview.t === "pay") {
|
|
25075
25330
|
return isChatPayment(preview) || isPendingChatPayment(preview);
|
|
25076
25331
|
}
|
|
25332
|
+
if (preview.t === CALL_MSG_TYPE)
|
|
25333
|
+
return !!readCallMessage(preview);
|
|
25077
25334
|
if (isAttachmentMsgType(preview?.t)) {
|
|
25078
25335
|
return true;
|
|
25079
25336
|
}
|
|
@@ -25701,8 +25958,9 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
|
|
|
25701
25958
|
const route = routes[member.chatPK] || {};
|
|
25702
25959
|
const descriptor = await sealNotificationDescriptor(member.notificationPK, {
|
|
25703
25960
|
peerTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, member.chatPK),
|
|
25704
|
-
eventId: messageId
|
|
25705
|
-
|
|
25961
|
+
eventId: messageId,
|
|
25962
|
+
kind: fields.presentationKind ?? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
|
|
25963
|
+
}, identity);
|
|
25706
25964
|
const ping = await sealPing(identity, member.chatPK, {
|
|
25707
25965
|
kind: fields.kind,
|
|
25708
25966
|
chatId: epochState.manifest.chatId,
|
|
@@ -25776,6 +26034,7 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
|
|
|
25776
26034
|
cid: head.cid,
|
|
25777
26035
|
record: { lane: epoch.messageLane, head, body, ttlMs },
|
|
25778
26036
|
message: ownerPreview(epoch, identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
|
|
26037
|
+
actionOp,
|
|
25779
26038
|
mentionRecipientChatPKs: mentionTargets.recipientChatPKs,
|
|
25780
26039
|
tsMs
|
|
25781
26040
|
};
|
|
@@ -25809,6 +26068,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
|
|
|
25809
26068
|
msgId: messageId,
|
|
25810
26069
|
record,
|
|
25811
26070
|
message: sentMessage,
|
|
26071
|
+
actionOp,
|
|
25812
26072
|
mentionRecipientChatPKs,
|
|
25813
26073
|
tsMs
|
|
25814
26074
|
} = prepared;
|
|
@@ -25839,8 +26099,9 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
|
|
|
25839
26099
|
shouldPing,
|
|
25840
26100
|
deliverRecipients: options.deliverRecipients !== false,
|
|
25841
26101
|
kind: pingKind,
|
|
26102
|
+
presentationKind: isReactionMsg(sentMessage) && sentMessage.emoji ? NOTIFICATION_PRESENTATION_KINDS.REACTION : actionOp !== CHAT_ACTION_OPS.CREATE || isControlMsg(sentMessage) ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : notificationMessageKind(sentMessage),
|
|
25842
26103
|
ownDeliveryCapability: routeState.capability,
|
|
25843
|
-
silent: options.notify === false,
|
|
26104
|
+
silent: options.notify === false || notificationMessageIsSilent(sentMessage),
|
|
25844
26105
|
recipientChatPKs: deliveryRecipientChatPKs,
|
|
25845
26106
|
attentionRecipientChatPKs: mentionRecipientChatPKs,
|
|
25846
26107
|
tsMs
|
|
@@ -27806,8 +28067,9 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
|
|
|
27806
28067
|
}
|
|
27807
28068
|
const descriptor = await sealNotificationDescriptor(member.notificationPK, {
|
|
27808
28069
|
peerTag: notificationChatTag(nextState2.stateCapability, nextState2.manifest.chatId, member.chatPK),
|
|
27809
|
-
eventId: nextState2.mlsPackageId
|
|
27810
|
-
|
|
28070
|
+
eventId: nextState2.mlsPackageId,
|
|
28071
|
+
kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
|
|
28072
|
+
}, identity);
|
|
27811
28073
|
const ping = await sealPing(identity, member.chatPK, {
|
|
27812
28074
|
kind: "welcome",
|
|
27813
28075
|
chatId: nextState2.manifest.chatId,
|
|
@@ -37904,6 +38166,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
37904
38166
|
const boundary = authorityBoundaries.get(chat?.id);
|
|
37905
38167
|
if (boundary?.terminal)
|
|
37906
38168
|
return false;
|
|
38169
|
+
if (boundary?.leftEpoch != null && !(chat?.epochVersion > boundary.leftEpoch))
|
|
38170
|
+
return false;
|
|
37907
38171
|
if (!chat?.id || !Number.isSafeInteger(sourceRevision))
|
|
37908
38172
|
return true;
|
|
37909
38173
|
if (sourceRevision < sourceRevisionFloor)
|
|
@@ -38067,7 +38331,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38067
38331
|
continue;
|
|
38068
38332
|
if (!sourceCanRemove(chatId, sourceRevision))
|
|
38069
38333
|
continue;
|
|
38070
|
-
if (entry.announcement && !authorityBoundaries.get(chatId)?.owner) {
|
|
38334
|
+
if (entry.announcement && !ownerCoversAnnouncement(authorityBoundaries.get(chatId)?.owner, entry.announcement)) {
|
|
38071
38335
|
entry.cache = null;
|
|
38072
38336
|
continue;
|
|
38073
38337
|
}
|
|
@@ -38153,6 +38417,9 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38153
38417
|
return false;
|
|
38154
38418
|
if (authorityBoundaries.get(announcement.chatId)?.terminal)
|
|
38155
38419
|
return false;
|
|
38420
|
+
const leftEpoch = authorityBoundaries.get(announcement.chatId)?.leftEpoch;
|
|
38421
|
+
if (leftEpoch != null && !(announcement.epochVersion > leftEpoch))
|
|
38422
|
+
return false;
|
|
38156
38423
|
const sources = getSources();
|
|
38157
38424
|
if (sources.pendingDeleteIdsRef.current.has(announcement.chatId))
|
|
38158
38425
|
return false;
|
|
@@ -38211,6 +38478,26 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38211
38478
|
return false;
|
|
38212
38479
|
return removeOwner(chatId, options);
|
|
38213
38480
|
};
|
|
38481
|
+
const leaveOwner = (chatId, epochVersion) => {
|
|
38482
|
+
if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
|
|
38483
|
+
return false;
|
|
38484
|
+
const previous = authorityBoundaries.get(chatId);
|
|
38485
|
+
if (previous?.terminal)
|
|
38486
|
+
return false;
|
|
38487
|
+
if (previous?.owner?.epochVersion > epochVersion)
|
|
38488
|
+
return false;
|
|
38489
|
+
authorityRevision += 1;
|
|
38490
|
+
authorityBoundaries.set(chatId, {
|
|
38491
|
+
owner: previous?.owner || structuralChat(ownerFor(entries.get(chatId))),
|
|
38492
|
+
revision: authorityRevision,
|
|
38493
|
+
terminal: false,
|
|
38494
|
+
leftEpoch: Math.max(previous?.leftEpoch || 0, epochVersion)
|
|
38495
|
+
});
|
|
38496
|
+
entries.delete(chatId);
|
|
38497
|
+
clearRemovedOwners([chatId]);
|
|
38498
|
+
publishOwners({ warm: false });
|
|
38499
|
+
return true;
|
|
38500
|
+
};
|
|
38214
38501
|
const getOwnerChat = (chatId) => {
|
|
38215
38502
|
if (!chatId || isHiddenChatId(chatId))
|
|
38216
38503
|
return null;
|
|
@@ -38397,6 +38684,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38397
38684
|
observeMessages,
|
|
38398
38685
|
patchRowSettings,
|
|
38399
38686
|
removeOwner,
|
|
38687
|
+
leaveOwner,
|
|
38400
38688
|
removeInboxOwner,
|
|
38401
38689
|
render,
|
|
38402
38690
|
reset,
|
|
@@ -41712,10 +42000,10 @@ function createChatSessionMembership({
|
|
|
41712
42000
|
await cloud.delivery.revoke(capability).catch(() => false);
|
|
41713
42001
|
assertCurrent(operation);
|
|
41714
42002
|
if (retirementStarted) {
|
|
41715
|
-
operation.deleteActions.commitLeftChat(chatId);
|
|
42003
|
+
operation.deleteActions.commitLeftChat(chatId, chat.epochVersion);
|
|
41716
42004
|
retirementStarted = false;
|
|
41717
42005
|
} else
|
|
41718
|
-
operation.deleteActions.dropLeftChat(chatId);
|
|
42006
|
+
operation.deleteActions.dropLeftChat(chatId, chat.epochVersion);
|
|
41719
42007
|
return { left: true, message: proposal.message };
|
|
41720
42008
|
} catch (error) {
|
|
41721
42009
|
if (retirementStarted && isCurrent(operation)) {
|
|
@@ -41967,8 +42255,9 @@ async function deliverChatDeletedPings(cloud, identityValue, epochState, members
|
|
|
41967
42255
|
continue;
|
|
41968
42256
|
const descriptor = await sealNotificationDescriptor(member.notificationPK, {
|
|
41969
42257
|
peerTag: notificationChatTag(epochState.stateCapability, manifest.chatId, member.chatPK),
|
|
41970
|
-
eventId
|
|
41971
|
-
|
|
42258
|
+
eventId,
|
|
42259
|
+
kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
|
|
42260
|
+
}, identity);
|
|
41972
42261
|
const ping = await sealPing(identity, member.chatPK, {
|
|
41973
42262
|
kind: "chat_deleted",
|
|
41974
42263
|
chatId: manifest.chatId,
|
|
@@ -42121,20 +42410,20 @@ function createChatDelete({
|
|
|
42121
42410
|
return;
|
|
42122
42411
|
listActionsRef.current?.removeOwner?.(chatId, { warm: false });
|
|
42123
42412
|
};
|
|
42124
|
-
const dropLeftChat = (chatId) => {
|
|
42413
|
+
const dropLeftChat = (chatId, epochVersion) => {
|
|
42125
42414
|
if (!chatId)
|
|
42126
42415
|
return false;
|
|
42127
|
-
|
|
42416
|
+
listActionsRef.current?.leaveOwner?.(chatId, epochVersion);
|
|
42128
42417
|
return true;
|
|
42129
42418
|
};
|
|
42130
|
-
const commitLeftChat = (chatId) => {
|
|
42419
|
+
const commitLeftChat = (chatId, epochVersion) => {
|
|
42131
42420
|
if (!chatId)
|
|
42132
42421
|
return false;
|
|
42133
42422
|
pendingDeleteIdsRef.current.delete(chatId);
|
|
42134
42423
|
locallyDeletedChatIdsRef.current.delete(chatId);
|
|
42135
42424
|
deletedChatIdsRef.current.delete(chatId);
|
|
42136
42425
|
keepSelectedDeletedChatIdsRef.current.delete(chatId);
|
|
42137
|
-
|
|
42426
|
+
dropLeftChat(chatId, epochVersion);
|
|
42138
42427
|
return true;
|
|
42139
42428
|
};
|
|
42140
42429
|
const dropUnavailableChat = (chatId) => {
|
|
@@ -43699,6 +43988,7 @@ function createChatSession({
|
|
|
43699
43988
|
mls = null,
|
|
43700
43989
|
live = null,
|
|
43701
43990
|
maintenance,
|
|
43991
|
+
resolveActiveCall,
|
|
43702
43992
|
incomingChatDecision = null,
|
|
43703
43993
|
getPeerProfile = null,
|
|
43704
43994
|
refreshPeerProfile = null,
|
|
@@ -44474,7 +44764,6 @@ function createChatSession({
|
|
|
44474
44764
|
assertBulkOperationCurrent(operation);
|
|
44475
44765
|
await cloud.user.chats.unlink(identity.uid, chat.entryId);
|
|
44476
44766
|
assertBulkOperationCurrent(operation);
|
|
44477
|
-
operation.deleteActions.commitRetirement(chat.id);
|
|
44478
44767
|
} else {
|
|
44479
44768
|
await operation.deleteActions.deleteChat(chat, { cleanup: false });
|
|
44480
44769
|
}
|
|
@@ -44561,6 +44850,26 @@ function createChatSession({
|
|
|
44561
44850
|
return false;
|
|
44562
44851
|
}
|
|
44563
44852
|
};
|
|
44853
|
+
const prepareCall = async (chatId) => {
|
|
44854
|
+
const generation = authGeneration;
|
|
44855
|
+
const current = () => generation === authGeneration && online && pendingOwner.canSendToChat(chatId);
|
|
44856
|
+
if (!current())
|
|
44857
|
+
throw new Error("chat unavailable");
|
|
44858
|
+
await pendingOwner.assertChatAdmission(chatId);
|
|
44859
|
+
if (!getOwnerChat(chatId)?.epochState)
|
|
44860
|
+
await ensureChat(chatId);
|
|
44861
|
+
if (!current())
|
|
44862
|
+
throw new Error("chat unavailable");
|
|
44863
|
+
const reconciliation = await chatListOwner.reconcileChatEpoch(chatId);
|
|
44864
|
+
if (!current() || reconciliation?.removed)
|
|
44865
|
+
throw new Error("chat unavailable");
|
|
44866
|
+
await pendingOwner.assertChatAdmission(chatId);
|
|
44867
|
+
const chat = getVisibleOwnerChat(chatId);
|
|
44868
|
+
if (!current() || !chat?.epochState)
|
|
44869
|
+
throw new Error("chat unavailable");
|
|
44870
|
+
return chat.epochState;
|
|
44871
|
+
};
|
|
44872
|
+
const materializeChat = (chatId) => pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
|
|
44564
44873
|
const flushChatReadFrontier = (chatId) => {
|
|
44565
44874
|
if (!online)
|
|
44566
44875
|
return false;
|
|
@@ -44843,6 +45152,8 @@ function createChatSession({
|
|
|
44843
45152
|
markChatTyping,
|
|
44844
45153
|
hasChat: hasVisibleChat,
|
|
44845
45154
|
getOwnerChat: getVisibleOwnerChat,
|
|
45155
|
+
prepareCall,
|
|
45156
|
+
materializeChat,
|
|
44846
45157
|
sendOptionsForPeer,
|
|
44847
45158
|
getLocalMessages,
|
|
44848
45159
|
wasChatDeletedLocally,
|
|
@@ -44876,6 +45187,7 @@ function createChatSession({
|
|
|
44876
45187
|
deleteMessage,
|
|
44877
45188
|
deleteMessages,
|
|
44878
45189
|
deleteMessageDocs,
|
|
45190
|
+
resolveActiveCall,
|
|
44879
45191
|
setChatTtl,
|
|
44880
45192
|
makeMessagePermanent,
|
|
44881
45193
|
makeMessageTemporary,
|
|
@@ -44933,6 +45245,7 @@ function createChatSession({
|
|
|
44933
45245
|
stateOwner.setListSnapshot(chatListOwner.getSnapshot());
|
|
44934
45246
|
deleteListActionsRef.current = {
|
|
44935
45247
|
removeOwner: (...args) => chatListOwner.removeOwner(...args),
|
|
45248
|
+
leaveOwner: (...args) => chatListOwner.leaveOwner(...args),
|
|
44936
45249
|
render: (...args) => chatListOwner.render(...args)
|
|
44937
45250
|
};
|
|
44938
45251
|
actionOwner.createActionOwners();
|
|
@@ -45654,6 +45967,2695 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
|
|
|
45654
45967
|
});
|
|
45655
45968
|
}
|
|
45656
45969
|
|
|
45970
|
+
// ../../core/calls/protocol.js
|
|
45971
|
+
var CALL_MAX_PARTICIPANTS = 16;
|
|
45972
|
+
var CALL_TTL_MS = 120000;
|
|
45973
|
+
var CALL_ADMISSION_TIMEOUT_MS = 30000;
|
|
45974
|
+
var HEX16 = /^[0-9a-f]{32}$/u;
|
|
45975
|
+
var HEX32 = /^[0-9a-f]{64}$/u;
|
|
45976
|
+
function bytes(packet) {
|
|
45977
|
+
const { signature: _signature, ...body } = packet;
|
|
45978
|
+
return canonicalBytes(["veyl.call.packet.v1", body]);
|
|
45979
|
+
}
|
|
45980
|
+
function createCallProtocol({ realm: realm2, epochState, identity, endpoint, callId = "" }) {
|
|
45981
|
+
const { manifest } = epochState;
|
|
45982
|
+
const members = new Map(manifest.members.map((member) => [member.chatSigningPK, member]));
|
|
45983
|
+
if (!members.has(identity.chatSigningPK))
|
|
45984
|
+
throw new Error("not a chat member");
|
|
45985
|
+
const actor = identity.chatSigningPK;
|
|
45986
|
+
const secret = identity.chatSigningSecret.slice();
|
|
45987
|
+
let closed = false;
|
|
45988
|
+
const context = { v: 1, realm: realm2, chatId: manifest.chatId, epochId: manifest.epochId, callId };
|
|
45989
|
+
return Object.freeze({
|
|
45990
|
+
sign(kind, data) {
|
|
45991
|
+
if (closed)
|
|
45992
|
+
throw new Error("call protocol closed");
|
|
45993
|
+
const packet = {
|
|
45994
|
+
...context,
|
|
45995
|
+
actor,
|
|
45996
|
+
holder: endpoint.holder,
|
|
45997
|
+
endpoint: endpoint.publicKey,
|
|
45998
|
+
nonce: toHex(randomBytes3(16)),
|
|
45999
|
+
at: Date.now(),
|
|
46000
|
+
kind,
|
|
46001
|
+
data
|
|
46002
|
+
};
|
|
46003
|
+
return { ...packet, signature: signChatBytes({ secret, publicKey: actor }, bytes(packet)) };
|
|
46004
|
+
},
|
|
46005
|
+
verify(packet, { fresh = true } = {}) {
|
|
46006
|
+
if (!packet || Object.keys(context).some((key) => packet[key] !== context[key]) || !members.has(packet.actor) || !HEX16.test(packet.holder) || !HEX32.test(packet.endpoint) || !HEX16.test(packet.nonce) || !Number.isSafeInteger(packet.at) || fresh && (packet.at > Date.now() + 5000 || packet.at < Date.now() - CALL_TTL_MS) || !verifyChatBytes(packet.actor, packet.signature, bytes(packet)))
|
|
46007
|
+
throw new Error("invalid call participant proof");
|
|
46008
|
+
return members.get(packet.actor);
|
|
46009
|
+
},
|
|
46010
|
+
close() {
|
|
46011
|
+
if (!closed) {
|
|
46012
|
+
closed = true;
|
|
46013
|
+
cleanBytes(secret);
|
|
46014
|
+
}
|
|
46015
|
+
}
|
|
46016
|
+
});
|
|
46017
|
+
}
|
|
46018
|
+
function verifyCallAdmissions(admissions, protocol) {
|
|
46019
|
+
if (!Array.isArray(admissions) || !admissions.length || admissions.length > CALL_MAX_PARTICIPANTS)
|
|
46020
|
+
throw new Error("call membership mismatch");
|
|
46021
|
+
const holders = new Set;
|
|
46022
|
+
const identities = new Set;
|
|
46023
|
+
const members = new Map;
|
|
46024
|
+
for (const admission of admissions) {
|
|
46025
|
+
const identity = protocol.verify(admission, { fresh: false });
|
|
46026
|
+
if (admission.kind !== "join" || !HEX32.test(admission.data?.signaturePK) || admission.data.replaces != null && !HEX16.test(admission.data.replaces) || holders.has(admission.holder) || identities.has(admission.actor))
|
|
46027
|
+
throw new Error("invalid call participant");
|
|
46028
|
+
holders.add(admission.holder);
|
|
46029
|
+
identities.add(admission.actor);
|
|
46030
|
+
members.set(admission.holder, identity);
|
|
46031
|
+
}
|
|
46032
|
+
return members;
|
|
46033
|
+
}
|
|
46034
|
+
function verifyCallMembers(mlsState, admissions, protocol) {
|
|
46035
|
+
const members = verifyCallAdmissions(admissions, protocol);
|
|
46036
|
+
if (mlsState.members.length !== admissions.length)
|
|
46037
|
+
throw new Error("call membership mismatch");
|
|
46038
|
+
for (const admission of admissions) {
|
|
46039
|
+
const member = mlsState.members.find((item) => toHex(item.leafId) === admission.holder);
|
|
46040
|
+
if (!member || toHex(member.signaturePK) !== admission.data.signaturePK)
|
|
46041
|
+
throw new Error("call leaf identity mismatch");
|
|
46042
|
+
}
|
|
46043
|
+
return members;
|
|
46044
|
+
}
|
|
46045
|
+
function verifyCallReplacement(packet, currentHead, protocol) {
|
|
46046
|
+
protocol.verify(packet, { fresh: false });
|
|
46047
|
+
const previous = packet.data?.previous;
|
|
46048
|
+
const replacement = packet.data?.participant;
|
|
46049
|
+
protocol.verify(previous, { fresh: false });
|
|
46050
|
+
if (packet.kind !== "replace" || previous.kind !== "head" || previous.data?.participants?.length !== 1)
|
|
46051
|
+
throw new Error("invalid call replacement");
|
|
46052
|
+
verifyCallAdmissions(previous.data.participants, protocol);
|
|
46053
|
+
verifyCallAdmissions([replacement], protocol);
|
|
46054
|
+
const original = previous.data.participants[0];
|
|
46055
|
+
if (previous.actor !== original.actor || previous.holder !== original.holder || previous.endpoint !== original.endpoint || packet.actor !== original.actor || replacement.actor !== original.actor || packet.holder !== replacement.holder || packet.endpoint !== replacement.endpoint || replacement.holder === original.holder || replacement.data.replaces !== original.holder)
|
|
46056
|
+
throw new Error("invalid call replacement");
|
|
46057
|
+
if (currentHead && (currentHead.epoch !== previous.data.epoch || currentHead.participants.length !== 1 || currentHead.participants[0].signature !== original.signature))
|
|
46058
|
+
throw new Error("stale call replacement");
|
|
46059
|
+
return { epoch: 0, participants: [replacement] };
|
|
46060
|
+
}
|
|
46061
|
+
|
|
46062
|
+
// ../../core/calls/mailbox.js
|
|
46063
|
+
function openCallMailbox({ cloud, capability, endpoint, protocol, onChange, onError, stream = true }) {
|
|
46064
|
+
let closed = false;
|
|
46065
|
+
let record = null;
|
|
46066
|
+
let candidate = null;
|
|
46067
|
+
let tail = Promise.resolve();
|
|
46068
|
+
let cursor = 0;
|
|
46069
|
+
const decode = async (value, fresh = true, maximum) => {
|
|
46070
|
+
const packet = await capability.open(decodeCallBytes(value, maximum));
|
|
46071
|
+
protocol.verify(packet, { fresh });
|
|
46072
|
+
return packet;
|
|
46073
|
+
};
|
|
46074
|
+
const accept = async (value) => {
|
|
46075
|
+
if (closed)
|
|
46076
|
+
return;
|
|
46077
|
+
if (!Number.isSafeInteger(value?.revision) || !Number.isSafeInteger(value?.cursor) || value.revision < 0 || value.cursor < 0 || !Array.isArray(value.events))
|
|
46078
|
+
throw new Error("invalid call mailbox");
|
|
46079
|
+
if (record && value.revision < record.revision)
|
|
46080
|
+
return;
|
|
46081
|
+
if (value.cursor < cursor)
|
|
46082
|
+
throw new Error("invalid call event order");
|
|
46083
|
+
if (value.events.some((item) => !Number.isSafeInteger(item?.cursor) || item.cursor < 1 || item.cursor > value.cursor))
|
|
46084
|
+
throw new Error("invalid call event order");
|
|
46085
|
+
const empty = value.headDigest === null && value.payload === null && value.proof === null && value.events.length === 0;
|
|
46086
|
+
const unseen = value.events.filter((item) => item.cursor > cursor);
|
|
46087
|
+
const historyLost = !!(!empty && cursor && value.cursor > cursor && (!unseen.length || unseen[0].cursor !== cursor + 1));
|
|
46088
|
+
if (historyLost && (stream || value.truncated !== true))
|
|
46089
|
+
throw new Error("call signaling history unavailable");
|
|
46090
|
+
let head = null;
|
|
46091
|
+
let payload = null;
|
|
46092
|
+
if (value.headDigest !== null) {
|
|
46093
|
+
if (typeof value.headDigest !== "string" || !/^[a-f0-9]{64}$/u.test(value.headDigest))
|
|
46094
|
+
throw new Error("invalid call head digest");
|
|
46095
|
+
const proof = await decode(value.proof, true, CALL_HEAD_PROOF_MAX_BYTES);
|
|
46096
|
+
if (proof.kind !== "head-proof" || proof.data?.digest !== value.headDigest)
|
|
46097
|
+
throw new Error("invalid call head proof");
|
|
46098
|
+
if (record?.headDigest === value.headDigest && record.head) {
|
|
46099
|
+
if (Object.hasOwn(value, "payload") && callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
|
|
46100
|
+
throw new Error("invalid call head digest");
|
|
46101
|
+
head = record.head;
|
|
46102
|
+
payload = record.payload;
|
|
46103
|
+
} else {
|
|
46104
|
+
if (!value.payload || callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
|
|
46105
|
+
throw new Error("missing call head");
|
|
46106
|
+
head = await decode(value.payload, false);
|
|
46107
|
+
if (head.kind !== "head")
|
|
46108
|
+
throw new Error("invalid call head");
|
|
46109
|
+
payload = value.payload;
|
|
46110
|
+
}
|
|
46111
|
+
} else if (value.payload !== null || value.proof !== null)
|
|
46112
|
+
throw new Error("invalid empty call head");
|
|
46113
|
+
const events = [];
|
|
46114
|
+
let nextCursor = empty ? value.cursor : historyLost ? 0 : cursor;
|
|
46115
|
+
for (const item of unseen) {
|
|
46116
|
+
if (!Number.isSafeInteger(item.cursor) || item.cursor < 1 || nextCursor && item.cursor !== nextCursor + 1)
|
|
46117
|
+
throw new Error("invalid call event order");
|
|
46118
|
+
const packet = await decode(item.payload, false, CALL_EVENT_MAX_BYTES);
|
|
46119
|
+
events.push(packet);
|
|
46120
|
+
nextCursor = item.cursor;
|
|
46121
|
+
}
|
|
46122
|
+
if (nextCursor !== value.cursor)
|
|
46123
|
+
throw new Error("call signaling history unavailable");
|
|
46124
|
+
if (closed)
|
|
46125
|
+
return;
|
|
46126
|
+
candidate = { ...value, payload, head, events, historyLost };
|
|
46127
|
+
try {
|
|
46128
|
+
await onChange?.(candidate);
|
|
46129
|
+
if (closed)
|
|
46130
|
+
return;
|
|
46131
|
+
cursor = nextCursor;
|
|
46132
|
+
record = candidate;
|
|
46133
|
+
} finally {
|
|
46134
|
+
candidate = null;
|
|
46135
|
+
}
|
|
46136
|
+
};
|
|
46137
|
+
const consume = (value) => {
|
|
46138
|
+
const result = tail.then(() => accept(value));
|
|
46139
|
+
tail = result.catch((error) => {
|
|
46140
|
+
if (!closed)
|
|
46141
|
+
onError?.(error);
|
|
46142
|
+
});
|
|
46143
|
+
return result;
|
|
46144
|
+
};
|
|
46145
|
+
let channel;
|
|
46146
|
+
try {
|
|
46147
|
+
channel = cloud.calls.connect({
|
|
46148
|
+
capability,
|
|
46149
|
+
endpoint,
|
|
46150
|
+
onSnapshot: consume,
|
|
46151
|
+
onError,
|
|
46152
|
+
stream,
|
|
46153
|
+
getCheckpoint: () => ({ cursor, headDigest: record?.headDigest ?? null })
|
|
46154
|
+
});
|
|
46155
|
+
} catch (error) {
|
|
46156
|
+
capability.close();
|
|
46157
|
+
protocol.close?.();
|
|
46158
|
+
throw error;
|
|
46159
|
+
}
|
|
46160
|
+
async function mutate(headDigest, payload, events, { expectedRevision = (candidate || record)?.revision ?? 0, ttlMs = CALL_TTL_MS } = {}) {
|
|
46161
|
+
if (!stream)
|
|
46162
|
+
throw new Error("read-only call mailbox");
|
|
46163
|
+
const proof = encodeCallBytes(await capability.seal(protocol.sign("head-proof", { digest: headDigest })));
|
|
46164
|
+
const packets = await Promise.all(events.map(async (packet) => encodeCallBytes(await capability.seal(packet))));
|
|
46165
|
+
if (closed)
|
|
46166
|
+
throw new Error("call mailbox closed");
|
|
46167
|
+
const command = {
|
|
46168
|
+
type: "commit",
|
|
46169
|
+
expectedRevision,
|
|
46170
|
+
operation: toHex(randomBytes3(16)),
|
|
46171
|
+
headDigest,
|
|
46172
|
+
proof,
|
|
46173
|
+
...payload !== undefined ? { payload } : {},
|
|
46174
|
+
events: packets,
|
|
46175
|
+
ttlMs
|
|
46176
|
+
};
|
|
46177
|
+
let response;
|
|
46178
|
+
try {
|
|
46179
|
+
response = await channel.request(command);
|
|
46180
|
+
} catch (error) {
|
|
46181
|
+
if (closed || !["calls/connection", "calls/timeout"].includes(error?.code))
|
|
46182
|
+
throw error;
|
|
46183
|
+
response = await channel.request(command);
|
|
46184
|
+
}
|
|
46185
|
+
if (closed)
|
|
46186
|
+
throw new Error("call mailbox closed");
|
|
46187
|
+
consume(response.record).catch(() => {});
|
|
46188
|
+
return response.record;
|
|
46189
|
+
}
|
|
46190
|
+
return Object.freeze({
|
|
46191
|
+
getSnapshot: () => candidate || record,
|
|
46192
|
+
async read() {
|
|
46193
|
+
const response = await channel.request({ type: "read", cursor, headDigest: record?.headDigest ?? null });
|
|
46194
|
+
await consume(response.record);
|
|
46195
|
+
return record;
|
|
46196
|
+
},
|
|
46197
|
+
async commit(head, events = [], options) {
|
|
46198
|
+
const bytes2 = await capability.seal(protocol.sign("head", head));
|
|
46199
|
+
return mutate(callHeadDigest(bytes2), encodeCallBytes(bytes2), events, options);
|
|
46200
|
+
},
|
|
46201
|
+
async append(events, options) {
|
|
46202
|
+
const current = candidate || record;
|
|
46203
|
+
if (!current?.headDigest || !current.head)
|
|
46204
|
+
throw new Error("call ended");
|
|
46205
|
+
return mutate(current.headDigest, undefined, events, options);
|
|
46206
|
+
},
|
|
46207
|
+
close() {
|
|
46208
|
+
closed = true;
|
|
46209
|
+
channel.close();
|
|
46210
|
+
capability.close();
|
|
46211
|
+
protocol.close?.();
|
|
46212
|
+
}
|
|
46213
|
+
});
|
|
46214
|
+
}
|
|
46215
|
+
|
|
46216
|
+
// ../../core/calls/ownership.js
|
|
46217
|
+
var CALL_OWNERSHIP_LEASE_MS = 15000;
|
|
46218
|
+
|
|
46219
|
+
// ../../core/calls/clock.js
|
|
46220
|
+
function callLeaseClock() {
|
|
46221
|
+
return {
|
|
46222
|
+
wall: Date.now(),
|
|
46223
|
+
monotonic: globalThis.performance.timeOrigin + globalThis.performance.now()
|
|
46224
|
+
};
|
|
46225
|
+
}
|
|
46226
|
+
|
|
46227
|
+
// ../../core/calls/leaseattempt.js
|
|
46228
|
+
var STOP_MARGIN_MS = 250;
|
|
46229
|
+
function validTime(value) {
|
|
46230
|
+
return Number.isFinite(value) && value >= 0;
|
|
46231
|
+
}
|
|
46232
|
+
function createCallLeaseAttempt(command, clock = callLeaseClock) {
|
|
46233
|
+
if (!["claim", "activate", "renew"].includes(command?.type))
|
|
46234
|
+
throw new Error("call lease command required");
|
|
46235
|
+
if (command.operation !== undefined)
|
|
46236
|
+
throw new Error("reuse the original call lease attempt");
|
|
46237
|
+
const revision = command.type === "claim" ? command.expectedRevision + 1 : command.revision;
|
|
46238
|
+
const sequence = command.type === "renew" ? command.sequence : 0;
|
|
46239
|
+
if (!/^[a-f0-9]{32}$/u.test(command.holder || "") || !Number.isSafeInteger(revision) || revision < 1 || !Number.isSafeInteger(sequence) || sequence < (command.type === "renew" ? 1 : 0))
|
|
46240
|
+
throw new Error("invalid call lease command");
|
|
46241
|
+
const started = clock();
|
|
46242
|
+
if (!validTime(started?.wall) || !validTime(started?.monotonic))
|
|
46243
|
+
throw new Error("call lease clock unavailable");
|
|
46244
|
+
const deadline = Object.freeze({
|
|
46245
|
+
sessionId: command.holder,
|
|
46246
|
+
revision,
|
|
46247
|
+
wallDeadline: started.wall + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS,
|
|
46248
|
+
monotonicDeadline: started.monotonic + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS
|
|
46249
|
+
});
|
|
46250
|
+
const startWall = started.wall;
|
|
46251
|
+
const startMonotonic = started.monotonic;
|
|
46252
|
+
const operation = toHex(randomBytes3(16));
|
|
46253
|
+
const request = Object.freeze({ ...command, ...command.payload ? { payload: command.payload.slice() } : {}, operation });
|
|
46254
|
+
let cancelled2 = false;
|
|
46255
|
+
return Object.freeze({
|
|
46256
|
+
command: request,
|
|
46257
|
+
cancel() {
|
|
46258
|
+
cancelled2 = true;
|
|
46259
|
+
},
|
|
46260
|
+
grant(record) {
|
|
46261
|
+
if (cancelled2)
|
|
46262
|
+
return null;
|
|
46263
|
+
const now = clock();
|
|
46264
|
+
if (!validTime(now?.wall) || !validTime(now?.monotonic) || now.wall < startWall || now.monotonic < startMonotonic || now.wall >= deadline.wallDeadline || now.monotonic >= deadline.monotonicDeadline) {
|
|
46265
|
+
cancelled2 = true;
|
|
46266
|
+
return null;
|
|
46267
|
+
}
|
|
46268
|
+
const active = record?.active;
|
|
46269
|
+
if (record?.pending || record?.revision !== revision || active?.revision !== revision || active.holder !== deadline.sessionId || active.sequence !== sequence || active.operation !== operation)
|
|
46270
|
+
return null;
|
|
46271
|
+
return deadline;
|
|
46272
|
+
}
|
|
46273
|
+
});
|
|
46274
|
+
}
|
|
46275
|
+
|
|
46276
|
+
// ../../core/calls/accountlease.js
|
|
46277
|
+
var transient = (error) => ["calls/connection", "calls/timeout"].includes(error?.code);
|
|
46278
|
+
function openAccountCallLease({ cloud, capability: accountCapability, endpoint, clock = callLeaseClock, onRecord, onGrant, onLost, onError }) {
|
|
46279
|
+
const capability = accountCapability.retain();
|
|
46280
|
+
let closed = false;
|
|
46281
|
+
let record = null;
|
|
46282
|
+
let owned = null;
|
|
46283
|
+
let ownedDeadline = null;
|
|
46284
|
+
let retiring = null;
|
|
46285
|
+
let renewal = null;
|
|
46286
|
+
let expiry = null;
|
|
46287
|
+
let generation = 0;
|
|
46288
|
+
let acquiring = false;
|
|
46289
|
+
const attempts = new Set;
|
|
46290
|
+
const waiters = new Set;
|
|
46291
|
+
const current = () => !closed;
|
|
46292
|
+
const wake = () => {
|
|
46293
|
+
for (const resolve of [...waiters])
|
|
46294
|
+
resolve();
|
|
46295
|
+
};
|
|
46296
|
+
function waitForChange(milliseconds) {
|
|
46297
|
+
return new Promise((resolve) => {
|
|
46298
|
+
const finish = () => {
|
|
46299
|
+
clearTimeout(timer);
|
|
46300
|
+
waiters.delete(finish);
|
|
46301
|
+
resolve();
|
|
46302
|
+
};
|
|
46303
|
+
const timer = setTimeout(finish, milliseconds);
|
|
46304
|
+
waiters.add(finish);
|
|
46305
|
+
});
|
|
46306
|
+
}
|
|
46307
|
+
function stop() {
|
|
46308
|
+
generation++;
|
|
46309
|
+
clearTimeout(renewal);
|
|
46310
|
+
clearTimeout(expiry);
|
|
46311
|
+
renewal = expiry = null;
|
|
46312
|
+
for (const attempt of attempts)
|
|
46313
|
+
attempt.cancel();
|
|
46314
|
+
attempts.clear();
|
|
46315
|
+
const previous = owned;
|
|
46316
|
+
owned = ownedDeadline = null;
|
|
46317
|
+
wake();
|
|
46318
|
+
return previous;
|
|
46319
|
+
}
|
|
46320
|
+
async function accept(value) {
|
|
46321
|
+
if (closed)
|
|
46322
|
+
return;
|
|
46323
|
+
const next = decodeCallRecord(value);
|
|
46324
|
+
if (!Number.isSafeInteger(next?.version) || next.version < 0)
|
|
46325
|
+
throw new Error("invalid call ownership version");
|
|
46326
|
+
if (record && next.version <= record.version)
|
|
46327
|
+
return;
|
|
46328
|
+
record = next;
|
|
46329
|
+
wake();
|
|
46330
|
+
if (owned && (next.pending || next.active?.holder !== endpoint.holder || next.active?.revision !== owned.revision)) {
|
|
46331
|
+
retiring = stop();
|
|
46332
|
+
onLost?.();
|
|
46333
|
+
}
|
|
46334
|
+
const active = next.pending || next.active;
|
|
46335
|
+
const descriptor = active ? await capability.open(active.payload) : null;
|
|
46336
|
+
if (current() && record === next)
|
|
46337
|
+
onRecord?.(next, descriptor);
|
|
46338
|
+
}
|
|
46339
|
+
let channel;
|
|
46340
|
+
try {
|
|
46341
|
+
channel = cloud.calls.connect({
|
|
46342
|
+
capability,
|
|
46343
|
+
endpoint,
|
|
46344
|
+
onSnapshot: (value) => accept(value).catch(onError),
|
|
46345
|
+
onError(error) {
|
|
46346
|
+
if (!transient(error))
|
|
46347
|
+
onError?.(error);
|
|
46348
|
+
}
|
|
46349
|
+
});
|
|
46350
|
+
} catch (error) {
|
|
46351
|
+
capability.close();
|
|
46352
|
+
throw error;
|
|
46353
|
+
}
|
|
46354
|
+
async function request(command) {
|
|
46355
|
+
const response = await channel.request({ ...command, ...command.payload ? { payload: encodeCallBytes(command.payload) } : {} });
|
|
46356
|
+
await accept(response.record);
|
|
46357
|
+
return decodeCallRecord(response.record);
|
|
46358
|
+
}
|
|
46359
|
+
function renewalRemaining(started) {
|
|
46360
|
+
if (!current() || started !== generation || !owned || !ownedDeadline)
|
|
46361
|
+
return 0;
|
|
46362
|
+
const now = clock();
|
|
46363
|
+
if (!Number.isFinite(now?.wall) || !Number.isFinite(now?.monotonic) || now.wall < 0 || now.monotonic < 0)
|
|
46364
|
+
return 0;
|
|
46365
|
+
return Math.max(0, Math.min(ownedDeadline.wallDeadline - now.wall, ownedDeadline.monotonicDeadline - now.monotonic));
|
|
46366
|
+
}
|
|
46367
|
+
async function requestGrant(attempt, started) {
|
|
46368
|
+
let delay = 250;
|
|
46369
|
+
for (;; ) {
|
|
46370
|
+
if (attempt.command.type === "renew" && !renewalRemaining(started))
|
|
46371
|
+
throw new Error("call ownership expired");
|
|
46372
|
+
try {
|
|
46373
|
+
const value = await request(attempt.command);
|
|
46374
|
+
if (attempt.command.type === "renew" && !renewalRemaining(started))
|
|
46375
|
+
throw new Error("call ownership expired");
|
|
46376
|
+
return value;
|
|
46377
|
+
} catch (error) {
|
|
46378
|
+
const remaining = renewalRemaining(started);
|
|
46379
|
+
if (attempt.command.type !== "renew" || !transient(error) || !remaining)
|
|
46380
|
+
throw error;
|
|
46381
|
+
await waitForChange(Math.min(delay, remaining));
|
|
46382
|
+
delay = Math.min(delay * 2, 1000);
|
|
46383
|
+
}
|
|
46384
|
+
}
|
|
46385
|
+
}
|
|
46386
|
+
async function grant(command) {
|
|
46387
|
+
if (closed)
|
|
46388
|
+
throw new Error("call lease closed");
|
|
46389
|
+
const attempt = createCallLeaseAttempt(command, clock);
|
|
46390
|
+
const started = generation;
|
|
46391
|
+
attempts.add(attempt);
|
|
46392
|
+
try {
|
|
46393
|
+
const value = await requestGrant(attempt, started);
|
|
46394
|
+
const deadline = attempt.grant(value);
|
|
46395
|
+
if (!current() || started !== generation || !deadline || record.version !== value.version)
|
|
46396
|
+
throw new Error("call ownership unavailable");
|
|
46397
|
+
owned = value.active;
|
|
46398
|
+
await onGrant(deadline);
|
|
46399
|
+
if (!current() || owned !== value.active)
|
|
46400
|
+
throw new Error("call ownership changed");
|
|
46401
|
+
ownedDeadline = deadline;
|
|
46402
|
+
clearTimeout(expiry);
|
|
46403
|
+
expiry = setTimeout(() => {
|
|
46404
|
+
stop();
|
|
46405
|
+
onLost?.();
|
|
46406
|
+
}, Math.max(0, deadline.wallDeadline - Date.now()));
|
|
46407
|
+
clearTimeout(renewal);
|
|
46408
|
+
renewal = setTimeout(() => {
|
|
46409
|
+
if (!owned)
|
|
46410
|
+
return;
|
|
46411
|
+
const renewing = generation;
|
|
46412
|
+
grant({ type: "renew", holder: endpoint.holder, revision: owned.revision, sequence: owned.sequence + 1 }).catch((error) => {
|
|
46413
|
+
if (closed || renewing !== generation)
|
|
46414
|
+
return;
|
|
46415
|
+
stop();
|
|
46416
|
+
onLost?.();
|
|
46417
|
+
onError?.(error);
|
|
46418
|
+
});
|
|
46419
|
+
}, 5000);
|
|
46420
|
+
return value;
|
|
46421
|
+
} finally {
|
|
46422
|
+
attempts.delete(attempt);
|
|
46423
|
+
attempt.cancel();
|
|
46424
|
+
}
|
|
46425
|
+
}
|
|
46426
|
+
return Object.freeze({
|
|
46427
|
+
async acquire(descriptor) {
|
|
46428
|
+
if (closed)
|
|
46429
|
+
throw new Error("call lease closed");
|
|
46430
|
+
if (owned || acquiring)
|
|
46431
|
+
throw new Error("already in a call");
|
|
46432
|
+
acquiring = true;
|
|
46433
|
+
const started = generation;
|
|
46434
|
+
try {
|
|
46435
|
+
await request({ type: "read" });
|
|
46436
|
+
const payload = await capability.seal(descriptor);
|
|
46437
|
+
if (closed || started !== generation)
|
|
46438
|
+
throw new Error("call transfer cancelled");
|
|
46439
|
+
const claim = { type: "claim", holder: endpoint.holder, expectedRevision: record.revision, payload, takeover: true };
|
|
46440
|
+
if (!record.active && !record.pending)
|
|
46441
|
+
return await grant(claim);
|
|
46442
|
+
const pending = createCallLeaseAttempt(claim, clock);
|
|
46443
|
+
attempts.add(pending);
|
|
46444
|
+
try {
|
|
46445
|
+
await request(pending.command);
|
|
46446
|
+
const until = Date.now() + 20000;
|
|
46447
|
+
while (current() && started === generation && record.pending?.holder === endpoint.holder && record.active && Date.now() < until) {
|
|
46448
|
+
await waitForChange(Math.max(1, Math.min(until - Date.now(), record.active.expiresAt - Date.now() + 50)));
|
|
46449
|
+
if (current() && started === generation && record.active?.expiresAt <= Date.now())
|
|
46450
|
+
await request({ type: "read" });
|
|
46451
|
+
}
|
|
46452
|
+
if (!current() || started !== generation || record.pending?.holder !== endpoint.holder)
|
|
46453
|
+
throw new Error("call transfer cancelled");
|
|
46454
|
+
return await grant({ type: "activate", holder: endpoint.holder, revision: record.pending.revision });
|
|
46455
|
+
} finally {
|
|
46456
|
+
attempts.delete(pending);
|
|
46457
|
+
pending.cancel();
|
|
46458
|
+
}
|
|
46459
|
+
} finally {
|
|
46460
|
+
acquiring = false;
|
|
46461
|
+
}
|
|
46462
|
+
},
|
|
46463
|
+
provider(action, payload) {
|
|
46464
|
+
if (!owned || closed)
|
|
46465
|
+
return Promise.reject(new Error("call ownership required"));
|
|
46466
|
+
return channel.provider({ holder: endpoint.holder, revision: owned.revision, action, payload }).then((result) => result.record);
|
|
46467
|
+
},
|
|
46468
|
+
async release() {
|
|
46469
|
+
const previous = stop() || retiring;
|
|
46470
|
+
retiring = null;
|
|
46471
|
+
if (previous && !closed)
|
|
46472
|
+
await request({ type: "release", holder: endpoint.holder, revision: previous.revision });
|
|
46473
|
+
else if (record?.pending?.holder === endpoint.holder && !closed)
|
|
46474
|
+
await request({ type: "cancel", holder: endpoint.holder, revision: record.pending.revision });
|
|
46475
|
+
},
|
|
46476
|
+
retire() {
|
|
46477
|
+
retiring = stop() || retiring;
|
|
46478
|
+
},
|
|
46479
|
+
close() {
|
|
46480
|
+
closed = true;
|
|
46481
|
+
stop();
|
|
46482
|
+
channel.close();
|
|
46483
|
+
capability.close();
|
|
46484
|
+
}
|
|
46485
|
+
});
|
|
46486
|
+
}
|
|
46487
|
+
|
|
46488
|
+
// ../../core/calls/room.js
|
|
46489
|
+
function openCallRoom({
|
|
46490
|
+
cloud,
|
|
46491
|
+
capability,
|
|
46492
|
+
protocol,
|
|
46493
|
+
endpoint,
|
|
46494
|
+
mls,
|
|
46495
|
+
member,
|
|
46496
|
+
admission,
|
|
46497
|
+
callId,
|
|
46498
|
+
onKeys,
|
|
46499
|
+
onParticipants,
|
|
46500
|
+
onSignal,
|
|
46501
|
+
onRemoved,
|
|
46502
|
+
onError
|
|
46503
|
+
}) {
|
|
46504
|
+
let closed = false;
|
|
46505
|
+
let state = null;
|
|
46506
|
+
let head = null;
|
|
46507
|
+
let verifiedHead = null;
|
|
46508
|
+
let admittedIdentities = new Map;
|
|
46509
|
+
let pulseTimer = null;
|
|
46510
|
+
let changing = false;
|
|
46511
|
+
let reconcileAgain = false;
|
|
46512
|
+
let joined = false;
|
|
46513
|
+
let departing = false;
|
|
46514
|
+
let pendingCandidate = null;
|
|
46515
|
+
let ownMedia = null;
|
|
46516
|
+
let audio = { muted: false, deafened: false };
|
|
46517
|
+
let startTask = null;
|
|
46518
|
+
let startPromise = null;
|
|
46519
|
+
let admissionTimer = null;
|
|
46520
|
+
let admissionResult = null;
|
|
46521
|
+
let admissionFailure = null;
|
|
46522
|
+
let pendingInitial = null;
|
|
46523
|
+
let submittedJoin = false;
|
|
46524
|
+
let predecessorHolder = null;
|
|
46525
|
+
let eventTail = Promise.resolve();
|
|
46526
|
+
let retryWait = null;
|
|
46527
|
+
const pendingJoins = new Map;
|
|
46528
|
+
const leaving = new Set;
|
|
46529
|
+
const seen = new Map;
|
|
46530
|
+
const published = new Map;
|
|
46531
|
+
const localHolder = endpoint.holder;
|
|
46532
|
+
function disposeState(value) {
|
|
46533
|
+
cleanBytes(value?.snapshot, value?.baseKey);
|
|
46534
|
+
}
|
|
46535
|
+
const alive = (holder) => !leaving.has(holder) && Date.now() - (seen.get(holder) || 0) < 30000;
|
|
46536
|
+
const replacementFor = (item) => [...pendingJoins.values()].find((packet) => packet.actor === item.actor && packet.holder !== item.holder && packet.data.replaces === item.holder && alive(packet.holder));
|
|
46537
|
+
const leader = () => {
|
|
46538
|
+
const candidates = head?.participants.filter((item) => alive(item.holder)) || [];
|
|
46539
|
+
return (candidates.filter((item) => !replacementFor(item)).length ? candidates.filter((item) => !replacementFor(item)) : candidates).map((item) => item.holder).sort()[0];
|
|
46540
|
+
};
|
|
46541
|
+
function admissionError(code, message) {
|
|
46542
|
+
return Object.assign(new Error(message), { code });
|
|
46543
|
+
}
|
|
46544
|
+
const cancelled2 = () => admissionError("calls/cancelled", "call cancelled");
|
|
46545
|
+
const empty = () => admissionError("calls/room-empty", "this call is empty");
|
|
46546
|
+
function settleAdmission(error) {
|
|
46547
|
+
if (error)
|
|
46548
|
+
admissionFailure ||= error;
|
|
46549
|
+
if (!admissionResult)
|
|
46550
|
+
return;
|
|
46551
|
+
const result = admissionResult;
|
|
46552
|
+
admissionResult = null;
|
|
46553
|
+
clearTimeout(admissionTimer);
|
|
46554
|
+
admissionTimer = null;
|
|
46555
|
+
if (error)
|
|
46556
|
+
result.reject(error);
|
|
46557
|
+
else
|
|
46558
|
+
result.resolve();
|
|
46559
|
+
}
|
|
46560
|
+
function checkAdmission() {
|
|
46561
|
+
if (closed || departing || !head)
|
|
46562
|
+
return;
|
|
46563
|
+
if (!joined && head.participants.every((item) => leaving.has(item.holder)))
|
|
46564
|
+
settleAdmission(empty());
|
|
46565
|
+
else if (joined && head.participants.some((item) => item.holder === localHolder && item.actor === admission.actor))
|
|
46566
|
+
settleAdmission();
|
|
46567
|
+
else if (!joined && head.participants.length >= CALL_MAX_PARTICIPANTS && !head.participants.some((item) => item.actor === admission.actor)) {
|
|
46568
|
+
settleAdmission(admissionError("calls/full", "this call is full"));
|
|
46569
|
+
} else if (!joined && submittedJoin && head.participants.some((item) => item.actor === admission.actor && item.holder !== localHolder && item.holder !== predecessorHolder)) {
|
|
46570
|
+
settleAdmission(admissionError("calls/taken-over", "call moved to another device"));
|
|
46571
|
+
}
|
|
46572
|
+
}
|
|
46573
|
+
async function adopt(next, admissions) {
|
|
46574
|
+
verifyCallMembers(next, admissions, protocol);
|
|
46575
|
+
if (closed) {
|
|
46576
|
+
disposeState(next);
|
|
46577
|
+
return;
|
|
46578
|
+
}
|
|
46579
|
+
const previous = state;
|
|
46580
|
+
state = next;
|
|
46581
|
+
disposeState(previous);
|
|
46582
|
+
const self = next.members.find((item) => toHex(item.leafId) === localHolder);
|
|
46583
|
+
if (!self) {
|
|
46584
|
+
onRemoved?.();
|
|
46585
|
+
return;
|
|
46586
|
+
}
|
|
46587
|
+
if (!admissions.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey))
|
|
46588
|
+
throw new Error("call endpoint identity mismatch");
|
|
46589
|
+
joined = true;
|
|
46590
|
+
await onKeys({ baseKey: next.baseKey, epoch: next.epoch, members: next.members, leafIndex: self.leafIndex });
|
|
46591
|
+
}
|
|
46592
|
+
function participants() {
|
|
46593
|
+
return (head?.participants || []).filter((item) => !leaving.has(item.holder)).map((item) => ({
|
|
46594
|
+
...admittedIdentities.get(item.holder),
|
|
46595
|
+
holder: item.holder,
|
|
46596
|
+
muted: published.get(item.holder)?.muted === true || published.get(item.holder)?.deafened === true,
|
|
46597
|
+
deafened: published.get(item.holder)?.deafened === true,
|
|
46598
|
+
media: published.get(item.holder)?.media || null
|
|
46599
|
+
}));
|
|
46600
|
+
}
|
|
46601
|
+
function prune() {
|
|
46602
|
+
const admitted = new Set(head?.participants.map((item) => item.holder));
|
|
46603
|
+
for (const [holder, packet] of pendingJoins) {
|
|
46604
|
+
const predecessor = head?.participants.find((item) => item.actor === packet.actor);
|
|
46605
|
+
if (admitted.has(holder) || Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS || predecessor && packet.data.replaces !== predecessor.holder || !predecessor && packet.data.replaces)
|
|
46606
|
+
pendingJoins.delete(holder);
|
|
46607
|
+
}
|
|
46608
|
+
for (const holder of seen.keys())
|
|
46609
|
+
if (!admitted.has(holder) && !pendingJoins.has(holder))
|
|
46610
|
+
seen.delete(holder);
|
|
46611
|
+
for (const holder of published.keys())
|
|
46612
|
+
if (!admitted.has(holder))
|
|
46613
|
+
published.delete(holder);
|
|
46614
|
+
for (const holder of leaving)
|
|
46615
|
+
if (!admitted.has(holder))
|
|
46616
|
+
leaving.delete(holder);
|
|
46617
|
+
}
|
|
46618
|
+
async function consume(record) {
|
|
46619
|
+
if (closed)
|
|
46620
|
+
return;
|
|
46621
|
+
prune();
|
|
46622
|
+
const nextHead = record.head?.data;
|
|
46623
|
+
const signals = [];
|
|
46624
|
+
let membershipChanged = false;
|
|
46625
|
+
if (nextHead && (!Array.isArray(nextHead.participants) || nextHead.participants.length > CALL_MAX_PARTICIPANTS))
|
|
46626
|
+
throw new Error("invalid call room");
|
|
46627
|
+
for (const packet of record.events) {
|
|
46628
|
+
const roster = head?.participants || nextHead?.participants || [];
|
|
46629
|
+
const bound = roster.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint);
|
|
46630
|
+
const pending = pendingJoins.get(packet.holder);
|
|
46631
|
+
const pendingBound = pending?.actor === packet.actor && pending?.endpoint === packet.endpoint;
|
|
46632
|
+
if (!["join", "replace"].includes(packet.kind) && !bound && !(packet.kind === "leave" && pendingBound))
|
|
46633
|
+
continue;
|
|
46634
|
+
if (packet.kind === "join" && roster.some((item) => item.holder === packet.holder) && !bound)
|
|
46635
|
+
continue;
|
|
46636
|
+
if (packet.kind === "join") {
|
|
46637
|
+
if (Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS)
|
|
46638
|
+
continue;
|
|
46639
|
+
const predecessor = roster.find((item) => item.actor === packet.actor && item.holder !== packet.holder);
|
|
46640
|
+
if (predecessor && packet.data.replaces !== predecessor.holder)
|
|
46641
|
+
continue;
|
|
46642
|
+
if (!predecessor && packet.data.replaces && !bound)
|
|
46643
|
+
continue;
|
|
46644
|
+
const previous = [...pendingJoins.values()].find((item) => item.actor === packet.actor && item.holder !== packet.holder);
|
|
46645
|
+
if (previous) {
|
|
46646
|
+
pendingJoins.delete(previous.holder);
|
|
46647
|
+
seen.delete(previous.holder);
|
|
46648
|
+
}
|
|
46649
|
+
if (!pendingJoins.has(packet.holder) && pendingJoins.size >= CALL_MAX_PARTICIPANTS)
|
|
46650
|
+
continue;
|
|
46651
|
+
pendingJoins.set(packet.holder, packet);
|
|
46652
|
+
} else if (packet.kind === "leave") {
|
|
46653
|
+
if (pendingBound)
|
|
46654
|
+
pendingJoins.delete(packet.holder);
|
|
46655
|
+
if (bound)
|
|
46656
|
+
leaving.add(packet.holder);
|
|
46657
|
+
} else if (packet.kind === "pulse") {
|
|
46658
|
+
published.set(packet.holder, packet.data);
|
|
46659
|
+
} else if (packet.kind === "signal") {
|
|
46660
|
+
if (packet.data.to === localHolder && head?.participants.some((item) => item.holder === packet.holder && item.actor === packet.actor)) {
|
|
46661
|
+
signals.push(packet);
|
|
46662
|
+
}
|
|
46663
|
+
} else if (packet.kind === "replace") {
|
|
46664
|
+
const replacement = verifyCallReplacement(packet, head, protocol);
|
|
46665
|
+
membershipChanged = true;
|
|
46666
|
+
if (state && !replacement.participants.some((item) => item.holder === localHolder)) {
|
|
46667
|
+
settleAdmission(cancelled2());
|
|
46668
|
+
onRemoved?.();
|
|
46669
|
+
return;
|
|
46670
|
+
}
|
|
46671
|
+
head = replacement;
|
|
46672
|
+
} else if (packet.kind === "change") {
|
|
46673
|
+
membershipChanged = true;
|
|
46674
|
+
const change = packet.data;
|
|
46675
|
+
if (!Array.isArray(change.participants) || !Number.isSafeInteger(change.epoch))
|
|
46676
|
+
throw new Error("invalid call epoch");
|
|
46677
|
+
if (state && change.epoch === state.epoch + 1) {
|
|
46678
|
+
if (!bound)
|
|
46679
|
+
throw new Error("unadmitted call commit");
|
|
46680
|
+
const next = pendingCandidate?.commit === change.commit ? pendingCandidate.state : await mls.processCommit(state.snapshot, decodeCallBytes(change.commit));
|
|
46681
|
+
pendingCandidate = null;
|
|
46682
|
+
if (next.removed) {
|
|
46683
|
+
disposeState(next);
|
|
46684
|
+
onRemoved?.();
|
|
46685
|
+
return;
|
|
46686
|
+
}
|
|
46687
|
+
await adopt(next, change.participants);
|
|
46688
|
+
} else if (!state && change.participants.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey)) {
|
|
46689
|
+
const next = await mls.join(member.snapshot, decodeCallBytes(change.welcome), decodeCallBytes(change.tree), fromHexBytes(callId));
|
|
46690
|
+
await adopt(next, change.participants);
|
|
46691
|
+
}
|
|
46692
|
+
head = { epoch: change.epoch, participants: change.participants };
|
|
46693
|
+
for (const item of change.participants) {
|
|
46694
|
+
pendingJoins.delete(item.holder);
|
|
46695
|
+
if (!seen.has(item.holder))
|
|
46696
|
+
seen.set(item.holder, packet.at);
|
|
46697
|
+
}
|
|
46698
|
+
}
|
|
46699
|
+
seen.set(packet.holder, Math.max(seen.get(packet.holder) || 0, packet.at));
|
|
46700
|
+
}
|
|
46701
|
+
if (!state && pendingInitial && nextHead?.epoch === 0 && nextHead.participants.length === 1 && nextHead.participants[0].signature === pendingInitial.admission.signature) {
|
|
46702
|
+
const initial = pendingInitial.state;
|
|
46703
|
+
pendingInitial = null;
|
|
46704
|
+
await adopt(initial, nextHead.participants);
|
|
46705
|
+
}
|
|
46706
|
+
if (nextHead && (verifiedHead !== record.head || membershipChanged)) {
|
|
46707
|
+
admittedIdentities = state ? verifyCallMembers(state, nextHead.participants, protocol) : verifyCallAdmissions(nextHead.participants, protocol);
|
|
46708
|
+
for (const item of nextHead.participants) {
|
|
46709
|
+
if (!seen.has(item.holder))
|
|
46710
|
+
seen.set(item.holder, record.head.at);
|
|
46711
|
+
}
|
|
46712
|
+
if (state && nextHead.epoch !== state.epoch)
|
|
46713
|
+
throw new Error("call epoch history unavailable");
|
|
46714
|
+
head = nextHead;
|
|
46715
|
+
verifiedHead = record.head;
|
|
46716
|
+
} else if (!nextHead) {
|
|
46717
|
+
head = null;
|
|
46718
|
+
verifiedHead = null;
|
|
46719
|
+
admittedIdentities.clear();
|
|
46720
|
+
if (joined) {
|
|
46721
|
+
onRemoved?.();
|
|
46722
|
+
return;
|
|
46723
|
+
}
|
|
46724
|
+
if (submittedJoin)
|
|
46725
|
+
settleAdmission(empty());
|
|
46726
|
+
}
|
|
46727
|
+
prune();
|
|
46728
|
+
if (closed)
|
|
46729
|
+
return;
|
|
46730
|
+
onParticipants?.(participants());
|
|
46731
|
+
checkAdmission();
|
|
46732
|
+
for (const packet of signals) {
|
|
46733
|
+
try {
|
|
46734
|
+
Promise.resolve(onSignal?.(packet.data.signal, packet.holder)).catch(fail);
|
|
46735
|
+
} catch (error) {
|
|
46736
|
+
fail(error);
|
|
46737
|
+
}
|
|
46738
|
+
}
|
|
46739
|
+
reconcile().catch(fail);
|
|
46740
|
+
}
|
|
46741
|
+
function fail(error) {
|
|
46742
|
+
if (!closed) {
|
|
46743
|
+
if (admissionResult)
|
|
46744
|
+
settleAdmission(error);
|
|
46745
|
+
onError?.(error);
|
|
46746
|
+
}
|
|
46747
|
+
}
|
|
46748
|
+
const mailbox = openCallMailbox({ cloud, capability, protocol, endpoint, onChange: consume, onError: fail });
|
|
46749
|
+
async function reconcile() {
|
|
46750
|
+
reconcileAgain = true;
|
|
46751
|
+
if (changing)
|
|
46752
|
+
return;
|
|
46753
|
+
changing = true;
|
|
46754
|
+
try {
|
|
46755
|
+
do {
|
|
46756
|
+
reconcileAgain = false;
|
|
46757
|
+
await reconcilePass();
|
|
46758
|
+
} while (reconcileAgain && !closed);
|
|
46759
|
+
} finally {
|
|
46760
|
+
changing = false;
|
|
46761
|
+
}
|
|
46762
|
+
}
|
|
46763
|
+
async function reconcilePass() {
|
|
46764
|
+
if (closed || departing || !state || leader() !== localHolder)
|
|
46765
|
+
return;
|
|
46766
|
+
const current = mailbox.getSnapshot();
|
|
46767
|
+
if (!current || !head)
|
|
46768
|
+
return;
|
|
46769
|
+
const removals = head.participants.filter((item) => item.holder !== localHolder && (!alive(item.holder) || replacementFor(item)));
|
|
46770
|
+
const retained = head.participants.filter((item) => !removals.includes(item));
|
|
46771
|
+
const admittedActors = new Set(retained.map((item) => item.actor));
|
|
46772
|
+
const additions = [...pendingJoins.values()].filter((item) => {
|
|
46773
|
+
if (head.participants.some((existing) => existing.holder === item.holder) || admittedActors.has(item.actor) || !alive(item.holder))
|
|
46774
|
+
return false;
|
|
46775
|
+
admittedActors.add(item.actor);
|
|
46776
|
+
return true;
|
|
46777
|
+
}).slice(0, CALL_MAX_PARTICIPANTS - retained.length);
|
|
46778
|
+
if (!removals.length && !additions.length)
|
|
46779
|
+
return;
|
|
46780
|
+
let candidate;
|
|
46781
|
+
try {
|
|
46782
|
+
for (const item of additions)
|
|
46783
|
+
protocol.verify(item);
|
|
46784
|
+
candidate = await mls.stageChange(state.snapshot, {
|
|
46785
|
+
adds: additions.map((item) => decodeCallBytes(item.data.keyPackage)),
|
|
46786
|
+
removes: removals.map((item) => fromHexBytes(item.holder))
|
|
46787
|
+
});
|
|
46788
|
+
if (closed || departing) {
|
|
46789
|
+
disposeState(candidate);
|
|
46790
|
+
return;
|
|
46791
|
+
}
|
|
46792
|
+
const admissions = [...retained, ...additions];
|
|
46793
|
+
verifyCallMembers(candidate, admissions, protocol);
|
|
46794
|
+
const change = {
|
|
46795
|
+
epoch: candidate.epoch,
|
|
46796
|
+
participants: admissions,
|
|
46797
|
+
commit: encodeCallBytes(candidate.commit),
|
|
46798
|
+
welcome: encodeCallBytes(candidate.welcome),
|
|
46799
|
+
tree: encodeCallBytes(candidate.tree)
|
|
46800
|
+
};
|
|
46801
|
+
pendingCandidate = { commit: change.commit, state: candidate };
|
|
46802
|
+
await mailbox.commit({ epoch: candidate.epoch, participants: admissions }, [protocol.sign("change", change)], { expectedRevision: current.revision });
|
|
46803
|
+
candidate = null;
|
|
46804
|
+
} catch (error) {
|
|
46805
|
+
if (pendingCandidate?.state === candidate)
|
|
46806
|
+
pendingCandidate = null;
|
|
46807
|
+
disposeState(candidate);
|
|
46808
|
+
if (String(error.code).endsWith("conflict")) {
|
|
46809
|
+
await mailbox.read();
|
|
46810
|
+
return;
|
|
46811
|
+
}
|
|
46812
|
+
throw error;
|
|
46813
|
+
}
|
|
46814
|
+
}
|
|
46815
|
+
function event(kind, data) {
|
|
46816
|
+
const result = eventTail.then(() => appendEvent(kind, data));
|
|
46817
|
+
eventTail = result.catch(() => {});
|
|
46818
|
+
return result;
|
|
46819
|
+
}
|
|
46820
|
+
async function appendEvent(kind, data) {
|
|
46821
|
+
const deadline = performance.now() + CALL_ADMISSION_TIMEOUT_MS;
|
|
46822
|
+
for (let attempt = 0;!closed && performance.now() < deadline; attempt += 1) {
|
|
46823
|
+
if (kind === "join" && departing)
|
|
46824
|
+
throw cancelled2();
|
|
46825
|
+
const current = mailbox.getSnapshot();
|
|
46826
|
+
if (!current?.head)
|
|
46827
|
+
throw kind === "join" ? empty() : new Error("call ended");
|
|
46828
|
+
if (kind === "join") {
|
|
46829
|
+
checkAdmission();
|
|
46830
|
+
if (admissionFailure)
|
|
46831
|
+
throw admissionFailure;
|
|
46832
|
+
}
|
|
46833
|
+
try {
|
|
46834
|
+
if (kind === "join")
|
|
46835
|
+
submittedJoin = true;
|
|
46836
|
+
return await mailbox.append([protocol.sign(kind, data)], { expectedRevision: current.revision });
|
|
46837
|
+
} catch (error) {
|
|
46838
|
+
if (closed)
|
|
46839
|
+
throw cancelled2();
|
|
46840
|
+
if (!String(error.code).endsWith("conflict"))
|
|
46841
|
+
throw error;
|
|
46842
|
+
const delay = Math.min(250, 25 * 2 ** Math.min(attempt, 4)) * (0.5 + Math.random() / 2);
|
|
46843
|
+
await new Promise((resolve) => {
|
|
46844
|
+
const timer = setTimeout(() => {
|
|
46845
|
+
retryWait = null;
|
|
46846
|
+
resolve();
|
|
46847
|
+
}, delay);
|
|
46848
|
+
retryWait = () => {
|
|
46849
|
+
clearTimeout(timer);
|
|
46850
|
+
retryWait = null;
|
|
46851
|
+
resolve();
|
|
46852
|
+
};
|
|
46853
|
+
});
|
|
46854
|
+
if (closed)
|
|
46855
|
+
throw cancelled2();
|
|
46856
|
+
await mailbox.read();
|
|
46857
|
+
}
|
|
46858
|
+
}
|
|
46859
|
+
if (closed)
|
|
46860
|
+
throw cancelled2();
|
|
46861
|
+
throw admissionError("calls/signaling-conflict", "call changed, try again");
|
|
46862
|
+
}
|
|
46863
|
+
async function pulse() {
|
|
46864
|
+
if (closed || departing || !joined)
|
|
46865
|
+
return;
|
|
46866
|
+
try {
|
|
46867
|
+
await event("pulse", { ...audio, media: ownMedia });
|
|
46868
|
+
} catch (error) {
|
|
46869
|
+
fail(error);
|
|
46870
|
+
}
|
|
46871
|
+
if (!closed && !departing)
|
|
46872
|
+
pulseTimer = setTimeout(pulse, 1e4);
|
|
46873
|
+
}
|
|
46874
|
+
async function beginStart(create) {
|
|
46875
|
+
const record = await mailbox.read();
|
|
46876
|
+
if (closed || departing)
|
|
46877
|
+
throw cancelled2();
|
|
46878
|
+
if (!create && !record?.head)
|
|
46879
|
+
throw empty();
|
|
46880
|
+
checkAdmission();
|
|
46881
|
+
if (admissionFailure)
|
|
46882
|
+
throw admissionFailure;
|
|
46883
|
+
const solePredecessor = record?.head?.data.participants.length === 1 && record.head.data.participants[0].actor === admission.actor && record.head.data.participants[0].holder !== localHolder;
|
|
46884
|
+
if (create || solePredecessor) {
|
|
46885
|
+
if (create && record?.head)
|
|
46886
|
+
throw new Error("call already exists");
|
|
46887
|
+
let initialMember = member;
|
|
46888
|
+
let initialAdmission = admission;
|
|
46889
|
+
if (solePredecessor)
|
|
46890
|
+
initialMember = await mls.createMember(fromHexBytes(localHolder));
|
|
46891
|
+
let initial;
|
|
46892
|
+
try {
|
|
46893
|
+
if (closed || departing)
|
|
46894
|
+
throw cancelled2();
|
|
46895
|
+
if (solePredecessor)
|
|
46896
|
+
initialAdmission = protocol.sign("join", {
|
|
46897
|
+
signaturePK: toHex(initialMember.signaturePK),
|
|
46898
|
+
keyPackage: "",
|
|
46899
|
+
replaces: record.head.data.participants[0].holder
|
|
46900
|
+
});
|
|
46901
|
+
initial = await mls.createGroup(initialMember.snapshot, fromHexBytes(callId));
|
|
46902
|
+
} finally {
|
|
46903
|
+
if (solePredecessor)
|
|
46904
|
+
disposeState(initialMember);
|
|
46905
|
+
}
|
|
46906
|
+
if (closed || departing) {
|
|
46907
|
+
disposeState(initial);
|
|
46908
|
+
throw cancelled2();
|
|
46909
|
+
}
|
|
46910
|
+
if (admissionFailure) {
|
|
46911
|
+
disposeState(initial);
|
|
46912
|
+
throw admissionFailure;
|
|
46913
|
+
}
|
|
46914
|
+
pendingInitial = { state: initial, admission: initialAdmission };
|
|
46915
|
+
const initialHead = { epoch: initial.epoch, participants: [initialAdmission] };
|
|
46916
|
+
const packet = solePredecessor ? protocol.sign("replace", { previous: record.head, participant: initialAdmission }) : protocol.sign("pulse", { ...audio, media: null });
|
|
46917
|
+
submittedJoin = true;
|
|
46918
|
+
await mailbox.commit(initialHead, [packet], { expectedRevision: record.revision });
|
|
46919
|
+
} else {
|
|
46920
|
+
const predecessor = head.participants.find((item) => item.actor === admission.actor);
|
|
46921
|
+
predecessorHolder = predecessor?.holder || null;
|
|
46922
|
+
await event("join", { ...admission.data, replaces: predecessor?.holder || null });
|
|
46923
|
+
checkAdmission();
|
|
46924
|
+
}
|
|
46925
|
+
}
|
|
46926
|
+
return Object.freeze({
|
|
46927
|
+
start({ create = false } = {}) {
|
|
46928
|
+
if (startPromise)
|
|
46929
|
+
return startPromise;
|
|
46930
|
+
const admitted = new Promise((resolve, reject) => {
|
|
46931
|
+
admissionResult = { resolve, reject };
|
|
46932
|
+
});
|
|
46933
|
+
admissionTimer = setTimeout(() => settleAdmission(admissionError("calls/admission-timeout", "could not join the call")), CALL_ADMISSION_TIMEOUT_MS);
|
|
46934
|
+
startTask = beginStart(create);
|
|
46935
|
+
startPromise = Promise.race([startTask.then(() => admitted), admitted]).then(() => {
|
|
46936
|
+
if (closed || departing)
|
|
46937
|
+
throw cancelled2();
|
|
46938
|
+
pulseTimer = setTimeout(pulse, 1e4);
|
|
46939
|
+
}).catch((error) => {
|
|
46940
|
+
settleAdmission(error);
|
|
46941
|
+
throw error;
|
|
46942
|
+
});
|
|
46943
|
+
return startPromise;
|
|
46944
|
+
},
|
|
46945
|
+
signal(to, signal) {
|
|
46946
|
+
return event("signal", { to, signal });
|
|
46947
|
+
},
|
|
46948
|
+
async publishMedia(media) {
|
|
46949
|
+
ownMedia = media;
|
|
46950
|
+
await event("pulse", { ...audio, media });
|
|
46951
|
+
},
|
|
46952
|
+
async setAudio(value) {
|
|
46953
|
+
audio = { muted: value.muted === true || value.deafened === true, deafened: value.deafened === true };
|
|
46954
|
+
if (joined && !departing && !closed)
|
|
46955
|
+
await event("pulse", { ...audio, media: ownMedia });
|
|
46956
|
+
},
|
|
46957
|
+
async leave() {
|
|
46958
|
+
if (closed)
|
|
46959
|
+
return { ended: false };
|
|
46960
|
+
departing = true;
|
|
46961
|
+
settleAdmission(cancelled2());
|
|
46962
|
+
clearTimeout(pulseTimer);
|
|
46963
|
+
await startTask?.catch(() => {});
|
|
46964
|
+
if (closed || !joined && !submittedJoin || !mailbox.getSnapshot()?.head)
|
|
46965
|
+
return { ended: false };
|
|
46966
|
+
await event("leave", {});
|
|
46967
|
+
await mailbox.read();
|
|
46968
|
+
return { ended: !!head && head.participants.every((item) => !alive(item.holder)) };
|
|
46969
|
+
},
|
|
46970
|
+
close() {
|
|
46971
|
+
if (closed)
|
|
46972
|
+
return;
|
|
46973
|
+
closed = true;
|
|
46974
|
+
retryWait?.();
|
|
46975
|
+
settleAdmission(cancelled2());
|
|
46976
|
+
clearTimeout(pulseTimer);
|
|
46977
|
+
mailbox.close();
|
|
46978
|
+
disposeState(state);
|
|
46979
|
+
disposeState(member);
|
|
46980
|
+
disposeState(pendingCandidate?.state);
|
|
46981
|
+
disposeState(pendingInitial?.state);
|
|
46982
|
+
pendingJoins.clear();
|
|
46983
|
+
published.clear();
|
|
46984
|
+
seen.clear();
|
|
46985
|
+
}
|
|
46986
|
+
});
|
|
46987
|
+
}
|
|
46988
|
+
|
|
46989
|
+
// ../../core/calls/activity.js
|
|
46990
|
+
var positive = (value) => Number.isFinite(value) && value >= CALL_SPEAKING_THRESHOLD && value <= 1;
|
|
46991
|
+
function createCallActivity({ holder, onSpeaking, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
46992
|
+
let closed = false;
|
|
46993
|
+
let timer = null;
|
|
46994
|
+
let local = false;
|
|
46995
|
+
let direct = null;
|
|
46996
|
+
let routes = new Map;
|
|
46997
|
+
let sources = new Map;
|
|
46998
|
+
let published = [];
|
|
46999
|
+
const speaking = new Map;
|
|
47000
|
+
function publish() {
|
|
47001
|
+
const next = [...speaking.keys()].sort();
|
|
47002
|
+
if (published.length === next.length && published.every((value, index) => value === next[index]))
|
|
47003
|
+
return;
|
|
47004
|
+
published = next;
|
|
47005
|
+
onSpeaking?.(next);
|
|
47006
|
+
}
|
|
47007
|
+
function schedule() {
|
|
47008
|
+
clearTimer(timer);
|
|
47009
|
+
timer = null;
|
|
47010
|
+
if (!closed && speaking.size)
|
|
47011
|
+
timer = setTimer(expire, Math.max(0, Math.min(...speaking.values()) - now()));
|
|
47012
|
+
}
|
|
47013
|
+
function expire() {
|
|
47014
|
+
timer = null;
|
|
47015
|
+
if (closed)
|
|
47016
|
+
return;
|
|
47017
|
+
const time = now();
|
|
47018
|
+
for (const [id, deadline] of speaking)
|
|
47019
|
+
if (deadline <= time)
|
|
47020
|
+
speaking.delete(id);
|
|
47021
|
+
publish();
|
|
47022
|
+
schedule();
|
|
47023
|
+
}
|
|
47024
|
+
function update({ peers, received, directPeer, mode, muted, deafened }) {
|
|
47025
|
+
if (closed)
|
|
47026
|
+
return;
|
|
47027
|
+
const admitted = new Map(peers.map((peer) => [peer.holder, peer]));
|
|
47028
|
+
local = admitted.has(holder) && !muted && !deafened;
|
|
47029
|
+
direct = mode === "direct" && directPeer !== holder && admitted.has(directPeer) && !admitted.get(directPeer).muted ? directPeer : null;
|
|
47030
|
+
const nextSources = new Map(local ? [[holder, "local"]] : []);
|
|
47031
|
+
const nextRoutes2 = new Map;
|
|
47032
|
+
const ambiguous = new Set;
|
|
47033
|
+
if (direct)
|
|
47034
|
+
nextSources.set(direct, "direct");
|
|
47035
|
+
if (mode === "group") {
|
|
47036
|
+
for (const [id, mid] of received) {
|
|
47037
|
+
if (id === holder || !admitted.has(id) || admitted.get(id).muted || typeof mid !== "string" || !mid)
|
|
47038
|
+
continue;
|
|
47039
|
+
if (nextRoutes2.has(mid)) {
|
|
47040
|
+
nextSources.delete(nextRoutes2.get(mid));
|
|
47041
|
+
nextRoutes2.delete(mid);
|
|
47042
|
+
ambiguous.add(mid);
|
|
47043
|
+
}
|
|
47044
|
+
if (ambiguous.has(mid))
|
|
47045
|
+
continue;
|
|
47046
|
+
nextRoutes2.set(mid, id);
|
|
47047
|
+
nextSources.set(id, mid);
|
|
47048
|
+
}
|
|
47049
|
+
}
|
|
47050
|
+
const time = now();
|
|
47051
|
+
for (const [id, deadline] of speaking) {
|
|
47052
|
+
if (deadline <= time || !nextSources.has(id) || nextSources.get(id) !== sources.get(id))
|
|
47053
|
+
speaking.delete(id);
|
|
47054
|
+
}
|
|
47055
|
+
routes = nextRoutes2;
|
|
47056
|
+
sources = nextSources;
|
|
47057
|
+
publish();
|
|
47058
|
+
schedule();
|
|
47059
|
+
}
|
|
47060
|
+
function sample(value) {
|
|
47061
|
+
if (closed)
|
|
47062
|
+
return;
|
|
47063
|
+
const time = now();
|
|
47064
|
+
for (const [id, deadline] of speaking)
|
|
47065
|
+
if (deadline <= time)
|
|
47066
|
+
speaking.delete(id);
|
|
47067
|
+
if (local && positive(value.local))
|
|
47068
|
+
speaking.set(holder, time + CALL_SPEAKING_HOLD_MS);
|
|
47069
|
+
for (const item of value.receive) {
|
|
47070
|
+
if (typeof item.mid !== "string" || !item.mid || !positive(item.level))
|
|
47071
|
+
continue;
|
|
47072
|
+
const id = direct || routes.get(item.mid);
|
|
47073
|
+
if (id)
|
|
47074
|
+
speaking.set(id, time + CALL_SPEAKING_HOLD_MS);
|
|
47075
|
+
}
|
|
47076
|
+
publish();
|
|
47077
|
+
schedule();
|
|
47078
|
+
}
|
|
47079
|
+
function reset() {
|
|
47080
|
+
clearTimer(timer);
|
|
47081
|
+
timer = null;
|
|
47082
|
+
speaking.clear();
|
|
47083
|
+
publish();
|
|
47084
|
+
}
|
|
47085
|
+
function close() {
|
|
47086
|
+
if (closed)
|
|
47087
|
+
return;
|
|
47088
|
+
closed = true;
|
|
47089
|
+
reset();
|
|
47090
|
+
local = false;
|
|
47091
|
+
direct = null;
|
|
47092
|
+
routes.clear();
|
|
47093
|
+
sources.clear();
|
|
47094
|
+
}
|
|
47095
|
+
return Object.freeze({ update, sample, reset, close });
|
|
47096
|
+
}
|
|
47097
|
+
|
|
47098
|
+
// ../../core/calls/media.js
|
|
47099
|
+
var sdp = (value) => ({ type: value.type, sdp: value.sdp });
|
|
47100
|
+
var validMid = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,32}$/u.test(value);
|
|
47101
|
+
var MAX_PENDING_SIGNALS = 128;
|
|
47102
|
+
var CONNECTION_TIMEOUT_MS = 15000;
|
|
47103
|
+
var retiredConnection = new Error("call connection retired");
|
|
47104
|
+
function createCallMediaSession({ port, mode, holder, provider, signal, publish, onState, onError, onExpired, onSpeaking }) {
|
|
47105
|
+
let media = null;
|
|
47106
|
+
let closed = false;
|
|
47107
|
+
let deadline = null;
|
|
47108
|
+
let keys = null;
|
|
47109
|
+
let peers = [];
|
|
47110
|
+
let sessionId = null;
|
|
47111
|
+
let started = false;
|
|
47112
|
+
let directPeer = null;
|
|
47113
|
+
let offered = false;
|
|
47114
|
+
let muted = false;
|
|
47115
|
+
let deafened = false;
|
|
47116
|
+
let microphoneVolume = 1;
|
|
47117
|
+
let closing = null;
|
|
47118
|
+
let connectionState = "new";
|
|
47119
|
+
let connectionWait = null;
|
|
47120
|
+
let tail = Promise.resolve();
|
|
47121
|
+
let connectionGeneration = 0;
|
|
47122
|
+
let openingConnection = null;
|
|
47123
|
+
let retiringConnection = Promise.resolve();
|
|
47124
|
+
let iceServers = null;
|
|
47125
|
+
const received = new Map;
|
|
47126
|
+
const peerAudio = new Map;
|
|
47127
|
+
const pendingPeerAudio = new Map;
|
|
47128
|
+
const activity = createCallActivity({ holder, onSpeaking });
|
|
47129
|
+
const updateActivity = () => activity.update({ peers, received, directPeer, mode, muted, deafened });
|
|
47130
|
+
const pendingSignals = [];
|
|
47131
|
+
const incomingSignals = [];
|
|
47132
|
+
function settlePeerAudio(id, error) {
|
|
47133
|
+
const pending = pendingPeerAudio.get(id);
|
|
47134
|
+
if (!pending)
|
|
47135
|
+
return;
|
|
47136
|
+
pendingPeerAudio.delete(id);
|
|
47137
|
+
if (error)
|
|
47138
|
+
pending.reject(error);
|
|
47139
|
+
else
|
|
47140
|
+
pending.resolve();
|
|
47141
|
+
}
|
|
47142
|
+
function current(generation = connectionGeneration) {
|
|
47143
|
+
if (closed)
|
|
47144
|
+
throw new Error("call media closed");
|
|
47145
|
+
if (generation !== connectionGeneration)
|
|
47146
|
+
throw retiredConnection;
|
|
47147
|
+
}
|
|
47148
|
+
async function disposeConnection(previous) {
|
|
47149
|
+
const results = await Promise.allSettled([() => previous?.revoke(), () => previous?.close()].map((operation) => {
|
|
47150
|
+
try {
|
|
47151
|
+
return Promise.resolve(operation());
|
|
47152
|
+
} catch (error) {
|
|
47153
|
+
return Promise.reject(error);
|
|
47154
|
+
}
|
|
47155
|
+
}));
|
|
47156
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
47157
|
+
if (failed)
|
|
47158
|
+
throw failed.reason;
|
|
47159
|
+
}
|
|
47160
|
+
function retireDirectConnection(nextPeer) {
|
|
47161
|
+
connectionGeneration++;
|
|
47162
|
+
const previous = media;
|
|
47163
|
+
media = null;
|
|
47164
|
+
received.clear();
|
|
47165
|
+
directPeer = nextPeer;
|
|
47166
|
+
activity.reset();
|
|
47167
|
+
updateActivity();
|
|
47168
|
+
offered = false;
|
|
47169
|
+
connectionState = "new";
|
|
47170
|
+
pendingSignals.length = 0;
|
|
47171
|
+
const opening = openingConnection;
|
|
47172
|
+
retiringConnection = Promise.all([
|
|
47173
|
+
retiringConnection,
|
|
47174
|
+
disposeConnection(previous),
|
|
47175
|
+
opening?.then(disposeConnection, () => {})
|
|
47176
|
+
]).then(() => {});
|
|
47177
|
+
retiringConnection.catch(() => {});
|
|
47178
|
+
onState?.({ state: "connecting" });
|
|
47179
|
+
}
|
|
47180
|
+
function settleConnection(error) {
|
|
47181
|
+
if (!connectionWait)
|
|
47182
|
+
return;
|
|
47183
|
+
const wait2 = connectionWait;
|
|
47184
|
+
connectionWait = null;
|
|
47185
|
+
clearTimeout(wait2.timer);
|
|
47186
|
+
if (error)
|
|
47187
|
+
wait2.reject(error);
|
|
47188
|
+
else
|
|
47189
|
+
wait2.resolve();
|
|
47190
|
+
}
|
|
47191
|
+
function connected() {
|
|
47192
|
+
current();
|
|
47193
|
+
if (connectionState === "connected")
|
|
47194
|
+
return Promise.resolve();
|
|
47195
|
+
if (["failed", "closed"].includes(connectionState))
|
|
47196
|
+
return Promise.reject(new Error("call connection failed"));
|
|
47197
|
+
if (!connectionWait) {
|
|
47198
|
+
const wait2 = {};
|
|
47199
|
+
wait2.promise = new Promise((resolve, reject) => {
|
|
47200
|
+
wait2.resolve = resolve;
|
|
47201
|
+
wait2.reject = reject;
|
|
47202
|
+
});
|
|
47203
|
+
wait2.timer = setTimeout(() => settleConnection(new Error("call connection timed out")), CONNECTION_TIMEOUT_MS);
|
|
47204
|
+
connectionWait = wait2;
|
|
47205
|
+
}
|
|
47206
|
+
return connectionWait.promise;
|
|
47207
|
+
}
|
|
47208
|
+
function serialize(work) {
|
|
47209
|
+
const result = tail.then(() => {
|
|
47210
|
+
current();
|
|
47211
|
+
return work();
|
|
47212
|
+
});
|
|
47213
|
+
tail = result.catch((error) => {
|
|
47214
|
+
if (!closed)
|
|
47215
|
+
onError?.(error);
|
|
47216
|
+
});
|
|
47217
|
+
return result;
|
|
47218
|
+
}
|
|
47219
|
+
function close() {
|
|
47220
|
+
if (closed)
|
|
47221
|
+
return closing;
|
|
47222
|
+
closed = true;
|
|
47223
|
+
activity.close();
|
|
47224
|
+
connectionGeneration++;
|
|
47225
|
+
settleConnection(new Error("call media closed"));
|
|
47226
|
+
const previous = media;
|
|
47227
|
+
media = keys = deadline = iceServers = null;
|
|
47228
|
+
received.clear();
|
|
47229
|
+
peerAudio.clear();
|
|
47230
|
+
for (const id of pendingPeerAudio.keys())
|
|
47231
|
+
settlePeerAudio(id, new Error("call media closed"));
|
|
47232
|
+
pendingSignals.length = incomingSignals.length = 0;
|
|
47233
|
+
const operations = [disposeConnection(previous), retiringConnection];
|
|
47234
|
+
closing = Promise.allSettled(operations).then((results) => {
|
|
47235
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
47236
|
+
if (failed)
|
|
47237
|
+
throw failed.reason;
|
|
47238
|
+
});
|
|
47239
|
+
return closing;
|
|
47240
|
+
}
|
|
47241
|
+
async function openConnection() {
|
|
47242
|
+
const generation = connectionGeneration;
|
|
47243
|
+
const live = () => !closed && generation === connectionGeneration;
|
|
47244
|
+
await retiringConnection;
|
|
47245
|
+
current(generation);
|
|
47246
|
+
if (!iceServers) {
|
|
47247
|
+
const configuration = await provider("turn", {});
|
|
47248
|
+
current(generation);
|
|
47249
|
+
iceServers = configuration.iceServers;
|
|
47250
|
+
}
|
|
47251
|
+
const opening = port.open({
|
|
47252
|
+
id: holder,
|
|
47253
|
+
mode,
|
|
47254
|
+
iceServers,
|
|
47255
|
+
onState: (value) => {
|
|
47256
|
+
if (!live())
|
|
47257
|
+
return;
|
|
47258
|
+
connectionState = value.state;
|
|
47259
|
+
if (connectionState === "connected")
|
|
47260
|
+
settleConnection();
|
|
47261
|
+
else if (["failed", "closed"].includes(connectionState))
|
|
47262
|
+
settleConnection(new Error("call connection failed"));
|
|
47263
|
+
onState?.(value);
|
|
47264
|
+
},
|
|
47265
|
+
onExpired: () => {
|
|
47266
|
+
if (live())
|
|
47267
|
+
onExpired?.();
|
|
47268
|
+
},
|
|
47269
|
+
onSignal(value) {
|
|
47270
|
+
if (mode !== "direct" || !live())
|
|
47271
|
+
return;
|
|
47272
|
+
if (directPeer)
|
|
47273
|
+
signal(directPeer, value).catch((error) => {
|
|
47274
|
+
if (live())
|
|
47275
|
+
onError?.(error);
|
|
47276
|
+
});
|
|
47277
|
+
else if (pendingSignals.length < MAX_PENDING_SIGNALS)
|
|
47278
|
+
pendingSignals.push(value);
|
|
47279
|
+
else
|
|
47280
|
+
onError?.(new Error("too many pending media signals"));
|
|
47281
|
+
},
|
|
47282
|
+
onTrack({ mid }) {
|
|
47283
|
+
if (!live())
|
|
47284
|
+
return;
|
|
47285
|
+
if (mode === "direct" && directPeer && validMid(mid))
|
|
47286
|
+
received.set(directPeer, mid);
|
|
47287
|
+
Promise.all([updateKeys(), applyPeerAudio()]).catch((error) => {
|
|
47288
|
+
if (live())
|
|
47289
|
+
onError?.(error);
|
|
47290
|
+
});
|
|
47291
|
+
},
|
|
47292
|
+
onActivity(value) {
|
|
47293
|
+
if (live())
|
|
47294
|
+
activity.sample(value);
|
|
47295
|
+
}
|
|
47296
|
+
});
|
|
47297
|
+
openingConnection = opening;
|
|
47298
|
+
let opened;
|
|
47299
|
+
try {
|
|
47300
|
+
opened = await opening;
|
|
47301
|
+
} finally {
|
|
47302
|
+
if (openingConnection === opening)
|
|
47303
|
+
openingConnection = null;
|
|
47304
|
+
}
|
|
47305
|
+
if (!live()) {
|
|
47306
|
+
await disposeConnection(opened);
|
|
47307
|
+
current(generation);
|
|
47308
|
+
}
|
|
47309
|
+
media = opened;
|
|
47310
|
+
await opened.mute(muted);
|
|
47311
|
+
current(generation);
|
|
47312
|
+
await opened.deafen(deafened);
|
|
47313
|
+
current(generation);
|
|
47314
|
+
if (microphoneVolume !== 1) {
|
|
47315
|
+
await opened.setMicrophoneVolume(microphoneVolume);
|
|
47316
|
+
current(generation);
|
|
47317
|
+
}
|
|
47318
|
+
await opened.prepare();
|
|
47319
|
+
current(generation);
|
|
47320
|
+
if (deadline) {
|
|
47321
|
+
await opened.grant(deadline);
|
|
47322
|
+
current(generation);
|
|
47323
|
+
}
|
|
47324
|
+
await updateKeys();
|
|
47325
|
+
current(generation);
|
|
47326
|
+
}
|
|
47327
|
+
async function applyDescription(result, required = false) {
|
|
47328
|
+
current();
|
|
47329
|
+
if (!result || result.errorCode || result.tracks?.some((track) => track.errorCode))
|
|
47330
|
+
throw new Error("call negotiation failed");
|
|
47331
|
+
const description = result.sessionDescription;
|
|
47332
|
+
if (!description) {
|
|
47333
|
+
if (required || result.requiresImmediateRenegotiation)
|
|
47334
|
+
throw new Error("missing call description");
|
|
47335
|
+
return;
|
|
47336
|
+
}
|
|
47337
|
+
if (!["offer", "answer"].includes(description.type) || typeof description.sdp !== "string" || !description.sdp.length)
|
|
47338
|
+
throw new Error("invalid call description");
|
|
47339
|
+
if (description.type === "offer") {
|
|
47340
|
+
const answer = await media.answer(description);
|
|
47341
|
+
current();
|
|
47342
|
+
await provider("renegotiate", { sessionId, sessionDescription: sdp(answer) });
|
|
47343
|
+
current();
|
|
47344
|
+
} else {
|
|
47345
|
+
await media.remote(description);
|
|
47346
|
+
current();
|
|
47347
|
+
}
|
|
47348
|
+
}
|
|
47349
|
+
async function updateKeys() {
|
|
47350
|
+
if (!media || !keys || mode !== "group")
|
|
47351
|
+
return;
|
|
47352
|
+
const receive = [];
|
|
47353
|
+
for (const peer of peers) {
|
|
47354
|
+
const mid = received.get(peer.holder);
|
|
47355
|
+
const member = keys.members.find((item) => toHex(item.leafId) === peer.holder);
|
|
47356
|
+
if (mid != null && member)
|
|
47357
|
+
receive.push({
|
|
47358
|
+
mid,
|
|
47359
|
+
baseKey: keys.baseKey,
|
|
47360
|
+
epoch: keys.epoch,
|
|
47361
|
+
leafIndex: member.leafIndex,
|
|
47362
|
+
contextId: 0
|
|
47363
|
+
});
|
|
47364
|
+
}
|
|
47365
|
+
await media.setKeys({ send: { baseKey: keys.baseKey, epoch: keys.epoch, leafIndex: keys.leafIndex, contextId: 0 }, receive });
|
|
47366
|
+
}
|
|
47367
|
+
function applyPeerAudio() {
|
|
47368
|
+
return Promise.all([...peerAudio].map(async ([id, entry]) => {
|
|
47369
|
+
const mid = received.get(id);
|
|
47370
|
+
if (!media || mid == null)
|
|
47371
|
+
return;
|
|
47372
|
+
const value = entry.value;
|
|
47373
|
+
try {
|
|
47374
|
+
await media.setPeerAudio({ mid, ...value });
|
|
47375
|
+
settlePeerAudio(id);
|
|
47376
|
+
} catch (error) {
|
|
47377
|
+
if (pendingPeerAudio.has(id))
|
|
47378
|
+
settlePeerAudio(id, error);
|
|
47379
|
+
else
|
|
47380
|
+
throw error;
|
|
47381
|
+
}
|
|
47382
|
+
}));
|
|
47383
|
+
}
|
|
47384
|
+
async function negotiate() {
|
|
47385
|
+
if (!started || !keys)
|
|
47386
|
+
return;
|
|
47387
|
+
if (mode === "group") {
|
|
47388
|
+
if (!sessionId) {
|
|
47389
|
+
const session = await provider("session/new", {});
|
|
47390
|
+
current();
|
|
47391
|
+
sessionId = session.sessionId;
|
|
47392
|
+
const offer = await media.offer();
|
|
47393
|
+
current();
|
|
47394
|
+
const sendMid = media.sendMid;
|
|
47395
|
+
if (!validMid(sendMid))
|
|
47396
|
+
throw new Error("microphone track unavailable");
|
|
47397
|
+
const trackName = toHex(randomBytes3(16));
|
|
47398
|
+
const result = await provider("tracks/new", {
|
|
47399
|
+
sessionId,
|
|
47400
|
+
sessionDescription: sdp(offer),
|
|
47401
|
+
tracks: [{ location: "local", mid: sendMid, trackName }]
|
|
47402
|
+
});
|
|
47403
|
+
current();
|
|
47404
|
+
if (!Array.isArray(result.tracks) || result.tracks.length !== 1 || result.tracks[0].mid !== sendMid || result.tracks[0].trackName !== trackName)
|
|
47405
|
+
throw new Error("microphone publication failed");
|
|
47406
|
+
await applyDescription(result, true);
|
|
47407
|
+
current();
|
|
47408
|
+
await connected();
|
|
47409
|
+
current();
|
|
47410
|
+
await publish({ sessionId, trackName });
|
|
47411
|
+
current();
|
|
47412
|
+
}
|
|
47413
|
+
const additions = peers.filter((peer) => peer.holder !== holder && peer.media?.sessionId && peer.media?.trackName && !received.has(peer.holder));
|
|
47414
|
+
if (additions.length) {
|
|
47415
|
+
await connected();
|
|
47416
|
+
current();
|
|
47417
|
+
const result = await provider("tracks/new", { sessionId, tracks: additions.map((peer) => ({
|
|
47418
|
+
location: "remote",
|
|
47419
|
+
sessionId: peer.media.sessionId,
|
|
47420
|
+
trackName: peer.media.trackName
|
|
47421
|
+
})) });
|
|
47422
|
+
current();
|
|
47423
|
+
if (!Array.isArray(result.tracks) || result.tracks.length !== additions.length)
|
|
47424
|
+
throw new Error("peer audio subscription failed");
|
|
47425
|
+
const mappings = [];
|
|
47426
|
+
const occupied = new Set([media.sendMid, ...received.values()]);
|
|
47427
|
+
for (const peer of additions) {
|
|
47428
|
+
const track = result.tracks?.find((item) => item.sessionId === peer.media.sessionId && item.trackName === peer.media.trackName);
|
|
47429
|
+
if (!validMid(track?.mid) || track.errorCode || occupied.has(track.mid))
|
|
47430
|
+
throw new Error("peer audio subscription failed");
|
|
47431
|
+
occupied.add(track.mid);
|
|
47432
|
+
mappings.push([peer.holder, track.mid]);
|
|
47433
|
+
}
|
|
47434
|
+
for (const [peer, mid] of mappings)
|
|
47435
|
+
received.set(peer, mid);
|
|
47436
|
+
updateActivity();
|
|
47437
|
+
await applyPeerAudio();
|
|
47438
|
+
current();
|
|
47439
|
+
await updateKeys();
|
|
47440
|
+
current();
|
|
47441
|
+
await applyDescription(result);
|
|
47442
|
+
}
|
|
47443
|
+
const removals = [...received].filter(([peer]) => !peers.some((item) => item.holder === peer));
|
|
47444
|
+
if (removals.length) {
|
|
47445
|
+
for (const [peer] of removals)
|
|
47446
|
+
received.delete(peer);
|
|
47447
|
+
updateActivity();
|
|
47448
|
+
await updateKeys();
|
|
47449
|
+
current();
|
|
47450
|
+
const result = await provider("tracks/close", { sessionId, tracks: removals.map(([, mid]) => ({ mid })) });
|
|
47451
|
+
await applyDescription(result);
|
|
47452
|
+
}
|
|
47453
|
+
} else {
|
|
47454
|
+
const peer = peers.find((item) => item.holder !== holder);
|
|
47455
|
+
if (!peer)
|
|
47456
|
+
return;
|
|
47457
|
+
directPeer = peer.holder;
|
|
47458
|
+
updateActivity();
|
|
47459
|
+
const generation = connectionGeneration;
|
|
47460
|
+
try {
|
|
47461
|
+
if (!media)
|
|
47462
|
+
await openConnection();
|
|
47463
|
+
current(generation);
|
|
47464
|
+
if (holder < peer.holder && !offered) {
|
|
47465
|
+
offered = true;
|
|
47466
|
+
const offer = await media.offer();
|
|
47467
|
+
current(generation);
|
|
47468
|
+
await signal(peer.holder, { type: "description", description: sdp(offer) });
|
|
47469
|
+
current(generation);
|
|
47470
|
+
}
|
|
47471
|
+
} catch (error) {
|
|
47472
|
+
if (!closed && generation !== connectionGeneration)
|
|
47473
|
+
return;
|
|
47474
|
+
await close();
|
|
47475
|
+
throw error;
|
|
47476
|
+
}
|
|
47477
|
+
}
|
|
47478
|
+
}
|
|
47479
|
+
return Object.freeze({
|
|
47480
|
+
async open() {
|
|
47481
|
+
current();
|
|
47482
|
+
try {
|
|
47483
|
+
await openConnection();
|
|
47484
|
+
started = true;
|
|
47485
|
+
await serialize(negotiate);
|
|
47486
|
+
for (const [value, from] of incomingSignals.splice(0))
|
|
47487
|
+
await handleSignal(value, from);
|
|
47488
|
+
} catch (error) {
|
|
47489
|
+
await close();
|
|
47490
|
+
throw error;
|
|
47491
|
+
}
|
|
47492
|
+
},
|
|
47493
|
+
grant(value) {
|
|
47494
|
+
current();
|
|
47495
|
+
deadline = value;
|
|
47496
|
+
return media?.grant(value);
|
|
47497
|
+
},
|
|
47498
|
+
async setKeys(value) {
|
|
47499
|
+
current();
|
|
47500
|
+
keys = value;
|
|
47501
|
+
await updateKeys();
|
|
47502
|
+
current();
|
|
47503
|
+
return serialize(negotiate);
|
|
47504
|
+
},
|
|
47505
|
+
async setParticipants(value) {
|
|
47506
|
+
current();
|
|
47507
|
+
peers = value;
|
|
47508
|
+
for (const id of peerAudio.keys())
|
|
47509
|
+
if (!peers.some((peer) => peer.holder === id)) {
|
|
47510
|
+
peerAudio.delete(id);
|
|
47511
|
+
settlePeerAudio(id, new Error("call participant unavailable"));
|
|
47512
|
+
}
|
|
47513
|
+
updateActivity();
|
|
47514
|
+
const nextPeer = peers.find((peer) => peer.holder !== holder)?.holder || null;
|
|
47515
|
+
if (mode === "direct" && started && directPeer && nextPeer !== directPeer)
|
|
47516
|
+
retireDirectConnection(nextPeer);
|
|
47517
|
+
await updateKeys();
|
|
47518
|
+
current();
|
|
47519
|
+
return serialize(negotiate);
|
|
47520
|
+
},
|
|
47521
|
+
signal(value, from) {
|
|
47522
|
+
if (closed)
|
|
47523
|
+
return Promise.reject(new Error("call media closed"));
|
|
47524
|
+
if (!started) {
|
|
47525
|
+
if (incomingSignals.length >= MAX_PENDING_SIGNALS)
|
|
47526
|
+
return Promise.reject(new Error("too many pending media signals"));
|
|
47527
|
+
incomingSignals.push([value, from]);
|
|
47528
|
+
return Promise.resolve();
|
|
47529
|
+
}
|
|
47530
|
+
return handleSignal(value, from);
|
|
47531
|
+
},
|
|
47532
|
+
mute(value) {
|
|
47533
|
+
current();
|
|
47534
|
+
muted = value === true;
|
|
47535
|
+
updateActivity();
|
|
47536
|
+
return media?.mute(muted);
|
|
47537
|
+
},
|
|
47538
|
+
deafen(value) {
|
|
47539
|
+
current();
|
|
47540
|
+
deafened = value === true;
|
|
47541
|
+
updateActivity();
|
|
47542
|
+
return media?.deafen(deafened);
|
|
47543
|
+
},
|
|
47544
|
+
async setMicrophoneVolume(value) {
|
|
47545
|
+
current();
|
|
47546
|
+
const previous = microphoneVolume;
|
|
47547
|
+
microphoneVolume = value;
|
|
47548
|
+
try {
|
|
47549
|
+
if (media)
|
|
47550
|
+
await media.setMicrophoneVolume(value);
|
|
47551
|
+
} catch (error) {
|
|
47552
|
+
if (microphoneVolume === value)
|
|
47553
|
+
microphoneVolume = previous;
|
|
47554
|
+
throw error;
|
|
47555
|
+
}
|
|
47556
|
+
},
|
|
47557
|
+
async setPeerAudio(id, value) {
|
|
47558
|
+
current();
|
|
47559
|
+
const mid = received.get(id);
|
|
47560
|
+
if (id === holder || !peers.some((peer) => peer.holder === id))
|
|
47561
|
+
throw new Error("call participant unavailable");
|
|
47562
|
+
let entry = peerAudio.get(id);
|
|
47563
|
+
if (!entry) {
|
|
47564
|
+
entry = { value: null, applied: null };
|
|
47565
|
+
peerAudio.set(id, entry);
|
|
47566
|
+
}
|
|
47567
|
+
entry.value = value;
|
|
47568
|
+
try {
|
|
47569
|
+
if (media && mid != null)
|
|
47570
|
+
await media.setPeerAudio({ mid, ...value });
|
|
47571
|
+
else {
|
|
47572
|
+
let pending = pendingPeerAudio.get(id);
|
|
47573
|
+
if (!pending) {
|
|
47574
|
+
pending = {};
|
|
47575
|
+
pending.promise = new Promise((resolve, reject) => {
|
|
47576
|
+
pending.resolve = resolve;
|
|
47577
|
+
pending.reject = reject;
|
|
47578
|
+
});
|
|
47579
|
+
pendingPeerAudio.set(id, pending);
|
|
47580
|
+
}
|
|
47581
|
+
await pending.promise;
|
|
47582
|
+
}
|
|
47583
|
+
if (peerAudio.get(id) === entry && entry.value === value)
|
|
47584
|
+
entry.applied = value;
|
|
47585
|
+
} catch (error) {
|
|
47586
|
+
if (peerAudio.get(id) === entry && entry.value === value) {
|
|
47587
|
+
if (entry.applied)
|
|
47588
|
+
entry.value = entry.applied;
|
|
47589
|
+
else
|
|
47590
|
+
peerAudio.delete(id);
|
|
47591
|
+
}
|
|
47592
|
+
throw error;
|
|
47593
|
+
}
|
|
47594
|
+
},
|
|
47595
|
+
close
|
|
47596
|
+
});
|
|
47597
|
+
function handleSignal(value, from) {
|
|
47598
|
+
return serialize(async () => {
|
|
47599
|
+
if (mode !== "direct")
|
|
47600
|
+
throw new Error("unadmitted media signal");
|
|
47601
|
+
if (from === holder || !peers.some((peer) => peer.holder === from))
|
|
47602
|
+
return;
|
|
47603
|
+
const generation = connectionGeneration;
|
|
47604
|
+
try {
|
|
47605
|
+
await negotiate();
|
|
47606
|
+
current(generation);
|
|
47607
|
+
if (value.type === "description") {
|
|
47608
|
+
if (value.description.type === "offer") {
|
|
47609
|
+
if (holder < from)
|
|
47610
|
+
throw new Error("unexpected direct offer");
|
|
47611
|
+
offered = true;
|
|
47612
|
+
const answer = await media.answer(value.description);
|
|
47613
|
+
current(generation);
|
|
47614
|
+
await signal(from, { type: "description", description: sdp(answer) });
|
|
47615
|
+
} else
|
|
47616
|
+
await media.remote(value.description);
|
|
47617
|
+
} else if (value.type === "ice")
|
|
47618
|
+
await media.ice(value.candidate);
|
|
47619
|
+
else
|
|
47620
|
+
throw new Error("invalid media signal");
|
|
47621
|
+
current(generation);
|
|
47622
|
+
for (const pending of pendingSignals.splice(0)) {
|
|
47623
|
+
await signal(from, pending);
|
|
47624
|
+
current(generation);
|
|
47625
|
+
}
|
|
47626
|
+
} catch (error) {
|
|
47627
|
+
if (!closed && generation !== connectionGeneration)
|
|
47628
|
+
return;
|
|
47629
|
+
throw error;
|
|
47630
|
+
}
|
|
47631
|
+
});
|
|
47632
|
+
}
|
|
47633
|
+
}
|
|
47634
|
+
|
|
47635
|
+
// ../../core/calls/observation.js
|
|
47636
|
+
function openCallObservation({ cloud, capability, endpoint, protocol, onChange, onEmpty, onError }) {
|
|
47637
|
+
let verified = null;
|
|
47638
|
+
let identities = new Map;
|
|
47639
|
+
const audio = new Map;
|
|
47640
|
+
const departed = new Set;
|
|
47641
|
+
let last = "";
|
|
47642
|
+
let pending = null;
|
|
47643
|
+
let dirty = false;
|
|
47644
|
+
let closed = false;
|
|
47645
|
+
const mailbox = openCallMailbox({
|
|
47646
|
+
cloud,
|
|
47647
|
+
capability,
|
|
47648
|
+
endpoint,
|
|
47649
|
+
protocol,
|
|
47650
|
+
onError,
|
|
47651
|
+
stream: false,
|
|
47652
|
+
onChange(record) {
|
|
47653
|
+
const head = record.head;
|
|
47654
|
+
const admissions = head?.data?.participants || [];
|
|
47655
|
+
if (head && head !== verified) {
|
|
47656
|
+
if (!Number.isSafeInteger(head.data?.epoch) || head.data.epoch < 0)
|
|
47657
|
+
throw new Error("invalid call observation");
|
|
47658
|
+
const nextIdentities = verifyCallAdmissions(admissions, protocol);
|
|
47659
|
+
if (!admissions.some((item) => item.actor === head.actor && item.holder === head.holder && item.endpoint === head.endpoint)) {
|
|
47660
|
+
throw new Error("unadmitted call head");
|
|
47661
|
+
}
|
|
47662
|
+
identities = nextIdentities;
|
|
47663
|
+
} else if (!head)
|
|
47664
|
+
identities.clear();
|
|
47665
|
+
verified = head;
|
|
47666
|
+
if (record.historyLost) {
|
|
47667
|
+
audio.clear();
|
|
47668
|
+
departed.clear();
|
|
47669
|
+
}
|
|
47670
|
+
const holders = new Set(admissions.map((item) => item.holder));
|
|
47671
|
+
for (const holder of audio.keys())
|
|
47672
|
+
if (!holders.has(holder))
|
|
47673
|
+
audio.delete(holder);
|
|
47674
|
+
for (const holder of departed)
|
|
47675
|
+
if (!holders.has(holder))
|
|
47676
|
+
departed.delete(holder);
|
|
47677
|
+
for (const packet of record.events) {
|
|
47678
|
+
if (!admissions.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint))
|
|
47679
|
+
continue;
|
|
47680
|
+
if (packet.kind === "leave")
|
|
47681
|
+
departed.add(packet.holder);
|
|
47682
|
+
if (packet.kind === "pulse")
|
|
47683
|
+
audio.set(packet.holder, { muted: packet.data.muted === true, deafened: packet.data.deafened === true });
|
|
47684
|
+
}
|
|
47685
|
+
const roster = admissions.filter((item) => !departed.has(item.holder)).map((item) => ({
|
|
47686
|
+
...identities.get(item.holder),
|
|
47687
|
+
holder: item.holder,
|
|
47688
|
+
muted: audio.get(item.holder)?.muted === true || audio.get(item.holder)?.deafened === true,
|
|
47689
|
+
deafened: audio.get(item.holder)?.deafened === true
|
|
47690
|
+
}));
|
|
47691
|
+
const fingerprint = JSON.stringify(roster);
|
|
47692
|
+
if (fingerprint !== last) {
|
|
47693
|
+
last = fingerprint;
|
|
47694
|
+
onChange(roster);
|
|
47695
|
+
}
|
|
47696
|
+
if (!head || admissions.length && roster.length === 0)
|
|
47697
|
+
onEmpty?.();
|
|
47698
|
+
}
|
|
47699
|
+
});
|
|
47700
|
+
function read() {
|
|
47701
|
+
if (closed)
|
|
47702
|
+
return Promise.resolve();
|
|
47703
|
+
dirty = true;
|
|
47704
|
+
pending ||= (async () => {
|
|
47705
|
+
while (dirty && !closed) {
|
|
47706
|
+
dirty = false;
|
|
47707
|
+
await mailbox.read();
|
|
47708
|
+
}
|
|
47709
|
+
})().finally(() => {
|
|
47710
|
+
pending = null;
|
|
47711
|
+
});
|
|
47712
|
+
return pending;
|
|
47713
|
+
}
|
|
47714
|
+
return Object.freeze({ read, close() {
|
|
47715
|
+
closed = true;
|
|
47716
|
+
mailbox.close();
|
|
47717
|
+
} });
|
|
47718
|
+
}
|
|
47719
|
+
|
|
47720
|
+
// ../../core/calls/session.js
|
|
47721
|
+
var initial = () => ({ phase: "idle", chatId: null, callId: null, joiningChatId: null, mode: null, transport: null, available: null, elsewhere: null, participants: [], speaking: [], peerAudio: {}, muted: false, deafened: false, microphoneVolume: 100, error: null });
|
|
47722
|
+
var cancelled2 = () => Object.assign(new Error("call cancelled"), { code: "calls/cancelled" });
|
|
47723
|
+
var ended = () => Object.assign(new Error("this call has ended"), { code: "calls/ended" });
|
|
47724
|
+
var callError = (chatId, error) => ({ chatId, code: error?.code || "calls/failed", message: error?.message || "could not connect the call" });
|
|
47725
|
+
function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
|
|
47726
|
+
const listeners = new Set;
|
|
47727
|
+
const discoveries = new Map;
|
|
47728
|
+
const discoveryReads = new Map;
|
|
47729
|
+
const observers = new Map;
|
|
47730
|
+
const retiring = new Set;
|
|
47731
|
+
let state = initial();
|
|
47732
|
+
const confirmedPeerAudio = new Map;
|
|
47733
|
+
let identity = null;
|
|
47734
|
+
let endpoint = null;
|
|
47735
|
+
let leaseEndpoint = null;
|
|
47736
|
+
let lease = null;
|
|
47737
|
+
let active = null;
|
|
47738
|
+
let focused = null;
|
|
47739
|
+
let generation = 0;
|
|
47740
|
+
let joinIntent = 0;
|
|
47741
|
+
let joining = null;
|
|
47742
|
+
let closed = false;
|
|
47743
|
+
let foreground = true;
|
|
47744
|
+
let online = true;
|
|
47745
|
+
let leaving = Promise.resolve();
|
|
47746
|
+
let drainingSources = Promise.resolve();
|
|
47747
|
+
let closingSession = null;
|
|
47748
|
+
const emit = (patch) => {
|
|
47749
|
+
if (Object.entries(patch).every(([field, value]) => Object.is(state[field], value)))
|
|
47750
|
+
return;
|
|
47751
|
+
if (patch.phase && patch.phase !== state.phase) {
|
|
47752
|
+
diag?.("call.phase", {
|
|
47753
|
+
phase: patch.phase,
|
|
47754
|
+
previous: state.phase,
|
|
47755
|
+
mode: patch.mode || state.mode,
|
|
47756
|
+
participants: (patch.participants || state.participants).length
|
|
47757
|
+
});
|
|
47758
|
+
}
|
|
47759
|
+
state = { ...state, ...patch };
|
|
47760
|
+
for (const listener of listeners)
|
|
47761
|
+
listener();
|
|
47762
|
+
};
|
|
47763
|
+
function announce(chatId, callId, event) {
|
|
47764
|
+
chat.getSnapshot().sendChatMessage(chatId, makeCallMessage(callId, event)).catch((error) => {
|
|
47765
|
+
diag?.("call.message.error", { event, code: error?.code || "" });
|
|
47766
|
+
});
|
|
47767
|
+
}
|
|
47768
|
+
function current(token) {
|
|
47769
|
+
if (closed || !identity || token !== generation)
|
|
47770
|
+
throw cancelled2();
|
|
47771
|
+
}
|
|
47772
|
+
function fail(error) {
|
|
47773
|
+
if (!identity || closed)
|
|
47774
|
+
return;
|
|
47775
|
+
diag?.("call.error", {
|
|
47776
|
+
code: error?.code || "calls/failed",
|
|
47777
|
+
phase: state.phase,
|
|
47778
|
+
mode: state.mode,
|
|
47779
|
+
participants: state.participants.length,
|
|
47780
|
+
message: error?.message || "call failed"
|
|
47781
|
+
});
|
|
47782
|
+
const chatId = active?.chatId || state.chatId;
|
|
47783
|
+
leave();
|
|
47784
|
+
emit({ phase: "error", chatId, error: callError(chatId, error) });
|
|
47785
|
+
}
|
|
47786
|
+
function available() {
|
|
47787
|
+
const entry = discoveries.get(focused);
|
|
47788
|
+
const value = (chatId, current2) => current2?.descriptor ? {
|
|
47789
|
+
chatId,
|
|
47790
|
+
callId: current2.descriptor.callId,
|
|
47791
|
+
startedAt: current2.descriptor.startedAt,
|
|
47792
|
+
participants: current2.descriptor.participants || 1,
|
|
47793
|
+
roster: (active?.discovery === current2 ? active.participants : current2.roster).filter((peer) => ![...retiring].some((call) => call.callId === current2.descriptor.callId && call.endpoint.holder === peer.holder))
|
|
47794
|
+
} : null;
|
|
47795
|
+
for (const observer of observers.values()) {
|
|
47796
|
+
if (!observer.onAvailable)
|
|
47797
|
+
continue;
|
|
47798
|
+
const next2 = value(observer.chatId, discoveries.get(observer.chatId));
|
|
47799
|
+
const fingerprint = JSON.stringify(next2);
|
|
47800
|
+
if (observer.fingerprint === fingerprint)
|
|
47801
|
+
continue;
|
|
47802
|
+
observer.fingerprint = fingerprint;
|
|
47803
|
+
try {
|
|
47804
|
+
observer.onAvailable(next2);
|
|
47805
|
+
} catch {}
|
|
47806
|
+
}
|
|
47807
|
+
const next = value(focused, entry);
|
|
47808
|
+
if (JSON.stringify(next) !== JSON.stringify(state.available))
|
|
47809
|
+
emit({ available: next });
|
|
47810
|
+
}
|
|
47811
|
+
function closeDiscovery(entry) {
|
|
47812
|
+
entry.mailbox.close();
|
|
47813
|
+
entry.observation?.close();
|
|
47814
|
+
entry.observation = null;
|
|
47815
|
+
entry.observedCallId = null;
|
|
47816
|
+
}
|
|
47817
|
+
function observeRoster(entry, refresh = false) {
|
|
47818
|
+
const descriptor = entry.descriptor;
|
|
47819
|
+
const callId = descriptor?.callId;
|
|
47820
|
+
if (!callId || active?.discovery === entry || ![...observers.values()].some((item) => item.chatId === entry.epochState.manifest.chatId)) {
|
|
47821
|
+
entry.observation?.close();
|
|
47822
|
+
entry.observation = null;
|
|
47823
|
+
entry.observedCallId = null;
|
|
47824
|
+
return;
|
|
47825
|
+
}
|
|
47826
|
+
if (entry.observedCallId === callId) {
|
|
47827
|
+
if (refresh)
|
|
47828
|
+
entry.observation.read().catch((error) => diag?.("call.discovery.error", { code: error?.code || "calls/roster" }));
|
|
47829
|
+
return;
|
|
47830
|
+
}
|
|
47831
|
+
entry.observation?.close();
|
|
47832
|
+
entry.roster = [];
|
|
47833
|
+
entry.observedCallId = callId;
|
|
47834
|
+
const token = generation;
|
|
47835
|
+
const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint, callId });
|
|
47836
|
+
const secret = fromHexBytes(descriptor.secret);
|
|
47837
|
+
const capability = createCallCapability(secret, cloud.environment, "signaling", [callId]);
|
|
47838
|
+
cleanBytes(secret);
|
|
47839
|
+
entry.observation = openCallObservation({
|
|
47840
|
+
cloud,
|
|
47841
|
+
capability,
|
|
47842
|
+
endpoint,
|
|
47843
|
+
protocol,
|
|
47844
|
+
onChange(roster) {
|
|
47845
|
+
if (generation !== token || entry.observedCallId !== callId || discoveries.get(entry.epochState.manifest.chatId) !== entry)
|
|
47846
|
+
return;
|
|
47847
|
+
entry.roster = roster;
|
|
47848
|
+
available();
|
|
47849
|
+
},
|
|
47850
|
+
onError(error) {
|
|
47851
|
+
if (generation === token)
|
|
47852
|
+
diag?.("call.discovery.error", { code: error?.code || "calls/roster" });
|
|
47853
|
+
},
|
|
47854
|
+
onEmpty() {
|
|
47855
|
+
if (generation === token && entry.observedCallId === callId)
|
|
47856
|
+
finishDiscovery(entry, descriptor);
|
|
47857
|
+
}
|
|
47858
|
+
});
|
|
47859
|
+
entry.observation.read().catch(() => {});
|
|
47860
|
+
}
|
|
47861
|
+
function prune() {
|
|
47862
|
+
for (const [chatId, value] of discoveries) {
|
|
47863
|
+
if (![...observers.values()].some((item) => item.chatId === chatId) && chatId !== active?.chatId && chatId !== joining?.chatId && ![...retiring].some((call) => call.chatId === chatId)) {
|
|
47864
|
+
closeDiscovery(value);
|
|
47865
|
+
discoveries.delete(chatId);
|
|
47866
|
+
} else
|
|
47867
|
+
observeRoster(value);
|
|
47868
|
+
}
|
|
47869
|
+
}
|
|
47870
|
+
function discovery(chatId, epochState) {
|
|
47871
|
+
const token = generation;
|
|
47872
|
+
const existing = discoveries.get(chatId);
|
|
47873
|
+
if (existing?.epochState.manifest.epochId === epochState.manifest.epochId)
|
|
47874
|
+
return existing;
|
|
47875
|
+
if (existing)
|
|
47876
|
+
closeDiscovery(existing);
|
|
47877
|
+
const protocol = createCallProtocol({ realm: cloud.environment, epochState, identity, endpoint });
|
|
47878
|
+
const entry = {
|
|
47879
|
+
epochState,
|
|
47880
|
+
protocol,
|
|
47881
|
+
descriptor: null,
|
|
47882
|
+
mailbox: null,
|
|
47883
|
+
observation: null,
|
|
47884
|
+
observedCallId: null,
|
|
47885
|
+
roster: [],
|
|
47886
|
+
revision: null,
|
|
47887
|
+
expiredCallId: null,
|
|
47888
|
+
finishing: new Map
|
|
47889
|
+
};
|
|
47890
|
+
entry.mailbox = openCallMailbox({
|
|
47891
|
+
cloud,
|
|
47892
|
+
capability: createCallDiscovery(epochState, cloud.environment),
|
|
47893
|
+
endpoint,
|
|
47894
|
+
protocol,
|
|
47895
|
+
onChange(record) {
|
|
47896
|
+
if (token !== generation || discoveries.get(chatId) !== entry)
|
|
47897
|
+
return;
|
|
47898
|
+
const descriptor = record.head?.data || null;
|
|
47899
|
+
if (descriptor && (!/^[a-f0-9]{64}$/u.test(descriptor.callId) || !/^[a-f0-9]{64}$/u.test(descriptor.secret) || descriptor.mode !== epochState.manifest.lineage || !Number.isSafeInteger(descriptor.startedAt) || descriptor.startedAt < 0 || descriptor.startedAt > Date.now() + 5000))
|
|
47900
|
+
throw new Error("invalid call discovery");
|
|
47901
|
+
const previous = entry.descriptor;
|
|
47902
|
+
entry.expiredCallId = !record.head ? previous?.callId || entry.expiredCallId : null;
|
|
47903
|
+
entry.descriptor = descriptor;
|
|
47904
|
+
if (!record.head && previous)
|
|
47905
|
+
finishDiscovery(entry, previous);
|
|
47906
|
+
const changed = entry.revision !== record.revision;
|
|
47907
|
+
entry.revision = record.revision;
|
|
47908
|
+
if (active?.discovery === entry && active.published && descriptor?.callId !== active.callId) {
|
|
47909
|
+
leave();
|
|
47910
|
+
}
|
|
47911
|
+
observeRoster(entry, changed);
|
|
47912
|
+
available();
|
|
47913
|
+
},
|
|
47914
|
+
onError(error) {
|
|
47915
|
+
if (token !== generation || discoveries.get(chatId) !== entry)
|
|
47916
|
+
return;
|
|
47917
|
+
diag?.("call.discovery.error", { code: error?.code || "calls/discovery" });
|
|
47918
|
+
}
|
|
47919
|
+
});
|
|
47920
|
+
discoveries.set(chatId, entry);
|
|
47921
|
+
return entry;
|
|
47922
|
+
}
|
|
47923
|
+
function readDiscovery(chatId, preparedEpoch) {
|
|
47924
|
+
const epochState = preparedEpoch || chat.getSnapshot().getOwnerChat(chatId)?.epochState;
|
|
47925
|
+
const epochId = epochState?.manifest.epochId;
|
|
47926
|
+
const pending = discoveryReads.get(chatId);
|
|
47927
|
+
if (pending && pending.epochId === epochId)
|
|
47928
|
+
return pending.promise;
|
|
47929
|
+
const slot = { epochId, promise: null };
|
|
47930
|
+
const token = generation;
|
|
47931
|
+
const reading = Promise.resolve().then(async () => {
|
|
47932
|
+
current(token);
|
|
47933
|
+
if (!online)
|
|
47934
|
+
throw Object.assign(new Error("chat unavailable"), { code: "unavailable" });
|
|
47935
|
+
if (!epochState)
|
|
47936
|
+
throw Object.assign(new Error("chat unavailable"), { code: "calls/chat-not-ready" });
|
|
47937
|
+
const entry = discovery(chatId, epochState);
|
|
47938
|
+
await entry.mailbox.read();
|
|
47939
|
+
current(token);
|
|
47940
|
+
return entry;
|
|
47941
|
+
}).finally(() => {
|
|
47942
|
+
if (discoveryReads.get(chatId) === slot)
|
|
47943
|
+
discoveryReads.delete(chatId);
|
|
47944
|
+
prune();
|
|
47945
|
+
});
|
|
47946
|
+
slot.promise = reading;
|
|
47947
|
+
discoveryReads.set(chatId, slot);
|
|
47948
|
+
return reading;
|
|
47949
|
+
}
|
|
47950
|
+
function observe(chatId, onAvailable) {
|
|
47951
|
+
if (!chatId || closed)
|
|
47952
|
+
return () => {};
|
|
47953
|
+
const observer = { chatId, onAvailable, fingerprint: undefined };
|
|
47954
|
+
observers.set(observer, observer);
|
|
47955
|
+
focused = chatId;
|
|
47956
|
+
if (state.error?.chatId === chatId)
|
|
47957
|
+
emit({ error: null });
|
|
47958
|
+
available();
|
|
47959
|
+
const entry = discoveries.get(chatId);
|
|
47960
|
+
if (entry)
|
|
47961
|
+
observeRoster(entry);
|
|
47962
|
+
else if (identity && online)
|
|
47963
|
+
readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
|
|
47964
|
+
return () => {
|
|
47965
|
+
if (!observers.delete(observer))
|
|
47966
|
+
return;
|
|
47967
|
+
focused = [...observers.values()].at(-1)?.chatId || null;
|
|
47968
|
+
available();
|
|
47969
|
+
prune();
|
|
47970
|
+
};
|
|
47971
|
+
}
|
|
47972
|
+
async function joinAttempt(chatId, intent, expectedCallId, onlyExisting) {
|
|
47973
|
+
if (!identity || !mediaPort || !mls || !cloud.calls)
|
|
47974
|
+
throw new Error("calling unavailable");
|
|
47975
|
+
if (!foreground)
|
|
47976
|
+
throw new Error("open veyl to join a call");
|
|
47977
|
+
if (active?.chatId === chatId)
|
|
47978
|
+
return chatId;
|
|
47979
|
+
const token = generation;
|
|
47980
|
+
const startedAt = Date.now();
|
|
47981
|
+
const step = (stage) => diag?.("call.join.stage", { stage, elapsedMs: Date.now() - startedAt });
|
|
47982
|
+
const check = () => {
|
|
47983
|
+
current(token);
|
|
47984
|
+
if (intent !== joinIntent || !foreground)
|
|
47985
|
+
throw cancelled2();
|
|
47986
|
+
};
|
|
47987
|
+
emit({ ...!active ? { phase: "joining", chatId } : {}, error: null });
|
|
47988
|
+
await mediaPort.prepare?.();
|
|
47989
|
+
check();
|
|
47990
|
+
step("permission");
|
|
47991
|
+
if (!onlyExisting)
|
|
47992
|
+
chatId = await chat.getSnapshot().materializeChat(chatId);
|
|
47993
|
+
check();
|
|
47994
|
+
joining.chatId = chatId;
|
|
47995
|
+
step("chat");
|
|
47996
|
+
let call;
|
|
47997
|
+
try {
|
|
47998
|
+
const epochState = await chat.getSnapshot().prepareCall(chatId);
|
|
47999
|
+
check();
|
|
48000
|
+
const entry = await readDiscovery(chatId, epochState);
|
|
48001
|
+
check();
|
|
48002
|
+
const discoveryRecord = entry.mailbox.getSnapshot();
|
|
48003
|
+
step("discovery");
|
|
48004
|
+
const create = !entry.descriptor;
|
|
48005
|
+
if (expectedCallId && (onlyExisting || entry.epochState.manifest.lineage === "direct") && entry.descriptor?.callId !== expectedCallId)
|
|
48006
|
+
throw ended();
|
|
48007
|
+
const descriptor = entry.descriptor || {
|
|
48008
|
+
callId: toHex(randomBytes3(32)),
|
|
48009
|
+
secret: toHex(randomBytes3(32)),
|
|
48010
|
+
mode: entry.epochState.manifest.lineage,
|
|
48011
|
+
participants: 1,
|
|
48012
|
+
startedAt: Date.now()
|
|
48013
|
+
};
|
|
48014
|
+
const callEndpoint = createCallEndpoint();
|
|
48015
|
+
call = {
|
|
48016
|
+
chatId,
|
|
48017
|
+
callId: descriptor.callId,
|
|
48018
|
+
discovery: entry,
|
|
48019
|
+
descriptor,
|
|
48020
|
+
endpoint: callEndpoint,
|
|
48021
|
+
published: false,
|
|
48022
|
+
room: null,
|
|
48023
|
+
member: null,
|
|
48024
|
+
media: null,
|
|
48025
|
+
owner: null,
|
|
48026
|
+
participants: [],
|
|
48027
|
+
error: null,
|
|
48028
|
+
timer: null,
|
|
48029
|
+
joinTimer: null,
|
|
48030
|
+
mediaReady: false,
|
|
48031
|
+
leaving: false,
|
|
48032
|
+
muteTail: Promise.resolve()
|
|
48033
|
+
};
|
|
48034
|
+
joining.candidate = call;
|
|
48035
|
+
const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint: callEndpoint, callId: descriptor.callId });
|
|
48036
|
+
call.protocol = protocol;
|
|
48037
|
+
const ownMember = (value) => {
|
|
48038
|
+
try {
|
|
48039
|
+
check();
|
|
48040
|
+
if (call.leaving)
|
|
48041
|
+
throw cancelled2();
|
|
48042
|
+
} catch (error) {
|
|
48043
|
+
cleanBytes(value.snapshot);
|
|
48044
|
+
throw error;
|
|
48045
|
+
}
|
|
48046
|
+
call.member = value;
|
|
48047
|
+
return value;
|
|
48048
|
+
};
|
|
48049
|
+
const callMember = ownMember(await mls.createMember(fromHexBytes(callEndpoint.holder)));
|
|
48050
|
+
const member = create ? callMember : ownMember(await mls.createKeyPackage(callMember.snapshot));
|
|
48051
|
+
if (!create)
|
|
48052
|
+
cleanBytes(callMember.snapshot);
|
|
48053
|
+
check();
|
|
48054
|
+
const admission = protocol.sign("join", { keyPackage: member.keyPackage ? encodeCallBytes(member.keyPackage) : "", signaturePK: toHex(member.signaturePK) });
|
|
48055
|
+
const secret = fromHexBytes(descriptor.secret);
|
|
48056
|
+
const capability = createCallCapability(secret, cloud.environment, "signaling", [descriptor.callId]);
|
|
48057
|
+
cleanBytes(secret);
|
|
48058
|
+
const valid = () => token === generation && active === call && !call.leaving;
|
|
48059
|
+
const preparing = () => token === generation && intent === joinIntent && !call.leaving;
|
|
48060
|
+
const checkCall = () => {
|
|
48061
|
+
check();
|
|
48062
|
+
if (call.leaving)
|
|
48063
|
+
throw cancelled2();
|
|
48064
|
+
if (call.error)
|
|
48065
|
+
throw call.error;
|
|
48066
|
+
};
|
|
48067
|
+
const connectionProgress = () => {
|
|
48068
|
+
if (!valid() || state.phase === "connected")
|
|
48069
|
+
return;
|
|
48070
|
+
if (call.mediaReady && descriptor.mode === "direct" && state.participants.length < 2) {
|
|
48071
|
+
clearTimeout(call.joinTimer);
|
|
48072
|
+
call.joinTimer = null;
|
|
48073
|
+
emit({ phase: "waiting" });
|
|
48074
|
+
} else {
|
|
48075
|
+
emit({ phase: "connecting" });
|
|
48076
|
+
if (!call.joinTimer)
|
|
48077
|
+
call.joinTimer = setTimeout(() => {
|
|
48078
|
+
if (valid())
|
|
48079
|
+
fail(new Error("call did not connect"));
|
|
48080
|
+
}, 30000);
|
|
48081
|
+
}
|
|
48082
|
+
};
|
|
48083
|
+
call.media = createCallMediaSession({
|
|
48084
|
+
port: mediaPort,
|
|
48085
|
+
mode: descriptor.mode,
|
|
48086
|
+
holder: callEndpoint.holder,
|
|
48087
|
+
provider: (...args) => call.owner.provider(...args),
|
|
48088
|
+
signal: (...args) => call.room.signal(...args),
|
|
48089
|
+
publish: (value) => call.room.publishMedia(value),
|
|
48090
|
+
onState(value) {
|
|
48091
|
+
if (!valid())
|
|
48092
|
+
return;
|
|
48093
|
+
diag?.("call.media.state", {
|
|
48094
|
+
state: value.state,
|
|
48095
|
+
reason: value.reason || "",
|
|
48096
|
+
mode: descriptor.mode,
|
|
48097
|
+
transport: value.transport || "",
|
|
48098
|
+
participants: call.participants.length
|
|
48099
|
+
});
|
|
48100
|
+
if (value.state === "connected") {
|
|
48101
|
+
clearTimeout(call.joinTimer);
|
|
48102
|
+
call.joinTimer = null;
|
|
48103
|
+
emit({ phase: "connected", transport: value.transport || (descriptor.mode === "group" ? "relay" : null) });
|
|
48104
|
+
} else if (value.state === "connecting") {
|
|
48105
|
+
emit({ phase: "connecting" });
|
|
48106
|
+
connectionProgress();
|
|
48107
|
+
} else if (["failed", "closed"].includes(value.state))
|
|
48108
|
+
fail(Object.assign(new Error("call connection ended"), { code: "calls/media-ended" }));
|
|
48109
|
+
},
|
|
48110
|
+
onExpired() {
|
|
48111
|
+
if (valid())
|
|
48112
|
+
fail(new Error("call connection lost"));
|
|
48113
|
+
},
|
|
48114
|
+
onSpeaking(speaking) {
|
|
48115
|
+
if (valid())
|
|
48116
|
+
emit({ speaking });
|
|
48117
|
+
},
|
|
48118
|
+
onError(error) {
|
|
48119
|
+
if (valid())
|
|
48120
|
+
fail(error);
|
|
48121
|
+
else
|
|
48122
|
+
call.error = error;
|
|
48123
|
+
}
|
|
48124
|
+
});
|
|
48125
|
+
call.room = openCallRoom({
|
|
48126
|
+
cloud,
|
|
48127
|
+
capability,
|
|
48128
|
+
protocol,
|
|
48129
|
+
endpoint: callEndpoint,
|
|
48130
|
+
mls,
|
|
48131
|
+
member,
|
|
48132
|
+
admission,
|
|
48133
|
+
callId: descriptor.callId,
|
|
48134
|
+
onKeys(value) {
|
|
48135
|
+
if (!valid() && !preparing())
|
|
48136
|
+
return;
|
|
48137
|
+
call.media.setKeys(value).catch((error) => {
|
|
48138
|
+
if (valid())
|
|
48139
|
+
fail(error);
|
|
48140
|
+
else
|
|
48141
|
+
call.error = error;
|
|
48142
|
+
});
|
|
48143
|
+
},
|
|
48144
|
+
onParticipants(value) {
|
|
48145
|
+
if (!valid() && !preparing())
|
|
48146
|
+
return;
|
|
48147
|
+
call.participants = value;
|
|
48148
|
+
call.media.setParticipants(value).catch((error) => {
|
|
48149
|
+
if (valid())
|
|
48150
|
+
fail(error);
|
|
48151
|
+
else
|
|
48152
|
+
call.error = error;
|
|
48153
|
+
});
|
|
48154
|
+
if (!valid())
|
|
48155
|
+
return;
|
|
48156
|
+
for (const peer of value) {
|
|
48157
|
+
const preference = state.peerAudio[peer.chatPK];
|
|
48158
|
+
if (preference)
|
|
48159
|
+
call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
|
|
48160
|
+
}
|
|
48161
|
+
emit({ participants: value });
|
|
48162
|
+
available();
|
|
48163
|
+
call.refresh?.();
|
|
48164
|
+
if (call.mediaReady)
|
|
48165
|
+
connectionProgress();
|
|
48166
|
+
},
|
|
48167
|
+
onSignal: (value, from) => valid() || preparing() ? call.media.signal(value, from) : undefined,
|
|
48168
|
+
onRemoved() {
|
|
48169
|
+
if (valid())
|
|
48170
|
+
leave();
|
|
48171
|
+
else
|
|
48172
|
+
call.error = cancelled2();
|
|
48173
|
+
},
|
|
48174
|
+
onError(error) {
|
|
48175
|
+
if (valid())
|
|
48176
|
+
fail(error);
|
|
48177
|
+
else
|
|
48178
|
+
call.error = error;
|
|
48179
|
+
}
|
|
48180
|
+
});
|
|
48181
|
+
await applyAudio(call);
|
|
48182
|
+
checkCall();
|
|
48183
|
+
await call.media.setMicrophoneVolume(state.microphoneVolume / 100);
|
|
48184
|
+
checkCall();
|
|
48185
|
+
await call.room.start({ create });
|
|
48186
|
+
checkCall();
|
|
48187
|
+
step("admission");
|
|
48188
|
+
if (create) {
|
|
48189
|
+
try {
|
|
48190
|
+
await entry.mailbox.commit(descriptor, [], { expectedRevision: discoveryRecord.revision });
|
|
48191
|
+
} catch (error) {
|
|
48192
|
+
if (error?.code === "calls/conflict")
|
|
48193
|
+
throw Object.assign(new Error("another call started"), { code: "calls/discovery-conflict" });
|
|
48194
|
+
throw error;
|
|
48195
|
+
}
|
|
48196
|
+
checkCall();
|
|
48197
|
+
announce(chatId, descriptor.callId, "started");
|
|
48198
|
+
} else {
|
|
48199
|
+
await entry.mailbox.read();
|
|
48200
|
+
checkCall();
|
|
48201
|
+
if (entry.descriptor?.callId !== descriptor.callId) {
|
|
48202
|
+
if (expectedCallId && (onlyExisting || descriptor.mode === "direct"))
|
|
48203
|
+
throw ended();
|
|
48204
|
+
throw Object.assign(new Error("the call room changed"), { code: "calls/discovery-conflict" });
|
|
48205
|
+
}
|
|
48206
|
+
}
|
|
48207
|
+
call.published = true;
|
|
48208
|
+
await stopCall();
|
|
48209
|
+
checkCall();
|
|
48210
|
+
mountLease(identity, callEndpoint);
|
|
48211
|
+
call.owner = lease;
|
|
48212
|
+
active = call;
|
|
48213
|
+
observeRoster(entry);
|
|
48214
|
+
available();
|
|
48215
|
+
emit({
|
|
48216
|
+
phase: state.elsewhere ? "transferring" : "joining",
|
|
48217
|
+
chatId,
|
|
48218
|
+
callId: descriptor.callId,
|
|
48219
|
+
mode: descriptor.mode,
|
|
48220
|
+
error: null,
|
|
48221
|
+
speaking: [],
|
|
48222
|
+
participants: call.participants
|
|
48223
|
+
});
|
|
48224
|
+
for (const peer of call.participants) {
|
|
48225
|
+
const preference = state.peerAudio[peer.chatPK];
|
|
48226
|
+
if (preference)
|
|
48227
|
+
call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
|
|
48228
|
+
}
|
|
48229
|
+
await lease.acquire({ chatId, callId: descriptor.callId });
|
|
48230
|
+
check();
|
|
48231
|
+
step("ownership");
|
|
48232
|
+
if (!valid())
|
|
48233
|
+
throw cancelled2();
|
|
48234
|
+
connectionProgress();
|
|
48235
|
+
await call.media.open();
|
|
48236
|
+
check();
|
|
48237
|
+
step("media");
|
|
48238
|
+
call.mediaReady = true;
|
|
48239
|
+
connectionProgress();
|
|
48240
|
+
let publishedRoster = "";
|
|
48241
|
+
let refreshing = null;
|
|
48242
|
+
let requested = false;
|
|
48243
|
+
const rosterFingerprint = () => JSON.stringify(call.participants.map(({ holder, muted, deafened }) => ({ holder, muted, deafened })).sort((a, b) => a.holder.localeCompare(b.holder)));
|
|
48244
|
+
const refresh = (heartbeat = false) => {
|
|
48245
|
+
if (!valid() || !heartbeat && publishedRoster === rosterFingerprint())
|
|
48246
|
+
return;
|
|
48247
|
+
requested = true;
|
|
48248
|
+
if (refreshing)
|
|
48249
|
+
return;
|
|
48250
|
+
clearTimeout(call.timer);
|
|
48251
|
+
refreshing = (async () => {
|
|
48252
|
+
while (requested && valid()) {
|
|
48253
|
+
requested = false;
|
|
48254
|
+
const fingerprint = rosterFingerprint();
|
|
48255
|
+
const ordered = call.participants.map((item) => item.holder).sort();
|
|
48256
|
+
if (ordered[0] === callEndpoint.holder) {
|
|
48257
|
+
const snapshot = await entry.mailbox.read();
|
|
48258
|
+
if (valid() && entry.descriptor?.callId === descriptor.callId) {
|
|
48259
|
+
await entry.mailbox.commit({ ...descriptor, participants: call.participants.length }, [], { expectedRevision: snapshot.revision });
|
|
48260
|
+
}
|
|
48261
|
+
}
|
|
48262
|
+
publishedRoster = fingerprint;
|
|
48263
|
+
}
|
|
48264
|
+
})().catch((error) => {
|
|
48265
|
+
if (valid() && error?.code !== "calls/conflict")
|
|
48266
|
+
fail(error);
|
|
48267
|
+
}).finally(() => {
|
|
48268
|
+
refreshing = null;
|
|
48269
|
+
if (valid())
|
|
48270
|
+
call.timer = setTimeout(() => refresh(true), 30000);
|
|
48271
|
+
});
|
|
48272
|
+
};
|
|
48273
|
+
call.refresh = refresh;
|
|
48274
|
+
refresh(true);
|
|
48275
|
+
return chatId;
|
|
48276
|
+
} catch (error) {
|
|
48277
|
+
const cleanup = active === call ? stopCall() : call ? disposeCall(call) : null;
|
|
48278
|
+
if (["calls/room-empty", "calls/discovery-conflict"].includes(error?.code))
|
|
48279
|
+
await cleanup;
|
|
48280
|
+
if (error?.code === "calls/room-empty" && call) {
|
|
48281
|
+
check();
|
|
48282
|
+
if (expectedCallId && (onlyExisting || call.descriptor.mode === "direct"))
|
|
48283
|
+
throw ended();
|
|
48284
|
+
await retireDiscovery(call);
|
|
48285
|
+
check();
|
|
48286
|
+
throw Object.assign(new Error("the call room is restarting"), { code: "calls/discovery-conflict" });
|
|
48287
|
+
}
|
|
48288
|
+
throw error;
|
|
48289
|
+
}
|
|
48290
|
+
}
|
|
48291
|
+
async function join(chatId, expectedCallId = null, onlyExisting = false) {
|
|
48292
|
+
if (active?.chatId === chatId) {
|
|
48293
|
+
if (expectedCallId && (onlyExisting || active.descriptor.mode === "direct") && active.callId !== expectedCallId)
|
|
48294
|
+
throw ended();
|
|
48295
|
+
return chatId;
|
|
48296
|
+
}
|
|
48297
|
+
const intent = ++joinIntent;
|
|
48298
|
+
const operation = { predecessor: active, chatId, candidate: null };
|
|
48299
|
+
const previous = joining;
|
|
48300
|
+
joining = operation;
|
|
48301
|
+
emit({ joiningChatId: chatId });
|
|
48302
|
+
if (previous?.candidate && previous.candidate !== active)
|
|
48303
|
+
disposeCall(previous.candidate);
|
|
48304
|
+
try {
|
|
48305
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
48306
|
+
try {
|
|
48307
|
+
return await joinAttempt(chatId, intent, expectedCallId, onlyExisting);
|
|
48308
|
+
} catch (error) {
|
|
48309
|
+
if (intent !== joinIntent || !identity || closed)
|
|
48310
|
+
throw cancelled2();
|
|
48311
|
+
if (error?.code !== "calls/discovery-conflict" || attempt === 2) {
|
|
48312
|
+
const target = operation.chatId;
|
|
48313
|
+
emit({ ...!active ? { phase: "error", chatId: target } : {}, error: callError(chatId, error) });
|
|
48314
|
+
throw error;
|
|
48315
|
+
}
|
|
48316
|
+
}
|
|
48317
|
+
}
|
|
48318
|
+
} finally {
|
|
48319
|
+
if (joining === operation) {
|
|
48320
|
+
joining = null;
|
|
48321
|
+
emit({ joiningChatId: null });
|
|
48322
|
+
}
|
|
48323
|
+
prune();
|
|
48324
|
+
}
|
|
48325
|
+
}
|
|
48326
|
+
function leave() {
|
|
48327
|
+
joinIntent += 1;
|
|
48328
|
+
const candidate = joining?.candidate;
|
|
48329
|
+
joining = null;
|
|
48330
|
+
emit({ joiningChatId: null });
|
|
48331
|
+
const pending = candidate && candidate !== active ? disposeCall(candidate) : null;
|
|
48332
|
+
const stopped = stopCall();
|
|
48333
|
+
return Promise.all([pending, stopped, ...[...retiring].map((call) => call.closing)]).then(() => {
|
|
48334
|
+
return;
|
|
48335
|
+
});
|
|
48336
|
+
}
|
|
48337
|
+
function cancelJoin() {
|
|
48338
|
+
const operation = joining;
|
|
48339
|
+
if (!operation)
|
|
48340
|
+
return Promise.resolve();
|
|
48341
|
+
joinIntent += 1;
|
|
48342
|
+
joining = null;
|
|
48343
|
+
emit({ joiningChatId: null });
|
|
48344
|
+
const pending = operation.candidate && operation.candidate !== active ? disposeCall(operation.candidate) : null;
|
|
48345
|
+
return Promise.all([pending, active && active === operation.predecessor ? null : stopCall()]).then(() => {
|
|
48346
|
+
return;
|
|
48347
|
+
});
|
|
48348
|
+
}
|
|
48349
|
+
function stopCall() {
|
|
48350
|
+
const call = active;
|
|
48351
|
+
if (!call) {
|
|
48352
|
+
emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
|
|
48353
|
+
return leaving;
|
|
48354
|
+
}
|
|
48355
|
+
active = null;
|
|
48356
|
+
leaving = disposeCall(call);
|
|
48357
|
+
if (identity && discoveries.get(call.chatId) === call.discovery)
|
|
48358
|
+
observeRoster(call.discovery);
|
|
48359
|
+
available();
|
|
48360
|
+
emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
|
|
48361
|
+
return leaving;
|
|
48362
|
+
}
|
|
48363
|
+
async function retireDiscovery(call) {
|
|
48364
|
+
const entry = call.discovery;
|
|
48365
|
+
const snapshot = await entry.mailbox.read();
|
|
48366
|
+
if (snapshot.head?.data?.callId !== call.callId && !(snapshot.head === null && entry.expiredCallId === call.callId))
|
|
48367
|
+
return;
|
|
48368
|
+
try {
|
|
48369
|
+
await entry.mailbox.commit(null, [], { expectedRevision: snapshot.revision });
|
|
48370
|
+
announce(call.chatId, call.callId, "ended");
|
|
48371
|
+
} catch (error) {
|
|
48372
|
+
if (error?.code !== "calls/conflict")
|
|
48373
|
+
throw error;
|
|
48374
|
+
}
|
|
48375
|
+
}
|
|
48376
|
+
function finishDiscovery(entry, descriptor) {
|
|
48377
|
+
if (entry.finishing.has(descriptor.callId))
|
|
48378
|
+
return;
|
|
48379
|
+
const finishing = retireDiscovery({ discovery: entry, callId: descriptor.callId, chatId: entry.epochState.manifest.chatId }).catch((error) => diag?.("call.close.error", { code: error?.code || "calls/discovery" })).finally(() => entry.finishing.delete(descriptor.callId));
|
|
48380
|
+
entry.finishing.set(descriptor.callId, finishing);
|
|
48381
|
+
}
|
|
48382
|
+
function disposeCall(call) {
|
|
48383
|
+
if (call.closing)
|
|
48384
|
+
return call.closing;
|
|
48385
|
+
call.leaving = true;
|
|
48386
|
+
retiring.add(call);
|
|
48387
|
+
clearTimeout(call.timer);
|
|
48388
|
+
clearTimeout(call.joinTimer);
|
|
48389
|
+
const stopping = call.media?.close();
|
|
48390
|
+
call.owner?.retire();
|
|
48391
|
+
const departing = call.room?.leave();
|
|
48392
|
+
const draining = (async () => {
|
|
48393
|
+
try {
|
|
48394
|
+
const [, departure] = await Promise.allSettled([Promise.resolve(stopping).then(() => call.owner?.release()), departing]);
|
|
48395
|
+
if (departure.status === "fulfilled" && departure.value?.ended && call.published && call.discovery.descriptor?.callId === call.callId) {
|
|
48396
|
+
await retireDiscovery(call);
|
|
48397
|
+
}
|
|
48398
|
+
} catch (error) {
|
|
48399
|
+
diag?.("call.close.error", { code: error?.code || "" });
|
|
48400
|
+
}
|
|
48401
|
+
})();
|
|
48402
|
+
let expiry;
|
|
48403
|
+
const expired = new Promise((resolve) => {
|
|
48404
|
+
expiry = setTimeout(() => {
|
|
48405
|
+
diag?.("call.close.error", { code: "calls/retirement-timeout" });
|
|
48406
|
+
resolve();
|
|
48407
|
+
}, CALL_OWNERSHIP_LEASE_MS);
|
|
48408
|
+
});
|
|
48409
|
+
call.closing = Promise.race([draining, expired]).finally(() => {
|
|
48410
|
+
clearTimeout(expiry);
|
|
48411
|
+
call.room?.close();
|
|
48412
|
+
call.protocol?.close();
|
|
48413
|
+
cleanBytes(call.member?.snapshot);
|
|
48414
|
+
if (call.endpoint !== leaseEndpoint)
|
|
48415
|
+
call.endpoint.close();
|
|
48416
|
+
retiring.delete(call);
|
|
48417
|
+
prune();
|
|
48418
|
+
});
|
|
48419
|
+
return call.closing;
|
|
48420
|
+
}
|
|
48421
|
+
function applyAudio(call) {
|
|
48422
|
+
const audio = { muted: state.muted || state.deafened, deafened: state.deafened };
|
|
48423
|
+
const applied = Promise.all([call.media.mute(audio.muted), call.media.deafen(audio.deafened)]);
|
|
48424
|
+
const result = Promise.all([applied, call.muteTail]).then(() => {
|
|
48425
|
+
if (!call.leaving)
|
|
48426
|
+
return call.room.setAudio(audio);
|
|
48427
|
+
});
|
|
48428
|
+
call.muteTail = result.catch(() => {});
|
|
48429
|
+
return result;
|
|
48430
|
+
}
|
|
48431
|
+
async function setAudio(patch) {
|
|
48432
|
+
emit(patch);
|
|
48433
|
+
const calls = new Set([active, joining?.candidate].filter((call) => call?.media && call.room && !call.leaving));
|
|
48434
|
+
await Promise.all([...calls].map((call) => applyAudio(call).catch((error) => {
|
|
48435
|
+
if (active === call)
|
|
48436
|
+
fail(error);
|
|
48437
|
+
else if (!call.leaving)
|
|
48438
|
+
call.error = error;
|
|
48439
|
+
throw error;
|
|
48440
|
+
})));
|
|
48441
|
+
}
|
|
48442
|
+
async function setPeerAudio(chatPK, patch) {
|
|
48443
|
+
const call = active;
|
|
48444
|
+
if (!identity || typeof chatPK !== "string" || !chatPK || chatPK === identity.chatPK)
|
|
48445
|
+
throw new Error("peer identity required");
|
|
48446
|
+
const token = generation;
|
|
48447
|
+
const peer = call?.participants.find((peer2) => peer2.chatPK === chatPK);
|
|
48448
|
+
const previous = state.peerAudio[chatPK];
|
|
48449
|
+
const value = { volume: 100, muted: false, ...previous, ...patch };
|
|
48450
|
+
emit({ peerAudio: { ...state.peerAudio, [chatPK]: value } });
|
|
48451
|
+
try {
|
|
48452
|
+
if (peer)
|
|
48453
|
+
await call.media.setPeerAudio(peer.holder, { volume: value.volume / 100, muted: value.muted });
|
|
48454
|
+
if (generation === token && state.peerAudio[chatPK] === value)
|
|
48455
|
+
confirmedPeerAudio.set(chatPK, value);
|
|
48456
|
+
} catch (error) {
|
|
48457
|
+
if (generation === token && state.peerAudio[chatPK] === value) {
|
|
48458
|
+
const peerAudio = { ...state.peerAudio };
|
|
48459
|
+
const confirmed = confirmedPeerAudio.get(chatPK);
|
|
48460
|
+
if (confirmed)
|
|
48461
|
+
peerAudio[chatPK] = confirmed;
|
|
48462
|
+
else
|
|
48463
|
+
delete peerAudio[chatPK];
|
|
48464
|
+
emit({ peerAudio });
|
|
48465
|
+
}
|
|
48466
|
+
throw error;
|
|
48467
|
+
}
|
|
48468
|
+
}
|
|
48469
|
+
async function setMicrophoneVolume(volume) {
|
|
48470
|
+
if (!Number.isFinite(volume) || volume < 1 || volume > 200)
|
|
48471
|
+
throw new Error("microphone volume must be between 1 and 200");
|
|
48472
|
+
const previous = state.microphoneVolume;
|
|
48473
|
+
const token = generation;
|
|
48474
|
+
emit({ microphoneVolume: volume });
|
|
48475
|
+
const calls = new Set([active, joining?.candidate].filter((call) => call?.media && !call.leaving));
|
|
48476
|
+
try {
|
|
48477
|
+
await Promise.all([...calls].map((call) => call.media.setMicrophoneVolume(volume / 100)));
|
|
48478
|
+
} catch (error) {
|
|
48479
|
+
if (generation === token && state.microphoneVolume === volume)
|
|
48480
|
+
emit({ microphoneVolume: previous });
|
|
48481
|
+
throw error;
|
|
48482
|
+
}
|
|
48483
|
+
}
|
|
48484
|
+
function mountLease(session, callEndpoint = endpoint) {
|
|
48485
|
+
lease?.close();
|
|
48486
|
+
if (leaseEndpoint && leaseEndpoint !== endpoint && leaseEndpoint !== callEndpoint)
|
|
48487
|
+
leaseEndpoint.close();
|
|
48488
|
+
leaseEndpoint = callEndpoint;
|
|
48489
|
+
const ownsLease = () => identity === session && leaseEndpoint === callEndpoint;
|
|
48490
|
+
lease = openAccountCallLease({
|
|
48491
|
+
cloud,
|
|
48492
|
+
capability: session.callIdentity,
|
|
48493
|
+
endpoint: callEndpoint,
|
|
48494
|
+
clock: mediaPort.clock,
|
|
48495
|
+
onRecord(record, descriptor) {
|
|
48496
|
+
if (!ownsLease())
|
|
48497
|
+
return;
|
|
48498
|
+
const holder = (record.pending || record.active)?.holder;
|
|
48499
|
+
const elsewhere = descriptor && holder !== callEndpoint.holder ? descriptor : null;
|
|
48500
|
+
if (elsewhere && state.elsewhere?.chatId === elsewhere.chatId && state.elsewhere.callId === elsewhere.callId)
|
|
48501
|
+
return;
|
|
48502
|
+
emit({ elsewhere });
|
|
48503
|
+
},
|
|
48504
|
+
onGrant: (value) => ownsLease() && active?.endpoint === callEndpoint ? active.media?.grant(value) : undefined,
|
|
48505
|
+
onLost: () => {
|
|
48506
|
+
if (ownsLease())
|
|
48507
|
+
leave();
|
|
48508
|
+
},
|
|
48509
|
+
onError(error) {
|
|
48510
|
+
if (ownsLease() && active)
|
|
48511
|
+
fail(error);
|
|
48512
|
+
}
|
|
48513
|
+
});
|
|
48514
|
+
}
|
|
48515
|
+
function setSources(session) {
|
|
48516
|
+
if (identity === session)
|
|
48517
|
+
return drainingSources;
|
|
48518
|
+
const token = ++generation;
|
|
48519
|
+
const previousLease = lease;
|
|
48520
|
+
const previousEndpoint = endpoint;
|
|
48521
|
+
const previousLeaseEndpoint = leaseEndpoint;
|
|
48522
|
+
const previousDiscoveries = [...discoveries.values()];
|
|
48523
|
+
let finishDrain, rejectDrain;
|
|
48524
|
+
const drained = new Promise((resolve, reject) => {
|
|
48525
|
+
finishDrain = resolve;
|
|
48526
|
+
rejectDrain = reject;
|
|
48527
|
+
});
|
|
48528
|
+
const retirement = Promise.all([drainingSources, drained]).then(() => {
|
|
48529
|
+
return;
|
|
48530
|
+
});
|
|
48531
|
+
drainingSources = retirement;
|
|
48532
|
+
lease = null;
|
|
48533
|
+
leaseEndpoint = null;
|
|
48534
|
+
endpoint = null;
|
|
48535
|
+
discoveries.clear();
|
|
48536
|
+
discoveryReads.clear();
|
|
48537
|
+
identity = session;
|
|
48538
|
+
const stopped = leave();
|
|
48539
|
+
Promise.resolve(stopped).finally(() => {
|
|
48540
|
+
previousLease?.close();
|
|
48541
|
+
previousLeaseEndpoint?.close();
|
|
48542
|
+
previousEndpoint?.close();
|
|
48543
|
+
for (const entry of previousDiscoveries)
|
|
48544
|
+
closeDiscovery(entry);
|
|
48545
|
+
}).then(finishDrain, rejectDrain);
|
|
48546
|
+
if (generation !== token || identity !== session)
|
|
48547
|
+
return retirement;
|
|
48548
|
+
confirmedPeerAudio.clear();
|
|
48549
|
+
emit(initial());
|
|
48550
|
+
available();
|
|
48551
|
+
if (generation !== token || identity !== session)
|
|
48552
|
+
return retirement;
|
|
48553
|
+
if (!session || closed || !mediaPort || !mls || !cloud.calls)
|
|
48554
|
+
return retirement;
|
|
48555
|
+
endpoint = createCallEndpoint();
|
|
48556
|
+
mountLease(session);
|
|
48557
|
+
if (online)
|
|
48558
|
+
refreshObserved();
|
|
48559
|
+
return retirement;
|
|
48560
|
+
}
|
|
48561
|
+
function refreshObserved() {
|
|
48562
|
+
for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
|
|
48563
|
+
readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
|
|
48564
|
+
}
|
|
48565
|
+
}
|
|
48566
|
+
const unsubscribe = chat.subscribe(() => {
|
|
48567
|
+
if (!identity)
|
|
48568
|
+
return;
|
|
48569
|
+
for (const [chatId, entry] of discoveries) {
|
|
48570
|
+
const epoch = chat.getSnapshot().getOwnerChat(chatId)?.epochState?.manifest.epochId;
|
|
48571
|
+
if (epoch === entry.epochState.manifest.epochId)
|
|
48572
|
+
continue;
|
|
48573
|
+
if (active?.chatId === chatId)
|
|
48574
|
+
leave();
|
|
48575
|
+
else if (joining?.chatId === chatId)
|
|
48576
|
+
cancelJoin();
|
|
48577
|
+
closeDiscovery(entry);
|
|
48578
|
+
discoveries.delete(chatId);
|
|
48579
|
+
available();
|
|
48580
|
+
if (online && [...observers.values()].some((item) => item.chatId === chatId))
|
|
48581
|
+
readDiscovery(chatId).catch(() => {});
|
|
48582
|
+
}
|
|
48583
|
+
if (online)
|
|
48584
|
+
for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
|
|
48585
|
+
if (!discoveries.has(chatId) && !discoveryReads.has(chatId) && chat.getSnapshot().getOwnerChat(chatId)?.epochState) {
|
|
48586
|
+
readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
|
|
48587
|
+
}
|
|
48588
|
+
}
|
|
48589
|
+
});
|
|
48590
|
+
return Object.freeze({
|
|
48591
|
+
getSnapshot: () => state,
|
|
48592
|
+
subscribe(listener) {
|
|
48593
|
+
listeners.add(listener);
|
|
48594
|
+
return () => listeners.delete(listener);
|
|
48595
|
+
},
|
|
48596
|
+
setSources,
|
|
48597
|
+
observe,
|
|
48598
|
+
join,
|
|
48599
|
+
leave,
|
|
48600
|
+
cancelJoin,
|
|
48601
|
+
setMicrophoneVolume,
|
|
48602
|
+
async resolveActiveCall(chatId) {
|
|
48603
|
+
const entry = await readDiscovery(chatId);
|
|
48604
|
+
return entry.descriptor?.callId || null;
|
|
48605
|
+
},
|
|
48606
|
+
joinFromMessage(chatId, callId) {
|
|
48607
|
+
if (!/^[a-f0-9]{64}$/u.test(callId))
|
|
48608
|
+
return Promise.reject(ended());
|
|
48609
|
+
return join(chatId, callId);
|
|
48610
|
+
},
|
|
48611
|
+
joinExisting(chatId, callId) {
|
|
48612
|
+
if (!/^[a-f0-9]{64}$/u.test(callId))
|
|
48613
|
+
return Promise.reject(ended());
|
|
48614
|
+
return join(chatId, callId, true);
|
|
48615
|
+
},
|
|
48616
|
+
setMuted(value) {
|
|
48617
|
+
return setAudio({ muted: value === true });
|
|
48618
|
+
},
|
|
48619
|
+
setDeafened(value) {
|
|
48620
|
+
return setAudio({ deafened: value === true });
|
|
48621
|
+
},
|
|
48622
|
+
setPeerVolume(chatPK, volume) {
|
|
48623
|
+
if (!Number.isFinite(volume) || volume < 1 || volume > 200)
|
|
48624
|
+
return Promise.reject(new Error("peer volume must be between 1 and 200"));
|
|
48625
|
+
return setPeerAudio(chatPK, { volume });
|
|
48626
|
+
},
|
|
48627
|
+
setPeerMuted(chatPK, muted) {
|
|
48628
|
+
return setPeerAudio(chatPK, { muted: muted === true });
|
|
48629
|
+
},
|
|
48630
|
+
setForeground(value) {
|
|
48631
|
+
foreground = value === true;
|
|
48632
|
+
if (!foreground && joining)
|
|
48633
|
+
cancelJoin();
|
|
48634
|
+
},
|
|
48635
|
+
setOnline(value) {
|
|
48636
|
+
const reconnecting = !online && value === true;
|
|
48637
|
+
online = value === true;
|
|
48638
|
+
if (reconnecting && identity && !closed)
|
|
48639
|
+
refreshObserved();
|
|
48640
|
+
},
|
|
48641
|
+
close() {
|
|
48642
|
+
if (closed)
|
|
48643
|
+
return closingSession;
|
|
48644
|
+
let finish, reject;
|
|
48645
|
+
closingSession = new Promise((resolve, fail2) => {
|
|
48646
|
+
finish = resolve;
|
|
48647
|
+
reject = fail2;
|
|
48648
|
+
});
|
|
48649
|
+
closed = true;
|
|
48650
|
+
const stopped = setSources(null);
|
|
48651
|
+
unsubscribe();
|
|
48652
|
+
listeners.clear();
|
|
48653
|
+
Promise.resolve(stopped).finally(() => Promise.all([mls?.close?.(), mediaPort?.close?.()])).then(finish, reject);
|
|
48654
|
+
return closingSession;
|
|
48655
|
+
}
|
|
48656
|
+
});
|
|
48657
|
+
}
|
|
48658
|
+
|
|
45657
48659
|
// ../../node_modules/.bun/@zxcvbn-ts+core@4.2.0/node_modules/@zxcvbn-ts/core/dist/utils/helper.mjs
|
|
45658
48660
|
var extend = (listToExtend, list) => listToExtend.push.apply(listToExtend, list);
|
|
45659
48661
|
var sorted = (matches) => matches.sort((m1, m2) => m1.i - m2.i || m1.j - m2.j);
|
|
@@ -49657,11 +52659,11 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
|
|
|
49657
52659
|
return null;
|
|
49658
52660
|
}
|
|
49659
52661
|
}
|
|
49660
|
-
async function writeCachedAvatar(uid, version,
|
|
49661
|
-
if (!uid || version == null || !
|
|
52662
|
+
async function writeCachedAvatar(uid, version, bytes2) {
|
|
52663
|
+
if (!uid || version == null || !bytes2 || typeof avatarCache?.write !== "function")
|
|
49662
52664
|
return null;
|
|
49663
52665
|
try {
|
|
49664
|
-
const cached = await avatarCache.write(uid, { version, bytes });
|
|
52666
|
+
const cached = await avatarCache.write(uid, { version, bytes: bytes2 });
|
|
49665
52667
|
return typeof cached === "string" && cached ? cached : typeof cached?.url === "string" && cached.url ? cached.url : typeof cached?.source === "string" && cached.source ? cached.source : null;
|
|
49666
52668
|
} catch {
|
|
49667
52669
|
return null;
|
|
@@ -49675,8 +52677,8 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
|
|
|
49675
52677
|
}
|
|
49676
52678
|
if (avatarVersion != null && typeof cloud.peer.avatar.read === "function") {
|
|
49677
52679
|
try {
|
|
49678
|
-
const
|
|
49679
|
-
const cachedSource = await writeCachedAvatar(uid, avatarVersion,
|
|
52680
|
+
const bytes2 = await cloud.peer.avatar.read(uid, { version: avatarVersion });
|
|
52681
|
+
const cachedSource = await writeCachedAvatar(uid, avatarVersion, bytes2);
|
|
49680
52682
|
if (cachedSource) {
|
|
49681
52683
|
return cachedSource;
|
|
49682
52684
|
}
|
|
@@ -55084,12 +58086,12 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
|
|
|
55084
58086
|
reason,
|
|
55085
58087
|
active: sdkActivityCountRef.current
|
|
55086
58088
|
});
|
|
55087
|
-
let
|
|
58089
|
+
let ended2 = false;
|
|
55088
58090
|
return () => {
|
|
55089
|
-
if (
|
|
58091
|
+
if (ended2) {
|
|
55090
58092
|
return;
|
|
55091
58093
|
}
|
|
55092
|
-
|
|
58094
|
+
ended2 = true;
|
|
55093
58095
|
sdkActivityCountRef.current = Math.max(0, sdkActivityCountRef.current - 1);
|
|
55094
58096
|
sdkActivityQuietUntilRef.current = Math.max(sdkActivityQuietUntilRef.current, Date.now() + SDK_BACKGROUND_QUIET_MS);
|
|
55095
58097
|
markDiag(diag, "wallet.sdkActivity.done", {
|
|
@@ -57304,8 +60306,10 @@ function openAccount(options = {}) {
|
|
|
57304
60306
|
getPeerProfile: getChatPeerProfile,
|
|
57305
60307
|
refreshPeerProfile: refreshChatPeerProfile,
|
|
57306
60308
|
resolvePeerProfile: resolveChatPeerProfile,
|
|
57307
|
-
maintenance: messageMaintenance
|
|
60309
|
+
maintenance: messageMaintenance,
|
|
60310
|
+
resolveActiveCall: (chatId) => calls.resolveActiveCall(chatId)
|
|
57308
60311
|
}, chatSources());
|
|
60312
|
+
const calls = createCallSession({ cloud, chat, ...options.calls, diag });
|
|
57309
60313
|
const wallet = openWallet({
|
|
57310
60314
|
...options.wallet || {},
|
|
57311
60315
|
network: state.network,
|
|
@@ -57378,6 +60382,8 @@ function openAccount(options = {}) {
|
|
|
57378
60382
|
syncWallet();
|
|
57379
60383
|
syncPresence();
|
|
57380
60384
|
syncPeers();
|
|
60385
|
+
calls.setOnline(state.online);
|
|
60386
|
+
calls.setSources(state.lockState === "unlocked" ? state.session : null);
|
|
57381
60387
|
}
|
|
57382
60388
|
function syncPresence() {
|
|
57383
60389
|
const session = state.session;
|
|
@@ -57413,7 +60419,7 @@ function openAccount(options = {}) {
|
|
|
57413
60419
|
connected: !closed && state.internetAvailable !== false && state.cloudAvailable !== false && state.user.authSessionReady && state.user.authSessionActive && !state.user.authSessionError && state.user.profileReady && !state.user.profileFromCache && !state.user.profileError
|
|
57414
60420
|
}),
|
|
57415
60421
|
preview: (value) => user.previewAvatar(value),
|
|
57416
|
-
write: (uid,
|
|
60422
|
+
write: (uid, bytes2) => cloud.user.profile.avatar.set(uid, bytes2),
|
|
57417
60423
|
commit: (change) => user.confirmAvatar(change),
|
|
57418
60424
|
onError: (error) => diag?.("account.avatar.update.error", { code: error?.code || "" })
|
|
57419
60425
|
});
|
|
@@ -57427,11 +60433,11 @@ function openAccount(options = {}) {
|
|
|
57427
60433
|
await cloud.user.username.get(username);
|
|
57428
60434
|
return { username };
|
|
57429
60435
|
},
|
|
57430
|
-
async setAvatar(
|
|
60436
|
+
async setAvatar(bytes2) {
|
|
57431
60437
|
profileUid();
|
|
57432
|
-
if (!
|
|
60438
|
+
if (!bytes2)
|
|
57433
60439
|
throw new Error("avatar data required");
|
|
57434
|
-
return avatarUpdate.set(sanitizeAvatar(
|
|
60440
|
+
return avatarUpdate.set(sanitizeAvatar(bytes2));
|
|
57435
60441
|
},
|
|
57436
60442
|
async clearAvatar() {
|
|
57437
60443
|
profileUid();
|
|
@@ -57562,13 +60568,32 @@ function openAccount(options = {}) {
|
|
|
57562
60568
|
return;
|
|
57563
60569
|
foreground = value === true;
|
|
57564
60570
|
syncPresence();
|
|
60571
|
+
calls.setForeground(foreground);
|
|
57565
60572
|
}
|
|
57566
60573
|
function closeSession({ notify: notify2 = true } = {}) {
|
|
57567
|
-
avatarUpdate.reset();
|
|
57568
|
-
attempt += 1;
|
|
57569
|
-
chat.setInboxEnabled(false);
|
|
57570
60574
|
const session = state.session;
|
|
60575
|
+
attempt += 1;
|
|
57571
60576
|
chatSession = null;
|
|
60577
|
+
const patch = {
|
|
60578
|
+
session: null,
|
|
60579
|
+
wallet: null,
|
|
60580
|
+
walletError: null,
|
|
60581
|
+
lockState: "locked",
|
|
60582
|
+
online: false,
|
|
60583
|
+
onlineError: null,
|
|
60584
|
+
agreementSession: null
|
|
60585
|
+
};
|
|
60586
|
+
state = { ...state, ...patch };
|
|
60587
|
+
const callRetirement = calls.setSources(null);
|
|
60588
|
+
if (callRetirement) {
|
|
60589
|
+
const work = Promise.resolve(callRetirement).catch((error) => {
|
|
60590
|
+
diag?.("account.calls.close.error", { code: error?.code || "" });
|
|
60591
|
+
});
|
|
60592
|
+
sessionCloseWork.add(work);
|
|
60593
|
+
work.finally(() => sessionCloseWork.delete(work));
|
|
60594
|
+
}
|
|
60595
|
+
avatarUpdate.reset();
|
|
60596
|
+
chat.setInboxEnabled(false);
|
|
57572
60597
|
closeAccountSession(session);
|
|
57573
60598
|
if (session && typeof options.onSessionClosed === "function") {
|
|
57574
60599
|
let result;
|
|
@@ -57588,19 +60613,9 @@ function openAccount(options = {}) {
|
|
|
57588
60613
|
}
|
|
57589
60614
|
state.user.lockSettings?.();
|
|
57590
60615
|
peers.setSources({});
|
|
57591
|
-
const patch = {
|
|
57592
|
-
session: null,
|
|
57593
|
-
wallet: null,
|
|
57594
|
-
walletError: null,
|
|
57595
|
-
lockState: "locked",
|
|
57596
|
-
online: false,
|
|
57597
|
-
onlineError: null,
|
|
57598
|
-
agreementSession: null
|
|
57599
|
-
};
|
|
57600
60616
|
if (notify2) {
|
|
57601
|
-
emitDomains(
|
|
60617
|
+
emitDomains({});
|
|
57602
60618
|
} else {
|
|
57603
|
-
state = { ...state, ...patch };
|
|
57604
60619
|
syncDomains();
|
|
57605
60620
|
}
|
|
57606
60621
|
return session;
|
|
@@ -58162,12 +61177,14 @@ function openAccount(options = {}) {
|
|
|
58162
61177
|
stopPeerAdmissionProjection();
|
|
58163
61178
|
stopPresence();
|
|
58164
61179
|
presence.close();
|
|
61180
|
+
const callClosing = calls.close();
|
|
58165
61181
|
clearMissingPeers();
|
|
58166
61182
|
wallet.close();
|
|
58167
61183
|
chat.close();
|
|
58168
61184
|
peers.close();
|
|
58169
61185
|
listeners.clear();
|
|
58170
61186
|
operationBarrier.finishClose();
|
|
61187
|
+
return callClosing;
|
|
58171
61188
|
});
|
|
58172
61189
|
return closePromise;
|
|
58173
61190
|
}
|
|
@@ -58183,6 +61200,7 @@ function openAccount(options = {}) {
|
|
|
58183
61200
|
messageMaintenance,
|
|
58184
61201
|
peers,
|
|
58185
61202
|
presence,
|
|
61203
|
+
calls,
|
|
58186
61204
|
closeBarrier: Object.freeze({
|
|
58187
61205
|
acquire: operationBarrier.acquireClose,
|
|
58188
61206
|
release: operationBarrier.releaseClose,
|