@glyphteck/veyl 0.72.0 → 0.74.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 +3617 -257
- package/dist/accountprofiles.js +3 -0
- package/dist/auth.js +3 -1
- package/dist/cli.js +4883 -792
- package/dist/index.js +4882 -791
- 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 +160 -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;
|
|
@@ -16827,6 +16818,341 @@ var WALLET_PENDING_TRANSFER_STALE_RETRY_MS = 2 * MINUTE_MS;
|
|
|
16827
16818
|
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;
|
|
16821
|
+
var BITCOIN_FEES_MAX_AGE_MS = 5 * 60000;
|
|
16822
|
+
|
|
16823
|
+
// ../../core/calls/wire.js
|
|
16824
|
+
var CALL_REQUEST_MAX_BYTES = 192 * 1024;
|
|
16825
|
+
var CALL_HEAD_MAX_BYTES = 64 * 1024;
|
|
16826
|
+
var CALL_HEAD_PROOF_MAX_BYTES = 2048;
|
|
16827
|
+
var CALL_EVENT_MAX_BYTES = 48 * 1024;
|
|
16828
|
+
var CALL_LOG_MAX_BYTES = 512 * 1024;
|
|
16829
|
+
var CALL_LOG_MAX_EVENTS = 128;
|
|
16830
|
+
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;
|
|
16831
|
+
var CALL_SCOPE_KINDS = ["ownership", "discovery", "signaling"];
|
|
16832
|
+
var SCOPE_SOCKETS = Object.freeze({ ownership: 8, discovery: CHAT_MAX_MEMBERS * 4, signaling: 32 });
|
|
16833
|
+
var SCOPE_LIMITS = Object.freeze(Object.fromEntries(Object.entries(SCOPE_SOCKETS).map(([kind, sockets]) => [
|
|
16834
|
+
kind,
|
|
16835
|
+
Object.freeze({ sockets, readsPerMinute: (kind === "signaling" ? SCOPE_SOCKETS.discovery : sockets) * 4, writesPerMinute: 600 })
|
|
16836
|
+
])));
|
|
16837
|
+
var HEX_KEY = /^[a-f0-9]{64}$/u;
|
|
16838
|
+
function callWireError(code = "invalid-request") {
|
|
16839
|
+
const error = new Error(`call ${code}`);
|
|
16840
|
+
error.code = `calls/${code}`;
|
|
16841
|
+
return error;
|
|
16842
|
+
}
|
|
16843
|
+
function encodeCallBytes(value) {
|
|
16844
|
+
if (!(value instanceof Uint8Array))
|
|
16845
|
+
throw callWireError();
|
|
16846
|
+
let text = "";
|
|
16847
|
+
for (const byte of value)
|
|
16848
|
+
text += String.fromCharCode(byte);
|
|
16849
|
+
return btoa(text);
|
|
16850
|
+
}
|
|
16851
|
+
function callHeadDigest(value) {
|
|
16852
|
+
if (!(value instanceof Uint8Array) || !value.length || value.length > CALL_HEAD_MAX_BYTES)
|
|
16853
|
+
throw callWireError();
|
|
16854
|
+
return toHex(sha256(value));
|
|
16855
|
+
}
|
|
16856
|
+
function decodeCallBytes(value, maximum = CALL_HEAD_MAX_BYTES) {
|
|
16857
|
+
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))
|
|
16858
|
+
throw callWireError();
|
|
16859
|
+
const bytes = Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
16860
|
+
if (bytes.length > maximum || encodeCallBytes(bytes) !== value)
|
|
16861
|
+
throw callWireError();
|
|
16862
|
+
return bytes;
|
|
16863
|
+
}
|
|
16864
|
+
function callScope(realm, kind, publicKey) {
|
|
16865
|
+
if (!["prod", "dev"].includes(realm) || !CALL_SCOPE_KINDS.includes(kind) || !HEX_KEY.test(publicKey))
|
|
16866
|
+
throw callWireError();
|
|
16867
|
+
return toHex(sha256(canonicalBytes(["veyl.call.scope.v1", realm, kind, publicKey])));
|
|
16868
|
+
}
|
|
16869
|
+
function decodeCallRecord(record) {
|
|
16870
|
+
return {
|
|
16871
|
+
...record,
|
|
16872
|
+
active: record.active ? { ...record.active, payload: decodeCallBytes(record.active.payload, 4096) } : null,
|
|
16873
|
+
pending: record.pending ? { ...record.pending, payload: decodeCallBytes(record.pending.payload, 4096) } : null
|
|
16874
|
+
};
|
|
16875
|
+
}
|
|
16876
|
+
|
|
16877
|
+
// ../../core/calls/identity.js
|
|
16878
|
+
var REALMS = new Set(["prod", "dev"]);
|
|
16879
|
+
var KINDS = new Set(["ownership", "discovery", "signaling"]);
|
|
16880
|
+
var MAX_SEALED_BYTES = 64 * 1024;
|
|
16881
|
+
function createCallCapability(seed, realm, kind, parts = []) {
|
|
16882
|
+
if (!REALMS.has(realm) || !KINDS.has(kind))
|
|
16883
|
+
throw new Error("invalid call capability");
|
|
16884
|
+
const signing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.signing.v1", [realm, kind, ...parts]);
|
|
16885
|
+
const sealing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.sealing.v1", [realm, kind, ...parts]);
|
|
16886
|
+
const publicKey = toHex(ed25519.getPublicKey(signing));
|
|
16887
|
+
const scope = callScope(realm, kind, publicKey);
|
|
16888
|
+
const aad = canonicalBytes(["veyl.call.sealed.v1", realm, kind, scope]);
|
|
16889
|
+
let owners = 0;
|
|
16890
|
+
function retain() {
|
|
16891
|
+
owners += 1;
|
|
16892
|
+
let closed = false;
|
|
16893
|
+
function current() {
|
|
16894
|
+
if (closed)
|
|
16895
|
+
throw new Error("call capability closed");
|
|
16896
|
+
}
|
|
16897
|
+
return Object.freeze({
|
|
16898
|
+
realm,
|
|
16899
|
+
kind,
|
|
16900
|
+
scope,
|
|
16901
|
+
publicKey,
|
|
16902
|
+
sign(bytes) {
|
|
16903
|
+
current();
|
|
16904
|
+
return toHex(ed25519.sign(toBytes(bytes), signing));
|
|
16905
|
+
},
|
|
16906
|
+
async seal(value) {
|
|
16907
|
+
current();
|
|
16908
|
+
const plain = canonicalBytes(value, "call payload");
|
|
16909
|
+
if (plain.length + 28 > MAX_SEALED_BYTES)
|
|
16910
|
+
throw new Error("call payload too large");
|
|
16911
|
+
try {
|
|
16912
|
+
const { iv, ct } = await sealAes(sealing, plain, aad);
|
|
16913
|
+
current();
|
|
16914
|
+
const result = new Uint8Array(iv.length + ct.length);
|
|
16915
|
+
result.set(iv);
|
|
16916
|
+
result.set(ct, iv.length);
|
|
16917
|
+
return result;
|
|
16918
|
+
} finally {
|
|
16919
|
+
cleanBytes(plain);
|
|
16920
|
+
}
|
|
16921
|
+
},
|
|
16922
|
+
async open(value) {
|
|
16923
|
+
current();
|
|
16924
|
+
const bytes = toBytes(value);
|
|
16925
|
+
if (bytes.length < 28 || bytes.length > MAX_SEALED_BYTES)
|
|
16926
|
+
throw new Error("invalid call payload");
|
|
16927
|
+
const plain = await openAes(sealing, bytes.subarray(0, 12), bytes.subarray(12), aad);
|
|
16928
|
+
try {
|
|
16929
|
+
current();
|
|
16930
|
+
return JSON.parse(decoder.decode(plain));
|
|
16931
|
+
} finally {
|
|
16932
|
+
cleanBytes(plain);
|
|
16933
|
+
}
|
|
16934
|
+
},
|
|
16935
|
+
retain() {
|
|
16936
|
+
current();
|
|
16937
|
+
return retain();
|
|
16938
|
+
},
|
|
16939
|
+
close() {
|
|
16940
|
+
if (closed)
|
|
16941
|
+
return;
|
|
16942
|
+
closed = true;
|
|
16943
|
+
if (--owners === 0)
|
|
16944
|
+
cleanBytes(signing, sealing);
|
|
16945
|
+
}
|
|
16946
|
+
});
|
|
16947
|
+
}
|
|
16948
|
+
return retain();
|
|
16949
|
+
}
|
|
16950
|
+
function createCallIdentity(masterSeed, realm) {
|
|
16951
|
+
const root = deriveKey(toBytes32(masterSeed, "vault root"), "veyl.call.account.v1", [realm]);
|
|
16952
|
+
try {
|
|
16953
|
+
return createCallCapability(root, realm, "ownership");
|
|
16954
|
+
} finally {
|
|
16955
|
+
cleanBytes(root);
|
|
16956
|
+
}
|
|
16957
|
+
}
|
|
16958
|
+
function createCallEndpoint() {
|
|
16959
|
+
const secret = randomBytes3(32);
|
|
16960
|
+
const publicKey = toHex(ed25519.getPublicKey(secret));
|
|
16961
|
+
let closed = false;
|
|
16962
|
+
return Object.freeze({
|
|
16963
|
+
publicKey,
|
|
16964
|
+
holder: toHex(randomBytes3(16)),
|
|
16965
|
+
sign(bytes) {
|
|
16966
|
+
if (closed)
|
|
16967
|
+
throw new Error("call endpoint closed");
|
|
16968
|
+
return toHex(ed25519.sign(toBytes(bytes), secret));
|
|
16969
|
+
},
|
|
16970
|
+
close() {
|
|
16971
|
+
closed = true;
|
|
16972
|
+
cleanBytes(secret);
|
|
16973
|
+
}
|
|
16974
|
+
});
|
|
16975
|
+
}
|
|
16976
|
+
function createCallDiscovery(epochState, realm) {
|
|
16977
|
+
const manifest = epochState?.manifest;
|
|
16978
|
+
if (!manifest?.chatId || !manifest?.epochId || !epochState?.epochSecret)
|
|
16979
|
+
throw new Error("current chat epoch required");
|
|
16980
|
+
return createCallCapability(toBytes32(epochState.epochSecret), realm, "discovery", [manifest.chatId, manifest.epochId]);
|
|
16981
|
+
}
|
|
16982
|
+
|
|
16983
|
+
// ../../core/utils/text.js
|
|
16984
|
+
function cleanText(value) {
|
|
16985
|
+
return typeof value === "string" ? value.trim() : "";
|
|
16986
|
+
}
|
|
16987
|
+
function lowerText(value) {
|
|
16988
|
+
return cleanText(value).toLowerCase();
|
|
16989
|
+
}
|
|
16990
|
+
function sameText(left, right) {
|
|
16991
|
+
return lowerText(left) === lowerText(right);
|
|
16992
|
+
}
|
|
16993
|
+
|
|
16994
|
+
// ../../core/utils/time.js
|
|
16995
|
+
function timestampMs(value, fallback = null, options = {}) {
|
|
16996
|
+
let ms = null;
|
|
16997
|
+
if (typeof value?.toMillis === "function") {
|
|
16998
|
+
ms = value.toMillis();
|
|
16999
|
+
} else if (value instanceof Date) {
|
|
17000
|
+
ms = value.getTime();
|
|
17001
|
+
} else if (typeof value?.seconds === "number") {
|
|
17002
|
+
ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
|
|
17003
|
+
} else if (typeof value?._seconds === "number") {
|
|
17004
|
+
ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
|
|
17005
|
+
} else if (Number.isFinite(value)) {
|
|
17006
|
+
ms = value;
|
|
17007
|
+
} else if (options.parseString && typeof value === "string") {
|
|
17008
|
+
const numberMs = Number(value);
|
|
17009
|
+
ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
|
|
17010
|
+
}
|
|
17011
|
+
if (!Number.isFinite(ms) || options.positive && ms <= 0) {
|
|
17012
|
+
return fallback;
|
|
17013
|
+
}
|
|
17014
|
+
return ms;
|
|
17015
|
+
}
|
|
17016
|
+
function timestampKey(value) {
|
|
17017
|
+
if (value == null) {
|
|
17018
|
+
return null;
|
|
17019
|
+
}
|
|
17020
|
+
return timestampMs(value, null) ?? String(value);
|
|
17021
|
+
}
|
|
17022
|
+
function makeTimestamp(ms) {
|
|
17023
|
+
return {
|
|
17024
|
+
toMillis() {
|
|
17025
|
+
return ms;
|
|
17026
|
+
},
|
|
17027
|
+
toDate() {
|
|
17028
|
+
return new Date(ms);
|
|
17029
|
+
}
|
|
17030
|
+
};
|
|
17031
|
+
}
|
|
17032
|
+
function twoDigits(value) {
|
|
17033
|
+
return String(value).padStart(2, "0");
|
|
17034
|
+
}
|
|
17035
|
+
function dayKey(date) {
|
|
17036
|
+
return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
|
|
17037
|
+
}
|
|
17038
|
+
function localDayKey(value) {
|
|
17039
|
+
const ms = timestampMs(value, null, { parseString: true });
|
|
17040
|
+
if (!Number.isFinite(ms))
|
|
17041
|
+
return "";
|
|
17042
|
+
return dayKey(new Date(ms));
|
|
17043
|
+
}
|
|
17044
|
+
function hourKey(dateOrHour) {
|
|
17045
|
+
const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
|
|
17046
|
+
return twoDigits(hour);
|
|
17047
|
+
}
|
|
17048
|
+
function dayHourKey(date) {
|
|
17049
|
+
return `${dayKey(date)}-${hourKey(date)}`;
|
|
17050
|
+
}
|
|
17051
|
+
var MINUTE_MS2 = 60000;
|
|
17052
|
+
var HOUR_MS2 = 60 * MINUTE_MS2;
|
|
17053
|
+
function nextLocalDayStartMs(ms) {
|
|
17054
|
+
const date = new Date(ms);
|
|
17055
|
+
date.setDate(date.getDate() + 1);
|
|
17056
|
+
date.setHours(0, 0, 0, 0);
|
|
17057
|
+
return date.getTime();
|
|
17058
|
+
}
|
|
17059
|
+
function nextRowDateTimeRefreshMs(value, now = Date.now()) {
|
|
17060
|
+
const ms = timestampMs(value, null, { parseString: true });
|
|
17061
|
+
const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
|
|
17062
|
+
if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
|
|
17063
|
+
return null;
|
|
17064
|
+
const age = nowMs2 - ms;
|
|
17065
|
+
if (age < -MINUTE_MS2)
|
|
17066
|
+
return ms - MINUTE_MS2;
|
|
17067
|
+
if (age < MINUTE_MS2)
|
|
17068
|
+
return ms + MINUTE_MS2;
|
|
17069
|
+
if (age < HOUR_MS2)
|
|
17070
|
+
return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
|
|
17071
|
+
if (localDayKey(ms) === localDayKey(nowMs2))
|
|
17072
|
+
return nextLocalDayStartMs(nowMs2);
|
|
17073
|
+
return null;
|
|
17074
|
+
}
|
|
17075
|
+
|
|
17076
|
+
// ../../core/chat/state.js
|
|
17077
|
+
function makeCid() {
|
|
17078
|
+
return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
|
|
17079
|
+
}
|
|
17080
|
+
function getMessageKey(message) {
|
|
17081
|
+
return message?.cid || message?.id || null;
|
|
17082
|
+
}
|
|
17083
|
+
function getCidMs(cid) {
|
|
17084
|
+
if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
|
|
17085
|
+
return null;
|
|
17086
|
+
}
|
|
17087
|
+
const base = cid.slice(0, -6);
|
|
17088
|
+
const ms = Number.parseInt(base, 36);
|
|
17089
|
+
return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
|
|
17090
|
+
}
|
|
17091
|
+
function getMessageOrderMs(message) {
|
|
17092
|
+
return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
|
|
17093
|
+
}
|
|
17094
|
+
function sortMessages(messages) {
|
|
17095
|
+
return [...messages].sort((a, b) => {
|
|
17096
|
+
const aMs = getMessageOrderMs(a);
|
|
17097
|
+
const bMs = getMessageOrderMs(b);
|
|
17098
|
+
if (aMs !== bMs) {
|
|
17099
|
+
return aMs - bMs;
|
|
17100
|
+
}
|
|
17101
|
+
return String(a?.id || "").localeCompare(String(b?.id || ""));
|
|
17102
|
+
});
|
|
17103
|
+
}
|
|
17104
|
+
function mergeMessages(...groups) {
|
|
17105
|
+
const merged = new Map;
|
|
17106
|
+
for (const group of groups) {
|
|
17107
|
+
for (const message of group || []) {
|
|
17108
|
+
const key = getMessageKey(message);
|
|
17109
|
+
if (!key) {
|
|
17110
|
+
continue;
|
|
17111
|
+
}
|
|
17112
|
+
merged.set(key, message);
|
|
17113
|
+
}
|
|
17114
|
+
}
|
|
17115
|
+
return sortMessages([...merged.values()]);
|
|
17116
|
+
}
|
|
17117
|
+
|
|
17118
|
+
// ../../core/chat/messages/call.js
|
|
17119
|
+
var CALL_MSG_TYPE = "call";
|
|
17120
|
+
function readCallMessage(message) {
|
|
17121
|
+
if (message?.t !== CALL_MSG_TYPE || typeof message.callId !== "string" || !/^[0-9a-f]{64}$/u.test(message.callId) || !["started", "ended"].includes(message.event))
|
|
17122
|
+
return null;
|
|
17123
|
+
return { callId: message.callId, event: message.event };
|
|
17124
|
+
}
|
|
17125
|
+
function makeCallMessage(callId, event) {
|
|
17126
|
+
const message = { t: CALL_MSG_TYPE, callId, event };
|
|
17127
|
+
if (!readCallMessage(message))
|
|
17128
|
+
throw new Error("invalid call message");
|
|
17129
|
+
return message;
|
|
17130
|
+
}
|
|
17131
|
+
function callMessageText(message) {
|
|
17132
|
+
const call = readCallMessage(message);
|
|
17133
|
+
return call ? `call ${call.event}` : "";
|
|
17134
|
+
}
|
|
17135
|
+
function latestCallStartKey(messages) {
|
|
17136
|
+
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
17137
|
+
if (readCallMessage(messages[index])?.event === "started")
|
|
17138
|
+
return getMessageKey(messages[index]);
|
|
17139
|
+
}
|
|
17140
|
+
return "";
|
|
17141
|
+
}
|
|
17142
|
+
|
|
17143
|
+
// ../../core/chat/messages/types.js
|
|
17144
|
+
var ATTACHMENT_MSG_TYPES = ["img", "gif", "m4a", "mp4", "file"];
|
|
17145
|
+
var MAX_TXT_CHARS = CHAT_MAX_TEXT_CHARS;
|
|
17146
|
+
var REACTION_MSG_TYPE = "rxn";
|
|
17147
|
+
var DELETE_MSG_TYPE = "del";
|
|
17148
|
+
var SYSTEM_MSG_TYPE = "sys";
|
|
17149
|
+
var EPOCH_TRANSITION_MSG_TYPE = "epoch";
|
|
17150
|
+
var EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal";
|
|
17151
|
+
var SYSTEM_SETTINGS_KIND = "settings";
|
|
17152
|
+
var DEFAULT_REACTION_EMOJI = "❤️";
|
|
17153
|
+
var MAX_REACTIONS = CHAT_MAX_REACTIONS;
|
|
17154
|
+
var HOLD_VISIBLE_KEY = "__holdVisible";
|
|
17155
|
+
var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
|
|
16830
17156
|
|
|
16831
17157
|
// ../../core/notifications.js
|
|
16832
17158
|
"use client";
|
|
@@ -16843,10 +17169,47 @@ var CHAT_ATTENTION_KINDS = Object.freeze({
|
|
|
16843
17169
|
ROTATION: "rotation",
|
|
16844
17170
|
SILENT: "silent"
|
|
16845
17171
|
});
|
|
17172
|
+
var NOTIFICATION_PRESENTATION_KINDS = Object.freeze({
|
|
17173
|
+
MESSAGE: 0,
|
|
17174
|
+
REACTION: 1,
|
|
17175
|
+
ACTIVITY: 2,
|
|
17176
|
+
PHOTO: 3,
|
|
17177
|
+
VIDEO: 4,
|
|
17178
|
+
AUDIO: 5,
|
|
17179
|
+
FILE: 6,
|
|
17180
|
+
CALL_STARTED: 7,
|
|
17181
|
+
CALL_ENDED: 8
|
|
17182
|
+
});
|
|
17183
|
+
function notificationMessageKind(message) {
|
|
17184
|
+
switch (message?.t) {
|
|
17185
|
+
case SYSTEM_MSG_TYPE:
|
|
17186
|
+
case EPOCH_TRANSITION_MSG_TYPE:
|
|
17187
|
+
return NOTIFICATION_PRESENTATION_KINDS.ACTIVITY;
|
|
17188
|
+
case "img":
|
|
17189
|
+
case "gif":
|
|
17190
|
+
return NOTIFICATION_PRESENTATION_KINDS.PHOTO;
|
|
17191
|
+
case "mp4":
|
|
17192
|
+
return NOTIFICATION_PRESENTATION_KINDS.VIDEO;
|
|
17193
|
+
case "m4a":
|
|
17194
|
+
return NOTIFICATION_PRESENTATION_KINDS.AUDIO;
|
|
17195
|
+
case "file":
|
|
17196
|
+
return NOTIFICATION_PRESENTATION_KINDS.FILE;
|
|
17197
|
+
case "call": {
|
|
17198
|
+
const call = readCallMessage(message);
|
|
17199
|
+
return !call ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : call.event === "started" ? NOTIFICATION_PRESENTATION_KINDS.CALL_STARTED : NOTIFICATION_PRESENTATION_KINDS.CALL_ENDED;
|
|
17200
|
+
}
|
|
17201
|
+
default:
|
|
17202
|
+
return NOTIFICATION_PRESENTATION_KINDS.MESSAGE;
|
|
17203
|
+
}
|
|
17204
|
+
}
|
|
17205
|
+
function notificationMessageIsSilent(message) {
|
|
17206
|
+
return message?.t === SYSTEM_MSG_TYPE && message.sys === SYSTEM_SETTINGS_KIND || readCallMessage(message)?.event === "ended";
|
|
17207
|
+
}
|
|
16846
17208
|
var HEX_32_RE = /^[0-9a-f]{64}$/u;
|
|
16847
17209
|
var PEER_TAG_RE = /^[0-9a-f]{32}$/u;
|
|
16848
17210
|
var ATTENTION_KINDS = new Set(Object.values(CHAT_ATTENTION_KINDS));
|
|
16849
17211
|
var NOTIFICATION_MODES = new Set(Object.values(CHAT_NOTIFICATION_MODES));
|
|
17212
|
+
var PRESENTATION_KINDS = new Set(Object.values(NOTIFICATION_PRESENTATION_KINDS));
|
|
16850
17213
|
function hasCurrentChatAttentionRegistration(value) {
|
|
16851
17214
|
return value?.deliveryRegistered === true && value?.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION;
|
|
16852
17215
|
}
|
|
@@ -17002,13 +17365,30 @@ function notificationRouteTag(envelope) {
|
|
|
17002
17365
|
cleanBytes(input);
|
|
17003
17366
|
}
|
|
17004
17367
|
}
|
|
17005
|
-
|
|
17006
|
-
const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
|
|
17368
|
+
function notificationDescriptorClaims(descriptor) {
|
|
17007
17369
|
const peerTag = cleanText(descriptor.peerTag).toLowerCase();
|
|
17008
17370
|
const eventId = cleanText(descriptor.eventId);
|
|
17009
|
-
|
|
17371
|
+
const senderChatPK = cleanHex32(descriptor.senderChatPK, "notification sender key");
|
|
17372
|
+
const kind = descriptor.kind;
|
|
17373
|
+
if (!PEER_TAG_RE.test(peerTag) || !eventId || eventId.length > 256 || !PRESENTATION_KINDS.has(kind)) {
|
|
17010
17374
|
throw new Error("notification descriptor required");
|
|
17011
17375
|
}
|
|
17376
|
+
return { v: NOTIFICATION_DESCRIPTOR_VERSION, peerTag, eventId, senderChatPK, kind };
|
|
17377
|
+
}
|
|
17378
|
+
function notificationDescriptorSignatureInput(recipientPK, descriptor) {
|
|
17379
|
+
return encoder.encode([
|
|
17380
|
+
"veyl-notification-presentation-v1",
|
|
17381
|
+
recipientPK,
|
|
17382
|
+
descriptor.peerTag,
|
|
17383
|
+
descriptor.senderChatPK,
|
|
17384
|
+
descriptor.kind,
|
|
17385
|
+
descriptor.eventId
|
|
17386
|
+
].join("\x00"));
|
|
17387
|
+
}
|
|
17388
|
+
async function sealNotificationDescriptor(recipientNotificationPK, descriptor, sender) {
|
|
17389
|
+
const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
|
|
17390
|
+
const claims = notificationDescriptorClaims({ ...descriptor, senderChatPK: sender?.chatPK });
|
|
17391
|
+
const signature = signChatBytes(sender?.signingKey, notificationDescriptorSignatureInput(recipientPK, claims));
|
|
17012
17392
|
let eph = null;
|
|
17013
17393
|
let shared = null;
|
|
17014
17394
|
let key = null;
|
|
@@ -17019,9 +17399,8 @@ async function sealNotificationDescriptor(recipientNotificationPK, descriptor =
|
|
|
17019
17399
|
const epk = toHex(eph.pub);
|
|
17020
17400
|
key = deriveKey(shared.subarray(0, 32), "notification-descriptor-v1", [epk, recipientPK]);
|
|
17021
17401
|
plaintext = encoder.encode(JSON.stringify({
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
eventId
|
|
17402
|
+
...claims,
|
|
17403
|
+
signature
|
|
17025
17404
|
}));
|
|
17026
17405
|
const { iv, ct } = await sealAes(key, plaintext, descriptorAad(epk, recipientPK));
|
|
17027
17406
|
return {
|
|
@@ -17294,7 +17673,8 @@ var domains = Object.freeze({
|
|
|
17294
17673
|
veyl: `veyl.${ROOT_DOMAIN}`,
|
|
17295
17674
|
veylDev: `dev.veyl.${ROOT_DOMAIN}`,
|
|
17296
17675
|
live: `live.veyl.${ROOT_DOMAIN}`,
|
|
17297
|
-
liveDev: `live.dev.veyl.${ROOT_DOMAIN}
|
|
17676
|
+
liveDev: `live.dev.veyl.${ROOT_DOMAIN}`,
|
|
17677
|
+
bitcoin: `bitcoin.veyl.${ROOT_DOMAIN}`
|
|
17298
17678
|
});
|
|
17299
17679
|
function getVeylDevWebOrigin(port) {
|
|
17300
17680
|
const value = Number(port);
|
|
@@ -17315,6 +17695,7 @@ var liveEndpoints = Object.freeze({
|
|
|
17315
17695
|
prod: `wss://${domains.live}`,
|
|
17316
17696
|
dev: `wss://${domains.liveDev}`
|
|
17317
17697
|
});
|
|
17698
|
+
var bitcoinEndpoint = `https://${domains.bitcoin}/current`;
|
|
17318
17699
|
var appDomains = Object.freeze([
|
|
17319
17700
|
domains.veyl
|
|
17320
17701
|
]);
|
|
@@ -17348,9 +17729,11 @@ function isAddressOnNetwork(address, network) {
|
|
|
17348
17729
|
|
|
17349
17730
|
// ../../core/settings.js
|
|
17350
17731
|
var SEND_ON_SCAN_ENABLED = false;
|
|
17732
|
+
var WEB_LAYOUTS = ["floating", "sidebar"];
|
|
17351
17733
|
var WALLET_NETWORKS = new Set([MAINNET_NETWORK, REGTEST_NETWORK]);
|
|
17352
17734
|
var defaultSettings = {
|
|
17353
17735
|
glass: true,
|
|
17736
|
+
webLayout: "floating",
|
|
17354
17737
|
moneyFormat: "usd",
|
|
17355
17738
|
ghostWallet: true,
|
|
17356
17739
|
showChatPreviews: true,
|
|
@@ -17358,6 +17741,8 @@ var defaultSettings = {
|
|
|
17358
17741
|
confirmSend: false,
|
|
17359
17742
|
faceID: null,
|
|
17360
17743
|
walletNetwork: null,
|
|
17744
|
+
peerAudio: {},
|
|
17745
|
+
callAudio: { muted: false, deafened: false },
|
|
17361
17746
|
autolock: {
|
|
17362
17747
|
timer: "never",
|
|
17363
17748
|
onHide: false,
|
|
@@ -17419,9 +17804,14 @@ function normalizeSettings(settings, base = defaultSettings) {
|
|
|
17419
17804
|
};
|
|
17420
17805
|
next.autolock = normalizeAutolock(settings.autolock, current.autolock);
|
|
17421
17806
|
next.walletNetwork = normalizeWalletNetworkSetting(next.walletNetwork);
|
|
17807
|
+
next.peerAudio = normalizePeerAudio(settings.peerAudio, current.peerAudio);
|
|
17808
|
+
next.callAudio = normalizeCallAudio(settings.callAudio, current.callAudio);
|
|
17422
17809
|
if (!MONEY_FORMATS.includes(next.moneyFormat)) {
|
|
17423
17810
|
throw new Error("bad moneyFormat");
|
|
17424
17811
|
}
|
|
17812
|
+
if (!WEB_LAYOUTS.includes(next.webLayout)) {
|
|
17813
|
+
throw new Error("bad webLayout");
|
|
17814
|
+
}
|
|
17425
17815
|
if (typeof next.glass !== "boolean") {
|
|
17426
17816
|
throw new Error("glass must be boolean");
|
|
17427
17817
|
}
|
|
@@ -17445,6 +17835,32 @@ function normalizeSettings(settings, base = defaultSettings) {
|
|
|
17445
17835
|
}
|
|
17446
17836
|
return next;
|
|
17447
17837
|
}
|
|
17838
|
+
function normalizeCallAudio(patch, base = defaultSettings.callAudio) {
|
|
17839
|
+
if (patch !== undefined && (!patch || typeof patch !== "object" || Array.isArray(patch)))
|
|
17840
|
+
throw new Error("bad call audio settings");
|
|
17841
|
+
const { muted, deafened } = { ...defaultSettings.callAudio, ...base, ...patch };
|
|
17842
|
+
if (typeof muted !== "boolean" || typeof deafened !== "boolean")
|
|
17843
|
+
throw new Error("bad call audio settings");
|
|
17844
|
+
return { muted, deafened };
|
|
17845
|
+
}
|
|
17846
|
+
function normalizePeerAudio(patch, base = {}) {
|
|
17847
|
+
if (patch === undefined)
|
|
17848
|
+
return base;
|
|
17849
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch))
|
|
17850
|
+
throw new Error("bad peer audio settings");
|
|
17851
|
+
const entries = new Map(Object.entries(base));
|
|
17852
|
+
for (const [chatPK, audio] of Object.entries(patch)) {
|
|
17853
|
+
if (!/^[a-f0-9]{64}$/u.test(chatPK) || !audio || typeof audio !== "object" || Array.isArray(audio)) {
|
|
17854
|
+
throw new Error("bad peer audio settings");
|
|
17855
|
+
}
|
|
17856
|
+
const { volume = 100, muted = false } = { ...entries.get(chatPK), ...audio };
|
|
17857
|
+
if (!Number.isFinite(volume) || volume < 1 || volume > 200 || typeof muted !== "boolean") {
|
|
17858
|
+
throw new Error("bad peer audio settings");
|
|
17859
|
+
}
|
|
17860
|
+
entries.set(chatPK, { volume, muted });
|
|
17861
|
+
}
|
|
17862
|
+
return Object.fromEntries(entries);
|
|
17863
|
+
}
|
|
17448
17864
|
|
|
17449
17865
|
// ../../node_modules/.bun/@noble+ciphers@2.4.0/node_modules/@noble/ciphers/_arx.js
|
|
17450
17866
|
var encodeStr = (str) => Uint8Array.from(str.split(""), (c) => c.charCodeAt(0));
|
|
@@ -18060,7 +18476,7 @@ function packRegistryData(registry) {
|
|
|
18060
18476
|
}
|
|
18061
18477
|
return concatBytes4(new Uint8Array([SECRET_REGISTRY_ENVELOPE_VERSION]), registry.iv, registry.ct);
|
|
18062
18478
|
}
|
|
18063
|
-
var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
|
|
18479
|
+
var packSeedData = ({ crypto: crypto2 = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
|
|
18064
18480
|
const body = ciphertext || ct;
|
|
18065
18481
|
if (!salt || !iv || !body || !registry) {
|
|
18066
18482
|
throw new Error("seed data missing");
|
|
@@ -18068,7 +18484,7 @@ var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry,
|
|
|
18068
18484
|
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
18485
|
throw new Error("invalid seed data");
|
|
18070
18486
|
}
|
|
18071
|
-
const magic = encoder.encode(
|
|
18487
|
+
const magic = encoder.encode(crypto2);
|
|
18072
18488
|
const header = new Uint8Array(1 + magic.length + 8);
|
|
18073
18489
|
header[0] = magic.length;
|
|
18074
18490
|
header.set(magic, 1);
|
|
@@ -18338,6 +18754,9 @@ function normalizeBitcoinPaymentFeeQuote(feeQuote) {
|
|
|
18338
18754
|
};
|
|
18339
18755
|
}
|
|
18340
18756
|
function getFeeRateSatsPerVbyte(bitcoin, speed = DEFAULT_FEE_RATE_SPEED) {
|
|
18757
|
+
const observedAt = Date.parse(bitcoin?.fees?.updatedAtIso);
|
|
18758
|
+
if (!Number.isFinite(observedAt) || Date.now() - observedAt > BITCOIN_FEES_MAX_AGE_MS || observedAt > Date.now() + 60000)
|
|
18759
|
+
return null;
|
|
18341
18760
|
const rates = bitcoin?.fees?.satPerVbyte;
|
|
18342
18761
|
const key = String(speed || DEFAULT_FEE_RATE_SPEED);
|
|
18343
18762
|
const keys = FEE_RATE_FALLBACKS[key] ?? [key, ...FEE_RATE_FALLBACKS.default];
|
|
@@ -18532,7 +18951,7 @@ function walletPubkey(value) {
|
|
|
18532
18951
|
}
|
|
18533
18952
|
return key;
|
|
18534
18953
|
}
|
|
18535
|
-
function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner } = {}) {
|
|
18954
|
+
function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity } = {}) {
|
|
18536
18955
|
const pending = [];
|
|
18537
18956
|
const close = (owner) => {
|
|
18538
18957
|
try {
|
|
@@ -18543,6 +18962,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
|
|
|
18543
18962
|
};
|
|
18544
18963
|
close(vaultAccess);
|
|
18545
18964
|
close(vaultSigner);
|
|
18965
|
+
close(callIdentity);
|
|
18546
18966
|
close(localCache);
|
|
18547
18967
|
lockWallet(wallet);
|
|
18548
18968
|
lockChat(chatPrivateKey, chatPK);
|
|
@@ -18598,6 +19018,7 @@ function createAccountSession(resources = {}) {
|
|
|
18598
19018
|
session.localCache = null;
|
|
18599
19019
|
session.vaultAccess = null;
|
|
18600
19020
|
session.vaultSigner = null;
|
|
19021
|
+
session.callIdentity = null;
|
|
18601
19022
|
closePromise = Promise.allSettled(pending).then(() => {
|
|
18602
19023
|
return;
|
|
18603
19024
|
});
|
|
@@ -18737,6 +19158,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18737
19158
|
let cacheKey = null;
|
|
18738
19159
|
let settingsKey = null;
|
|
18739
19160
|
let vaultSigner = null;
|
|
19161
|
+
let callIdentity = null;
|
|
18740
19162
|
let vaultAccess = null;
|
|
18741
19163
|
let walletBoot = null;
|
|
18742
19164
|
let network;
|
|
@@ -18777,6 +19199,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18777
19199
|
notificationPK = toHex(notificationPublicKey);
|
|
18778
19200
|
cacheKey = getCacheSeed(registry);
|
|
18779
19201
|
vaultSigner = createVaultSigner(masterSeed);
|
|
19202
|
+
callIdentity = createCallIdentity(masterSeed, cloud.environment);
|
|
18780
19203
|
settingsKey = deriveSettingsKey(cacheKey, uid);
|
|
18781
19204
|
mark(diag, "vault.unlock.derive.done", { elapsedMs: Date.now() - deriveStartedAt, source });
|
|
18782
19205
|
cleanBytes(masterSeed);
|
|
@@ -18825,6 +19248,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18825
19248
|
notificationPublicKey,
|
|
18826
19249
|
localCache,
|
|
18827
19250
|
vaultSigner,
|
|
19251
|
+
callIdentity,
|
|
18828
19252
|
vaultAccess,
|
|
18829
19253
|
vaultPK: vaultSigner.publicKey
|
|
18830
19254
|
});
|
|
@@ -18925,7 +19349,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
|
|
|
18925
19349
|
return session;
|
|
18926
19350
|
} catch (error) {
|
|
18927
19351
|
walletBoot?.close();
|
|
18928
|
-
await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner });
|
|
19352
|
+
await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity });
|
|
18929
19353
|
throw error;
|
|
18930
19354
|
} finally {
|
|
18931
19355
|
cleanBytes(masterSeed, walletEntropy, chatSeed, cacheKey, settingsKey);
|
|
@@ -18962,88 +19386,6 @@ function hasAgreement(user, contract = CURRENT_AGREEMENT) {
|
|
|
18962
19386
|
return isAgreementAccepted(user?.agreement, contract);
|
|
18963
19387
|
}
|
|
18964
19388
|
|
|
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
19389
|
// ../../core/moderation.js
|
|
19048
19390
|
function banUntilMs(ban) {
|
|
19049
19391
|
if (!ban || typeof ban !== "object" || Array.isArray(ban) || ban.until == null) {
|
|
@@ -19445,6 +19787,9 @@ function readAccountType(profile) {
|
|
|
19445
19787
|
function hasPeerKeys(profile) {
|
|
19446
19788
|
return !!(profile?.walletPK || profile?.chatPK);
|
|
19447
19789
|
}
|
|
19790
|
+
function hasChatIdentity(profile) {
|
|
19791
|
+
return !!(profile?.uid && profile?.chatPK && profile?.chatSigningPK && profile?.notificationPK);
|
|
19792
|
+
}
|
|
19448
19793
|
function readProfileChatIdentity(profile) {
|
|
19449
19794
|
const chat = profile?.identities?.chat;
|
|
19450
19795
|
return {
|
|
@@ -19459,7 +19804,7 @@ function peerUid(peer) {
|
|
|
19459
19804
|
return typeof peer === "string" ? cleanText(peer) : cleanText(peer?.uid);
|
|
19460
19805
|
}
|
|
19461
19806
|
function isFullProfile(profile) {
|
|
19462
|
-
return !!(profile?.uid && (
|
|
19807
|
+
return !!(profile?.uid && hasPeerKeys(profile) && ["username", "walletPK", "chatPK", "chatSigningPK", "notificationPK"].every((key) => (key in profile)) && (!profile.chatPK || hasChatIdentity(profile)));
|
|
19463
19808
|
}
|
|
19464
19809
|
function normalizeProfile(profile, uid = profile?.uid || null) {
|
|
19465
19810
|
return {
|
|
@@ -19560,6 +19905,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
|
|
|
19560
19905
|
let authSession = 0;
|
|
19561
19906
|
let userUid = null;
|
|
19562
19907
|
let settingsKey = null;
|
|
19908
|
+
let settingsWriter = null;
|
|
19909
|
+
let openedSettingsBody;
|
|
19563
19910
|
let agreementStored = null;
|
|
19564
19911
|
let agreementOverride = null;
|
|
19565
19912
|
let agreementAcceptance = null;
|
|
@@ -19948,6 +20295,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
|
|
|
19948
20295
|
settingsBody: privateData.settings ?? null,
|
|
19949
20296
|
settingsFromCache: false
|
|
19950
20297
|
}));
|
|
20298
|
+
refreshUnlockedSettings();
|
|
19951
20299
|
}), current((error) => {
|
|
19952
20300
|
markError(diag, "user.settings.snapshot", authStartedAt, error);
|
|
19953
20301
|
if (revokeAuthenticationError(authUser, error, "settings-listener"))
|
|
@@ -20163,10 +20511,12 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
|
|
|
20163
20511
|
throw new Error("settings key required");
|
|
20164
20512
|
if (state.settingsBody === undefined)
|
|
20165
20513
|
throw new Error("settings not available");
|
|
20166
|
-
const
|
|
20514
|
+
const body = state.settingsBody;
|
|
20515
|
+
const nextSettings = body === null ? settingsState() : await openSettings(key, uid, body);
|
|
20167
20516
|
if (!isCurrentUser(uid, session))
|
|
20168
20517
|
return nextSettings;
|
|
20169
20518
|
setSettingsKey(key);
|
|
20519
|
+
openedSettingsBody = body;
|
|
20170
20520
|
setState((user) => ({
|
|
20171
20521
|
...user,
|
|
20172
20522
|
settings: settingsState(nextSettings)
|
|
@@ -20187,15 +20537,59 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
|
|
|
20187
20537
|
throw new Error("auth");
|
|
20188
20538
|
if (!settingsKey)
|
|
20189
20539
|
throw new Error("settings locked");
|
|
20190
|
-
|
|
20191
|
-
if (!
|
|
20192
|
-
|
|
20193
|
-
|
|
20194
|
-
|
|
20195
|
-
|
|
20196
|
-
|
|
20197
|
-
|
|
20198
|
-
|
|
20540
|
+
normalizeSettings(patch, state.settings);
|
|
20541
|
+
if (!settingsWriter || settingsWriter.key !== settingsKey) {
|
|
20542
|
+
settingsWriter = { key: settingsKey, uid, session, pending: [], running: false };
|
|
20543
|
+
}
|
|
20544
|
+
const writer = settingsWriter;
|
|
20545
|
+
const result = new Promise((resolve, reject) => writer.pending.push({ patch, resolve, reject }));
|
|
20546
|
+
writeSettings(writer);
|
|
20547
|
+
return result;
|
|
20548
|
+
}
|
|
20549
|
+
async function writeSettings(writer) {
|
|
20550
|
+
if (writer.running)
|
|
20551
|
+
return;
|
|
20552
|
+
writer.running = true;
|
|
20553
|
+
const current = () => settingsKey === writer.key && isCurrentUser(writer.uid, writer.session);
|
|
20554
|
+
while (writer.pending.length) {
|
|
20555
|
+
const batch = writer.pending.splice(0);
|
|
20556
|
+
try {
|
|
20557
|
+
if (!current())
|
|
20558
|
+
throw new Error("settings locked");
|
|
20559
|
+
const desired = batch.reduce((settings2, { patch }) => normalizeSettings(patch, settings2), state.settings);
|
|
20560
|
+
const { settings, body } = await cloud.user.settings.write(writer.uid, desired, { currentSettings: state.settings, key: writer.key });
|
|
20561
|
+
if (!current())
|
|
20562
|
+
throw new Error("settings locked");
|
|
20563
|
+
openedSettingsBody = body;
|
|
20564
|
+
setState((user) => ({ ...user, settingsReady: true, settings: settingsState(settings), settingsBody: body }));
|
|
20565
|
+
for (const request of batch)
|
|
20566
|
+
request.resolve(settings);
|
|
20567
|
+
} catch (error) {
|
|
20568
|
+
for (const request of batch)
|
|
20569
|
+
request.reject(error);
|
|
20570
|
+
}
|
|
20571
|
+
}
|
|
20572
|
+
writer.running = false;
|
|
20573
|
+
if (settingsWriter === writer)
|
|
20574
|
+
settingsWriter = null;
|
|
20575
|
+
refreshUnlockedSettings();
|
|
20576
|
+
}
|
|
20577
|
+
async function refreshUnlockedSettings() {
|
|
20578
|
+
const key = settingsKey;
|
|
20579
|
+
const body = state.settingsBody;
|
|
20580
|
+
if (!key || body === undefined || body === openedSettingsBody || settingsWriter?.key === key)
|
|
20581
|
+
return;
|
|
20582
|
+
const uid = state.uid;
|
|
20583
|
+
try {
|
|
20584
|
+
const settings = body === null ? settingsState() : await openSettings(key, uid, body);
|
|
20585
|
+
if (settingsKey !== key || state.settingsBody !== body || settingsWriter?.key === key)
|
|
20586
|
+
return;
|
|
20587
|
+
openedSettingsBody = body;
|
|
20588
|
+
setState((user) => ({ ...user, settings: settingsState(settings) }));
|
|
20589
|
+
} catch (error) {
|
|
20590
|
+
if (settingsKey === key && state.settingsBody === body)
|
|
20591
|
+
markError(diag, "user.settings.open", Date.now(), error);
|
|
20592
|
+
}
|
|
20199
20593
|
}
|
|
20200
20594
|
function isBlocked(peer) {
|
|
20201
20595
|
const nextPeerUid = peerUid(peer);
|
|
@@ -20954,48 +21348,6 @@ function readAccountBootstrap(value, uid) {
|
|
|
20954
21348
|
};
|
|
20955
21349
|
}
|
|
20956
21350
|
|
|
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
21351
|
// ../../core/chat/ids.js
|
|
21000
21352
|
function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
|
|
21001
21353
|
const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
|
|
@@ -21381,16 +21733,16 @@ function normalizeOpenedAction(epoch, head, action) {
|
|
|
21381
21733
|
};
|
|
21382
21734
|
}
|
|
21383
21735
|
async function openMsgPlaintext(epoch, nonce, ct, aad, options) {
|
|
21384
|
-
const
|
|
21385
|
-
if (typeof
|
|
21386
|
-
return
|
|
21736
|
+
const crypto2 = options?.crypto;
|
|
21737
|
+
if (typeof crypto2?.openBox === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
|
|
21738
|
+
return crypto2.openBox(epoch.bodyKey, nonce, ct, aad);
|
|
21387
21739
|
}
|
|
21388
21740
|
return openBox(epoch.bodyKey, nonce, ct, aad);
|
|
21389
21741
|
}
|
|
21390
21742
|
async function verifyMsgSignature(publicKey, sig, bytes, options) {
|
|
21391
|
-
const
|
|
21392
|
-
if (typeof
|
|
21393
|
-
return
|
|
21743
|
+
const crypto2 = options?.crypto;
|
|
21744
|
+
if (typeof crypto2?.verifyChatBytes === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
|
|
21745
|
+
return crypto2.verifyChatBytes(publicKey, sig, bytes);
|
|
21394
21746
|
}
|
|
21395
21747
|
return verifyChatBytes(publicKey, toHex(sig), bytes);
|
|
21396
21748
|
}
|
|
@@ -21473,9 +21825,9 @@ async function openMessageBatchV3(epoch, records, options = {}) {
|
|
|
21473
21825
|
if (!epoch?.chatId || !epoch?.epochId || !epoch?.bodyKey || !epoch?.manifest) {
|
|
21474
21826
|
throw new Error("chat batch epoch required");
|
|
21475
21827
|
}
|
|
21476
|
-
const
|
|
21477
|
-
if (typeof
|
|
21478
|
-
const opened = await
|
|
21828
|
+
const crypto2 = options.crypto;
|
|
21829
|
+
if (typeof crypto2?.openMessageBatchV3 === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
|
|
21830
|
+
const opened = await crypto2.openMessageBatchV3(makeBatchOpenRequest(epoch, source));
|
|
21479
21831
|
if (!Array.isArray(opened) || opened.length !== source.length) {
|
|
21480
21832
|
throw new Error("invalid chat message batch result");
|
|
21481
21833
|
}
|
|
@@ -22831,20 +23183,6 @@ async function readChatFile(readChatMedia, file) {
|
|
|
22831
23183
|
}
|
|
22832
23184
|
}
|
|
22833
23185
|
|
|
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
23186
|
// ../../core/username.js
|
|
22849
23187
|
var MAX_USERNAME = USERNAME_MAX_CHARS;
|
|
22850
23188
|
var usernameKeyRegex = /^[a-z0-9]$/i;
|
|
@@ -24493,7 +24831,7 @@ function getChatSettingsEventAvatar(msg) {
|
|
|
24493
24831
|
return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
|
|
24494
24832
|
}
|
|
24495
24833
|
function isSystemMsg(msg) {
|
|
24496
|
-
return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
|
|
24834
|
+
return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || msg?.t === CALL_MSG_TYPE || isMembershipEventMsg(msg));
|
|
24497
24835
|
}
|
|
24498
24836
|
function makeChatInviteNotice(profiles) {
|
|
24499
24837
|
const names = [...new Map(profiles.map((profile) => [profile.chatPK, formatUserDisplay(profile, true)])).values()];
|
|
@@ -24528,6 +24866,8 @@ function isMembershipEventMsg(msg) {
|
|
|
24528
24866
|
return !!getMembershipEvent(msg);
|
|
24529
24867
|
}
|
|
24530
24868
|
function getSystemMsgText(msg) {
|
|
24869
|
+
if (msg?.t === CALL_MSG_TYPE)
|
|
24870
|
+
return callMessageText(msg);
|
|
24531
24871
|
if (isChatInviteNotice(msg)) {
|
|
24532
24872
|
const names = msg.names.length < 2 ? msg.names[0] : `${msg.names.slice(0, -1).join(", ")} and ${msg.names.at(-1)}`;
|
|
24533
24873
|
return `${names} ${msg.names.length === 1 ? "is" : "are"} not accepting chat invitations right now`;
|
|
@@ -24625,6 +24965,8 @@ function canShowMsg(msg) {
|
|
|
24625
24965
|
return hasText(msg.a);
|
|
24626
24966
|
case "pay":
|
|
24627
24967
|
return isChatPayment(msg) || isPendingChatPayment(msg);
|
|
24968
|
+
case CALL_MSG_TYPE:
|
|
24969
|
+
return !!readCallMessage(msg);
|
|
24628
24970
|
case SYSTEM_MSG_TYPE:
|
|
24629
24971
|
return !!getSystemMsgText(msg);
|
|
24630
24972
|
default:
|
|
@@ -24810,7 +25152,7 @@ function groupEvidence(messages, byKey, memberStatesByEpoch) {
|
|
|
24810
25152
|
const target = byKey.get(reference);
|
|
24811
25153
|
addEvidence(evidence, messageEpochId(target), actor, messageOrderMs(target), seenAt);
|
|
24812
25154
|
}
|
|
24813
|
-
if (canShowMsg(message)
|
|
25155
|
+
if (canShowMsg(message)) {
|
|
24814
25156
|
addEvidence(evidence, messageEpochId(message), actor, seenAt, seenAt);
|
|
24815
25157
|
}
|
|
24816
25158
|
}
|
|
@@ -24837,8 +25179,13 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
|
|
|
24837
25179
|
const heldExpired = [];
|
|
24838
25180
|
const shorten = [];
|
|
24839
25181
|
let nextExpiryAt = null;
|
|
25182
|
+
const latestStart = latestCallStartKey(projectedMessages.filter(isServerConfirmedMsg));
|
|
25183
|
+
const endedCalls = new Set(projectedMessages.filter(isServerConfirmedMsg).filter((message) => readCallMessage(message)?.event === "ended").map((message) => message.callId));
|
|
24840
25184
|
for (const message of projectedMessages) {
|
|
24841
|
-
if (!isServerConfirmedMsg(message) ||
|
|
25185
|
+
if (!isServerConfirmedMsg(message) || !canShowMsg(message) || message.ttl == null)
|
|
25186
|
+
continue;
|
|
25187
|
+
const call = readCallMessage(message);
|
|
25188
|
+
if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined && !endedCalls.has(call.callId) || options.activeCallId === call.callId))
|
|
24842
25189
|
continue;
|
|
24843
25190
|
const messageMs = messageOrderMs(message);
|
|
24844
25191
|
const epochId = messageEpochId(message);
|
|
@@ -24875,7 +25222,7 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
|
|
|
24875
25222
|
if (expiresAt == null)
|
|
24876
25223
|
continue;
|
|
24877
25224
|
const currentTtlMs = Number.isFinite(message.ttl) ? message.ttl : Number(message.ttl?.toMillis?.());
|
|
24878
|
-
if (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt)
|
|
25225
|
+
if (!isControlMsg(message) && (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt))
|
|
24879
25226
|
shorten.push({ message, expiresAt });
|
|
24880
25227
|
if (expiresAt > now) {
|
|
24881
25228
|
nextExpiryAt = nextExpiryAt == null ? expiresAt : Math.min(nextExpiryAt, expiresAt);
|
|
@@ -25028,6 +25375,8 @@ async function compactMessages({ chatId, maintenance, messages, deletedKeys, pro
|
|
|
25028
25375
|
function sendableMsgPayload(message) {
|
|
25029
25376
|
if (isChatInviteNotice(message))
|
|
25030
25377
|
throw new Error("local chat notices cannot be sent");
|
|
25378
|
+
if (message?.t === CALL_MSG_TYPE && !readCallMessage(message))
|
|
25379
|
+
throw new Error("invalid call message");
|
|
25031
25380
|
const { localData, localUri, pending, failed, peerChatPK, id, from, ts, chatId, chatDraft, protocol, editedAt, editId, editStatus, editOriginal, ...payload } = message || {};
|
|
25032
25381
|
return payload;
|
|
25033
25382
|
}
|
|
@@ -25074,6 +25423,8 @@ function canRenderPreviewContent(preview) {
|
|
|
25074
25423
|
if (preview.t === "pay") {
|
|
25075
25424
|
return isChatPayment(preview) || isPendingChatPayment(preview);
|
|
25076
25425
|
}
|
|
25426
|
+
if (preview.t === CALL_MSG_TYPE)
|
|
25427
|
+
return !!readCallMessage(preview);
|
|
25077
25428
|
if (isAttachmentMsgType(preview?.t)) {
|
|
25078
25429
|
return true;
|
|
25079
25430
|
}
|
|
@@ -25701,8 +26052,9 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
|
|
|
25701
26052
|
const route = routes[member.chatPK] || {};
|
|
25702
26053
|
const descriptor = await sealNotificationDescriptor(member.notificationPK, {
|
|
25703
26054
|
peerTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, member.chatPK),
|
|
25704
|
-
eventId: messageId
|
|
25705
|
-
|
|
26055
|
+
eventId: messageId,
|
|
26056
|
+
kind: fields.presentationKind ?? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
|
|
26057
|
+
}, identity);
|
|
25706
26058
|
const ping = await sealPing(identity, member.chatPK, {
|
|
25707
26059
|
kind: fields.kind,
|
|
25708
26060
|
chatId: epochState.manifest.chatId,
|
|
@@ -25776,6 +26128,7 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
|
|
|
25776
26128
|
cid: head.cid,
|
|
25777
26129
|
record: { lane: epoch.messageLane, head, body, ttlMs },
|
|
25778
26130
|
message: ownerPreview(epoch, identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
|
|
26131
|
+
actionOp,
|
|
25779
26132
|
mentionRecipientChatPKs: mentionTargets.recipientChatPKs,
|
|
25780
26133
|
tsMs
|
|
25781
26134
|
};
|
|
@@ -25809,6 +26162,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
|
|
|
25809
26162
|
msgId: messageId,
|
|
25810
26163
|
record,
|
|
25811
26164
|
message: sentMessage,
|
|
26165
|
+
actionOp,
|
|
25812
26166
|
mentionRecipientChatPKs,
|
|
25813
26167
|
tsMs
|
|
25814
26168
|
} = prepared;
|
|
@@ -25839,8 +26193,9 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
|
|
|
25839
26193
|
shouldPing,
|
|
25840
26194
|
deliverRecipients: options.deliverRecipients !== false,
|
|
25841
26195
|
kind: pingKind,
|
|
26196
|
+
presentationKind: isReactionMsg(sentMessage) && sentMessage.emoji ? NOTIFICATION_PRESENTATION_KINDS.REACTION : actionOp !== CHAT_ACTION_OPS.CREATE || isControlMsg(sentMessage) ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : notificationMessageKind(sentMessage),
|
|
25842
26197
|
ownDeliveryCapability: routeState.capability,
|
|
25843
|
-
silent: options.notify === false,
|
|
26198
|
+
silent: options.notify === false || notificationMessageIsSilent(sentMessage),
|
|
25844
26199
|
recipientChatPKs: deliveryRecipientChatPKs,
|
|
25845
26200
|
attentionRecipientChatPKs: mentionRecipientChatPKs,
|
|
25846
26201
|
tsMs
|
|
@@ -26185,7 +26540,7 @@ function partitionExpiredMessageRecords(records, expiredKeys, cache, now = Date.
|
|
|
26185
26540
|
return { activeRecords, expiredRecords };
|
|
26186
26541
|
}
|
|
26187
26542
|
function canHideExpiredMessage(message, chatPK) {
|
|
26188
|
-
return isServerConfirmedMsg(message) && isPeerMsg(message, chatPK) && canShowMsg(message) &&
|
|
26543
|
+
return isServerConfirmedMsg(message) && isPeerMsg(message, chatPK) && canShowMsg(message) && message.ttl != null && message.permanent !== true;
|
|
26189
26544
|
}
|
|
26190
26545
|
function hiddenMessageKeys(messages, chatPK, peerChatPK, options = {}) {
|
|
26191
26546
|
if (!chatPK || !peerChatPK || !Array.isArray(messages) || !messages.length)
|
|
@@ -27806,8 +28161,9 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
|
|
|
27806
28161
|
}
|
|
27807
28162
|
const descriptor = await sealNotificationDescriptor(member.notificationPK, {
|
|
27808
28163
|
peerTag: notificationChatTag(nextState2.stateCapability, nextState2.manifest.chatId, member.chatPK),
|
|
27809
|
-
eventId: nextState2.mlsPackageId
|
|
27810
|
-
|
|
28164
|
+
eventId: nextState2.mlsPackageId,
|
|
28165
|
+
kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
|
|
28166
|
+
}, identity);
|
|
27811
28167
|
const ping = await sealPing(identity, member.chatPK, {
|
|
27812
28168
|
kind: "welcome",
|
|
27813
28169
|
chatId: nextState2.manifest.chatId,
|
|
@@ -37904,6 +38260,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
37904
38260
|
const boundary = authorityBoundaries.get(chat?.id);
|
|
37905
38261
|
if (boundary?.terminal)
|
|
37906
38262
|
return false;
|
|
38263
|
+
if (boundary?.leftEpoch != null && !(chat?.epochVersion > boundary.leftEpoch))
|
|
38264
|
+
return false;
|
|
37907
38265
|
if (!chat?.id || !Number.isSafeInteger(sourceRevision))
|
|
37908
38266
|
return true;
|
|
37909
38267
|
if (sourceRevision < sourceRevisionFloor)
|
|
@@ -38067,7 +38425,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38067
38425
|
continue;
|
|
38068
38426
|
if (!sourceCanRemove(chatId, sourceRevision))
|
|
38069
38427
|
continue;
|
|
38070
|
-
if (entry.announcement && !authorityBoundaries.get(chatId)?.owner) {
|
|
38428
|
+
if (entry.announcement && !ownerCoversAnnouncement(authorityBoundaries.get(chatId)?.owner, entry.announcement)) {
|
|
38071
38429
|
entry.cache = null;
|
|
38072
38430
|
continue;
|
|
38073
38431
|
}
|
|
@@ -38153,6 +38511,9 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38153
38511
|
return false;
|
|
38154
38512
|
if (authorityBoundaries.get(announcement.chatId)?.terminal)
|
|
38155
38513
|
return false;
|
|
38514
|
+
const leftEpoch = authorityBoundaries.get(announcement.chatId)?.leftEpoch;
|
|
38515
|
+
if (leftEpoch != null && !(announcement.epochVersion > leftEpoch))
|
|
38516
|
+
return false;
|
|
38156
38517
|
const sources = getSources();
|
|
38157
38518
|
if (sources.pendingDeleteIdsRef.current.has(announcement.chatId))
|
|
38158
38519
|
return false;
|
|
@@ -38211,6 +38572,26 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38211
38572
|
return false;
|
|
38212
38573
|
return removeOwner(chatId, options);
|
|
38213
38574
|
};
|
|
38575
|
+
const leaveOwner = (chatId, epochVersion) => {
|
|
38576
|
+
if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
|
|
38577
|
+
return false;
|
|
38578
|
+
const previous = authorityBoundaries.get(chatId);
|
|
38579
|
+
if (previous?.terminal)
|
|
38580
|
+
return false;
|
|
38581
|
+
if (previous?.owner?.epochVersion > epochVersion)
|
|
38582
|
+
return false;
|
|
38583
|
+
authorityRevision += 1;
|
|
38584
|
+
authorityBoundaries.set(chatId, {
|
|
38585
|
+
owner: previous?.owner || structuralChat(ownerFor(entries.get(chatId))),
|
|
38586
|
+
revision: authorityRevision,
|
|
38587
|
+
terminal: false,
|
|
38588
|
+
leftEpoch: Math.max(previous?.leftEpoch || 0, epochVersion)
|
|
38589
|
+
});
|
|
38590
|
+
entries.delete(chatId);
|
|
38591
|
+
clearRemovedOwners([chatId]);
|
|
38592
|
+
publishOwners({ warm: false });
|
|
38593
|
+
return true;
|
|
38594
|
+
};
|
|
38214
38595
|
const getOwnerChat = (chatId) => {
|
|
38215
38596
|
if (!chatId || isHiddenChatId(chatId))
|
|
38216
38597
|
return null;
|
|
@@ -38397,6 +38778,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
|
|
|
38397
38778
|
observeMessages,
|
|
38398
38779
|
patchRowSettings,
|
|
38399
38780
|
removeOwner,
|
|
38781
|
+
leaveOwner,
|
|
38400
38782
|
removeInboxOwner,
|
|
38401
38783
|
render,
|
|
38402
38784
|
reset,
|
|
@@ -40398,27 +40780,23 @@ function createChatSessionLaunches({
|
|
|
40398
40780
|
const openDirectChat = async (profile) => {
|
|
40399
40781
|
const operation = operationContext();
|
|
40400
40782
|
const { chatPK, blocked } = operation.identity;
|
|
40401
|
-
|
|
40402
|
-
|
|
40783
|
+
if (!profile?.uid || !profile?.chatPK)
|
|
40784
|
+
throw new Error("peer identity required");
|
|
40785
|
+
if (profile.chatPK === chatPK)
|
|
40403
40786
|
return openNotesChat(operation);
|
|
40404
|
-
if (new Set(blocked).has(
|
|
40787
|
+
if (new Set(blocked).has(profile.uid))
|
|
40405
40788
|
throw new Error("blocked peer cannot be opened");
|
|
40406
40789
|
assertDirectAdmission(profile, operation);
|
|
40407
|
-
const existingChatId = getPeerChatId(
|
|
40790
|
+
const existingChatId = getPeerChatId(profile.chatPK, operation);
|
|
40408
40791
|
if (existingChatId) {
|
|
40409
40792
|
setSelectedChat(existingChatId);
|
|
40410
40793
|
return existingChatId;
|
|
40411
40794
|
}
|
|
40795
|
+
const member = { uid: profile.uid, chatPK: profile.chatPK };
|
|
40412
40796
|
const routeId = directRouteIdForPeer(member.chatPK, operation);
|
|
40413
40797
|
if (operation.pendingOwner.getPendingLaunch()?.id === routeId)
|
|
40414
40798
|
return routeId;
|
|
40415
|
-
operation.pendingOwner.showEmpty(routeId, [
|
|
40416
|
-
...member,
|
|
40417
|
-
username: profile?.username || null,
|
|
40418
|
-
avatar: profile?.avatar || null,
|
|
40419
|
-
accountType: profile?.accountType || null,
|
|
40420
|
-
chatAdmission: normalizeChatAdmission(profile?.chatAdmission)
|
|
40421
|
-
}], [member], "direct");
|
|
40799
|
+
operation.pendingOwner.showEmpty(routeId, [profile], [member], "direct");
|
|
40422
40800
|
const materialization = directMaterializations.get(routeId);
|
|
40423
40801
|
if (materialization && isCurrent(materialization.operation)) {
|
|
40424
40802
|
materialization.promise.then((chatId) => {
|
|
@@ -41712,10 +42090,10 @@ function createChatSessionMembership({
|
|
|
41712
42090
|
await cloud.delivery.revoke(capability).catch(() => false);
|
|
41713
42091
|
assertCurrent(operation);
|
|
41714
42092
|
if (retirementStarted) {
|
|
41715
|
-
operation.deleteActions.commitLeftChat(chatId);
|
|
42093
|
+
operation.deleteActions.commitLeftChat(chatId, chat.epochVersion);
|
|
41716
42094
|
retirementStarted = false;
|
|
41717
42095
|
} else
|
|
41718
|
-
operation.deleteActions.dropLeftChat(chatId);
|
|
42096
|
+
operation.deleteActions.dropLeftChat(chatId, chat.epochVersion);
|
|
41719
42097
|
return { left: true, message: proposal.message };
|
|
41720
42098
|
} catch (error) {
|
|
41721
42099
|
if (retirementStarted && isCurrent(operation)) {
|
|
@@ -41967,8 +42345,9 @@ async function deliverChatDeletedPings(cloud, identityValue, epochState, members
|
|
|
41967
42345
|
continue;
|
|
41968
42346
|
const descriptor = await sealNotificationDescriptor(member.notificationPK, {
|
|
41969
42347
|
peerTag: notificationChatTag(epochState.stateCapability, manifest.chatId, member.chatPK),
|
|
41970
|
-
eventId
|
|
41971
|
-
|
|
42348
|
+
eventId,
|
|
42349
|
+
kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
|
|
42350
|
+
}, identity);
|
|
41972
42351
|
const ping = await sealPing(identity, member.chatPK, {
|
|
41973
42352
|
kind: "chat_deleted",
|
|
41974
42353
|
chatId: manifest.chatId,
|
|
@@ -42121,20 +42500,20 @@ function createChatDelete({
|
|
|
42121
42500
|
return;
|
|
42122
42501
|
listActionsRef.current?.removeOwner?.(chatId, { warm: false });
|
|
42123
42502
|
};
|
|
42124
|
-
const dropLeftChat = (chatId) => {
|
|
42503
|
+
const dropLeftChat = (chatId, epochVersion) => {
|
|
42125
42504
|
if (!chatId)
|
|
42126
42505
|
return false;
|
|
42127
|
-
|
|
42506
|
+
listActionsRef.current?.leaveOwner?.(chatId, epochVersion);
|
|
42128
42507
|
return true;
|
|
42129
42508
|
};
|
|
42130
|
-
const commitLeftChat = (chatId) => {
|
|
42509
|
+
const commitLeftChat = (chatId, epochVersion) => {
|
|
42131
42510
|
if (!chatId)
|
|
42132
42511
|
return false;
|
|
42133
42512
|
pendingDeleteIdsRef.current.delete(chatId);
|
|
42134
42513
|
locallyDeletedChatIdsRef.current.delete(chatId);
|
|
42135
42514
|
deletedChatIdsRef.current.delete(chatId);
|
|
42136
42515
|
keepSelectedDeletedChatIdsRef.current.delete(chatId);
|
|
42137
|
-
|
|
42516
|
+
dropLeftChat(chatId, epochVersion);
|
|
42138
42517
|
return true;
|
|
42139
42518
|
};
|
|
42140
42519
|
const dropUnavailableChat = (chatId) => {
|
|
@@ -43699,6 +44078,7 @@ function createChatSession({
|
|
|
43699
44078
|
mls = null,
|
|
43700
44079
|
live = null,
|
|
43701
44080
|
maintenance,
|
|
44081
|
+
resolveActiveCall,
|
|
43702
44082
|
incomingChatDecision = null,
|
|
43703
44083
|
getPeerProfile = null,
|
|
43704
44084
|
refreshPeerProfile = null,
|
|
@@ -44474,7 +44854,6 @@ function createChatSession({
|
|
|
44474
44854
|
assertBulkOperationCurrent(operation);
|
|
44475
44855
|
await cloud.user.chats.unlink(identity.uid, chat.entryId);
|
|
44476
44856
|
assertBulkOperationCurrent(operation);
|
|
44477
|
-
operation.deleteActions.commitRetirement(chat.id);
|
|
44478
44857
|
} else {
|
|
44479
44858
|
await operation.deleteActions.deleteChat(chat, { cleanup: false });
|
|
44480
44859
|
}
|
|
@@ -44561,6 +44940,26 @@ function createChatSession({
|
|
|
44561
44940
|
return false;
|
|
44562
44941
|
}
|
|
44563
44942
|
};
|
|
44943
|
+
const prepareCall = async (chatId) => {
|
|
44944
|
+
const generation = authGeneration;
|
|
44945
|
+
const current = () => generation === authGeneration && online && pendingOwner.canSendToChat(chatId);
|
|
44946
|
+
if (!current())
|
|
44947
|
+
throw new Error("chat unavailable");
|
|
44948
|
+
await pendingOwner.assertChatAdmission(chatId);
|
|
44949
|
+
if (!getOwnerChat(chatId)?.epochState)
|
|
44950
|
+
await ensureChat(chatId);
|
|
44951
|
+
if (!current())
|
|
44952
|
+
throw new Error("chat unavailable");
|
|
44953
|
+
const reconciliation = await chatListOwner.reconcileChatEpoch(chatId);
|
|
44954
|
+
if (!current() || reconciliation?.removed)
|
|
44955
|
+
throw new Error("chat unavailable");
|
|
44956
|
+
await pendingOwner.assertChatAdmission(chatId);
|
|
44957
|
+
const chat = getVisibleOwnerChat(chatId);
|
|
44958
|
+
if (!current() || !chat?.epochState)
|
|
44959
|
+
throw new Error("chat unavailable");
|
|
44960
|
+
return chat.epochState;
|
|
44961
|
+
};
|
|
44962
|
+
const materializeChat = (chatId) => pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
|
|
44564
44963
|
const flushChatReadFrontier = (chatId) => {
|
|
44565
44964
|
if (!online)
|
|
44566
44965
|
return false;
|
|
@@ -44843,6 +45242,8 @@ function createChatSession({
|
|
|
44843
45242
|
markChatTyping,
|
|
44844
45243
|
hasChat: hasVisibleChat,
|
|
44845
45244
|
getOwnerChat: getVisibleOwnerChat,
|
|
45245
|
+
prepareCall,
|
|
45246
|
+
materializeChat,
|
|
44846
45247
|
sendOptionsForPeer,
|
|
44847
45248
|
getLocalMessages,
|
|
44848
45249
|
wasChatDeletedLocally,
|
|
@@ -44876,6 +45277,7 @@ function createChatSession({
|
|
|
44876
45277
|
deleteMessage,
|
|
44877
45278
|
deleteMessages,
|
|
44878
45279
|
deleteMessageDocs,
|
|
45280
|
+
resolveActiveCall,
|
|
44879
45281
|
setChatTtl,
|
|
44880
45282
|
makeMessagePermanent,
|
|
44881
45283
|
makeMessageTemporary,
|
|
@@ -44933,6 +45335,7 @@ function createChatSession({
|
|
|
44933
45335
|
stateOwner.setListSnapshot(chatListOwner.getSnapshot());
|
|
44934
45336
|
deleteListActionsRef.current = {
|
|
44935
45337
|
removeOwner: (...args) => chatListOwner.removeOwner(...args),
|
|
45338
|
+
leaveOwner: (...args) => chatListOwner.leaveOwner(...args),
|
|
44936
45339
|
render: (...args) => chatListOwner.render(...args)
|
|
44937
45340
|
};
|
|
44938
45341
|
actionOwner.createActionOwners();
|
|
@@ -45654,6 +46057,2942 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
|
|
|
45654
46057
|
});
|
|
45655
46058
|
}
|
|
45656
46059
|
|
|
46060
|
+
// ../../core/calls/protocol.js
|
|
46061
|
+
var CALL_MAX_PARTICIPANTS = 16;
|
|
46062
|
+
var CALL_TTL_MS = 120000;
|
|
46063
|
+
var CALL_ADMISSION_TIMEOUT_MS = 30000;
|
|
46064
|
+
var HEX16 = /^[0-9a-f]{32}$/u;
|
|
46065
|
+
var HEX32 = /^[0-9a-f]{64}$/u;
|
|
46066
|
+
function bytes(packet) {
|
|
46067
|
+
const { signature: _signature, ...body } = packet;
|
|
46068
|
+
return canonicalBytes(["veyl.call.packet.v1", body]);
|
|
46069
|
+
}
|
|
46070
|
+
function createCallProtocol({ realm: realm2, epochState, identity, endpoint, callId = "" }) {
|
|
46071
|
+
const { manifest } = epochState;
|
|
46072
|
+
const members = new Map(manifest.members.map((member) => [member.chatSigningPK, member]));
|
|
46073
|
+
if (!members.has(identity.chatSigningPK))
|
|
46074
|
+
throw new Error("not a chat member");
|
|
46075
|
+
const actor = identity.chatSigningPK;
|
|
46076
|
+
const secret = identity.chatSigningSecret.slice();
|
|
46077
|
+
let closed = false;
|
|
46078
|
+
const context = { v: 1, realm: realm2, chatId: manifest.chatId, epochId: manifest.epochId, callId };
|
|
46079
|
+
return Object.freeze({
|
|
46080
|
+
sign(kind, data) {
|
|
46081
|
+
if (closed)
|
|
46082
|
+
throw new Error("call protocol closed");
|
|
46083
|
+
const packet = {
|
|
46084
|
+
...context,
|
|
46085
|
+
actor,
|
|
46086
|
+
holder: endpoint.holder,
|
|
46087
|
+
endpoint: endpoint.publicKey,
|
|
46088
|
+
nonce: toHex(randomBytes3(16)),
|
|
46089
|
+
at: Date.now(),
|
|
46090
|
+
kind,
|
|
46091
|
+
data
|
|
46092
|
+
};
|
|
46093
|
+
return { ...packet, signature: signChatBytes({ secret, publicKey: actor }, bytes(packet)) };
|
|
46094
|
+
},
|
|
46095
|
+
verify(packet, { fresh = true } = {}) {
|
|
46096
|
+
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)))
|
|
46097
|
+
throw new Error("invalid call participant proof");
|
|
46098
|
+
return members.get(packet.actor);
|
|
46099
|
+
},
|
|
46100
|
+
close() {
|
|
46101
|
+
if (!closed) {
|
|
46102
|
+
closed = true;
|
|
46103
|
+
cleanBytes(secret);
|
|
46104
|
+
}
|
|
46105
|
+
}
|
|
46106
|
+
});
|
|
46107
|
+
}
|
|
46108
|
+
function verifyCallAdmissions(admissions, protocol) {
|
|
46109
|
+
if (!Array.isArray(admissions) || !admissions.length || admissions.length > CALL_MAX_PARTICIPANTS)
|
|
46110
|
+
throw new Error("call membership mismatch");
|
|
46111
|
+
const holders = new Set;
|
|
46112
|
+
const identities = new Set;
|
|
46113
|
+
const members = new Map;
|
|
46114
|
+
for (const admission of admissions) {
|
|
46115
|
+
const identity = protocol.verify(admission, { fresh: false });
|
|
46116
|
+
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))
|
|
46117
|
+
throw new Error("invalid call participant");
|
|
46118
|
+
holders.add(admission.holder);
|
|
46119
|
+
identities.add(admission.actor);
|
|
46120
|
+
members.set(admission.holder, identity);
|
|
46121
|
+
}
|
|
46122
|
+
return members;
|
|
46123
|
+
}
|
|
46124
|
+
function verifyCallMembers(mlsState, admissions, protocol) {
|
|
46125
|
+
const members = verifyCallAdmissions(admissions, protocol);
|
|
46126
|
+
if (mlsState.members.length !== admissions.length)
|
|
46127
|
+
throw new Error("call membership mismatch");
|
|
46128
|
+
for (const admission of admissions) {
|
|
46129
|
+
const member = mlsState.members.find((item) => toHex(item.leafId) === admission.holder);
|
|
46130
|
+
if (!member || toHex(member.signaturePK) !== admission.data.signaturePK)
|
|
46131
|
+
throw new Error("call leaf identity mismatch");
|
|
46132
|
+
}
|
|
46133
|
+
return members;
|
|
46134
|
+
}
|
|
46135
|
+
function verifyCallReplacement(packet, currentHead, protocol) {
|
|
46136
|
+
protocol.verify(packet, { fresh: false });
|
|
46137
|
+
const previous = packet.data?.previous;
|
|
46138
|
+
const replacement = packet.data?.participant;
|
|
46139
|
+
protocol.verify(previous, { fresh: false });
|
|
46140
|
+
if (packet.kind !== "replace" || previous.kind !== "head" || previous.data?.participants?.length !== 1)
|
|
46141
|
+
throw new Error("invalid call replacement");
|
|
46142
|
+
verifyCallAdmissions(previous.data.participants, protocol);
|
|
46143
|
+
verifyCallAdmissions([replacement], protocol);
|
|
46144
|
+
const original = previous.data.participants[0];
|
|
46145
|
+
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)
|
|
46146
|
+
throw new Error("invalid call replacement");
|
|
46147
|
+
if (currentHead && (currentHead.epoch !== previous.data.epoch || currentHead.participants.length !== 1 || currentHead.participants[0].signature !== original.signature))
|
|
46148
|
+
throw new Error("stale call replacement");
|
|
46149
|
+
return { epoch: 0, participants: [replacement] };
|
|
46150
|
+
}
|
|
46151
|
+
|
|
46152
|
+
// ../../core/calls/mailbox.js
|
|
46153
|
+
function openCallMailbox({ cloud, capability, endpoint, protocol, onChange, onError, stream = true }) {
|
|
46154
|
+
let closed = false;
|
|
46155
|
+
let record = null;
|
|
46156
|
+
let candidate = null;
|
|
46157
|
+
let tail = Promise.resolve();
|
|
46158
|
+
let cursor = 0;
|
|
46159
|
+
const decode = async (value, fresh = true, maximum) => {
|
|
46160
|
+
const packet = await capability.open(decodeCallBytes(value, maximum));
|
|
46161
|
+
protocol.verify(packet, { fresh });
|
|
46162
|
+
return packet;
|
|
46163
|
+
};
|
|
46164
|
+
const accept = async (value) => {
|
|
46165
|
+
if (closed)
|
|
46166
|
+
return;
|
|
46167
|
+
if (!Number.isSafeInteger(value?.revision) || !Number.isSafeInteger(value?.cursor) || value.revision < 0 || value.cursor < 0 || !Array.isArray(value.events))
|
|
46168
|
+
throw new Error("invalid call mailbox");
|
|
46169
|
+
if (record && value.revision < record.revision)
|
|
46170
|
+
return;
|
|
46171
|
+
if (value.cursor < cursor)
|
|
46172
|
+
throw new Error("invalid call event order");
|
|
46173
|
+
if (value.events.some((item) => !Number.isSafeInteger(item?.cursor) || item.cursor < 1 || item.cursor > value.cursor))
|
|
46174
|
+
throw new Error("invalid call event order");
|
|
46175
|
+
const empty = value.headDigest === null && value.payload === null && value.proof === null && value.events.length === 0;
|
|
46176
|
+
const unseen = value.events.filter((item) => item.cursor > cursor);
|
|
46177
|
+
const historyLost = !!(!empty && cursor && value.cursor > cursor && (!unseen.length || unseen[0].cursor !== cursor + 1));
|
|
46178
|
+
if (historyLost && (stream || value.truncated !== true))
|
|
46179
|
+
throw new Error("call signaling history unavailable");
|
|
46180
|
+
let head = null;
|
|
46181
|
+
let payload = null;
|
|
46182
|
+
if (value.headDigest !== null) {
|
|
46183
|
+
if (typeof value.headDigest !== "string" || !/^[a-f0-9]{64}$/u.test(value.headDigest))
|
|
46184
|
+
throw new Error("invalid call head digest");
|
|
46185
|
+
const proof = await decode(value.proof, true, CALL_HEAD_PROOF_MAX_BYTES);
|
|
46186
|
+
if (proof.kind !== "head-proof" || proof.data?.digest !== value.headDigest)
|
|
46187
|
+
throw new Error("invalid call head proof");
|
|
46188
|
+
if (record?.headDigest === value.headDigest && record.head) {
|
|
46189
|
+
if (Object.hasOwn(value, "payload") && callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
|
|
46190
|
+
throw new Error("invalid call head digest");
|
|
46191
|
+
head = record.head;
|
|
46192
|
+
payload = record.payload;
|
|
46193
|
+
} else {
|
|
46194
|
+
if (!value.payload || callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
|
|
46195
|
+
throw new Error("missing call head");
|
|
46196
|
+
head = await decode(value.payload, false);
|
|
46197
|
+
if (head.kind !== "head")
|
|
46198
|
+
throw new Error("invalid call head");
|
|
46199
|
+
payload = value.payload;
|
|
46200
|
+
}
|
|
46201
|
+
} else if (value.payload !== null || value.proof !== null)
|
|
46202
|
+
throw new Error("invalid empty call head");
|
|
46203
|
+
const events = [];
|
|
46204
|
+
let nextCursor = empty ? value.cursor : historyLost ? 0 : cursor;
|
|
46205
|
+
for (const item of unseen) {
|
|
46206
|
+
if (!Number.isSafeInteger(item.cursor) || item.cursor < 1 || nextCursor && item.cursor !== nextCursor + 1)
|
|
46207
|
+
throw new Error("invalid call event order");
|
|
46208
|
+
const packet = await decode(item.payload, false, CALL_EVENT_MAX_BYTES);
|
|
46209
|
+
events.push(packet);
|
|
46210
|
+
nextCursor = item.cursor;
|
|
46211
|
+
}
|
|
46212
|
+
if (nextCursor !== value.cursor)
|
|
46213
|
+
throw new Error("call signaling history unavailable");
|
|
46214
|
+
if (closed)
|
|
46215
|
+
return;
|
|
46216
|
+
candidate = { ...value, payload, head, events, historyLost };
|
|
46217
|
+
try {
|
|
46218
|
+
await onChange?.(candidate);
|
|
46219
|
+
if (closed)
|
|
46220
|
+
return;
|
|
46221
|
+
cursor = nextCursor;
|
|
46222
|
+
record = candidate;
|
|
46223
|
+
} finally {
|
|
46224
|
+
candidate = null;
|
|
46225
|
+
}
|
|
46226
|
+
};
|
|
46227
|
+
const consume = (value) => {
|
|
46228
|
+
const result = tail.then(() => accept(value));
|
|
46229
|
+
tail = result.catch((error) => {
|
|
46230
|
+
if (!closed)
|
|
46231
|
+
onError?.(error);
|
|
46232
|
+
});
|
|
46233
|
+
return result;
|
|
46234
|
+
};
|
|
46235
|
+
let channel;
|
|
46236
|
+
try {
|
|
46237
|
+
channel = cloud.calls.connect({
|
|
46238
|
+
capability,
|
|
46239
|
+
endpoint,
|
|
46240
|
+
onSnapshot: consume,
|
|
46241
|
+
onError,
|
|
46242
|
+
stream,
|
|
46243
|
+
getCheckpoint: () => ({ cursor, headDigest: record?.headDigest ?? null })
|
|
46244
|
+
});
|
|
46245
|
+
} catch (error) {
|
|
46246
|
+
capability.close();
|
|
46247
|
+
protocol.close?.();
|
|
46248
|
+
throw error;
|
|
46249
|
+
}
|
|
46250
|
+
async function mutate(headDigest, payload, events, { expectedRevision = (candidate || record)?.revision ?? 0, ttlMs = CALL_TTL_MS } = {}) {
|
|
46251
|
+
if (!stream)
|
|
46252
|
+
throw new Error("read-only call mailbox");
|
|
46253
|
+
const proof = encodeCallBytes(await capability.seal(protocol.sign("head-proof", { digest: headDigest })));
|
|
46254
|
+
const packets = await Promise.all(events.map(async (packet) => encodeCallBytes(await capability.seal(packet))));
|
|
46255
|
+
if (closed)
|
|
46256
|
+
throw new Error("call mailbox closed");
|
|
46257
|
+
const command = {
|
|
46258
|
+
type: "commit",
|
|
46259
|
+
expectedRevision,
|
|
46260
|
+
operation: toHex(randomBytes3(16)),
|
|
46261
|
+
headDigest,
|
|
46262
|
+
proof,
|
|
46263
|
+
...payload !== undefined ? { payload } : {},
|
|
46264
|
+
events: packets,
|
|
46265
|
+
ttlMs
|
|
46266
|
+
};
|
|
46267
|
+
let response;
|
|
46268
|
+
try {
|
|
46269
|
+
response = await channel.request(command);
|
|
46270
|
+
} catch (error) {
|
|
46271
|
+
if (closed || !["calls/connection", "calls/timeout"].includes(error?.code))
|
|
46272
|
+
throw error;
|
|
46273
|
+
response = await channel.request(command);
|
|
46274
|
+
}
|
|
46275
|
+
if (closed)
|
|
46276
|
+
throw new Error("call mailbox closed");
|
|
46277
|
+
consume(response.record).catch(() => {});
|
|
46278
|
+
return response.record;
|
|
46279
|
+
}
|
|
46280
|
+
return Object.freeze({
|
|
46281
|
+
getSnapshot: () => candidate || record,
|
|
46282
|
+
async ready() {
|
|
46283
|
+
const response = await channel.ready();
|
|
46284
|
+
await consume(response.record);
|
|
46285
|
+
return record;
|
|
46286
|
+
},
|
|
46287
|
+
async read() {
|
|
46288
|
+
const response = await channel.request({ type: "read", cursor, headDigest: record?.headDigest ?? null });
|
|
46289
|
+
await consume(response.record);
|
|
46290
|
+
return record;
|
|
46291
|
+
},
|
|
46292
|
+
async commit(head, events = [], options) {
|
|
46293
|
+
const bytes2 = await capability.seal(protocol.sign("head", head));
|
|
46294
|
+
return mutate(callHeadDigest(bytes2), encodeCallBytes(bytes2), events, options);
|
|
46295
|
+
},
|
|
46296
|
+
async append(events, options) {
|
|
46297
|
+
const current = candidate || record;
|
|
46298
|
+
if (!current?.headDigest || !current.head)
|
|
46299
|
+
throw new Error("call ended");
|
|
46300
|
+
return mutate(current.headDigest, undefined, events, options);
|
|
46301
|
+
},
|
|
46302
|
+
close() {
|
|
46303
|
+
closed = true;
|
|
46304
|
+
channel.close();
|
|
46305
|
+
capability.close();
|
|
46306
|
+
protocol.close?.();
|
|
46307
|
+
}
|
|
46308
|
+
});
|
|
46309
|
+
}
|
|
46310
|
+
|
|
46311
|
+
// ../../core/calls/ownership.js
|
|
46312
|
+
var CALL_OWNERSHIP_LEASE_MS = 15000;
|
|
46313
|
+
|
|
46314
|
+
// ../../core/calls/clock.js
|
|
46315
|
+
function callLeaseClock() {
|
|
46316
|
+
return {
|
|
46317
|
+
wall: Date.now(),
|
|
46318
|
+
monotonic: globalThis.performance.timeOrigin + globalThis.performance.now()
|
|
46319
|
+
};
|
|
46320
|
+
}
|
|
46321
|
+
|
|
46322
|
+
// ../../core/calls/leaseattempt.js
|
|
46323
|
+
var STOP_MARGIN_MS = 250;
|
|
46324
|
+
function validTime(value) {
|
|
46325
|
+
return Number.isFinite(value) && value >= 0;
|
|
46326
|
+
}
|
|
46327
|
+
function createCallLeaseAttempt(command, clock = callLeaseClock) {
|
|
46328
|
+
if (!["claim", "activate", "renew"].includes(command?.type))
|
|
46329
|
+
throw new Error("call lease command required");
|
|
46330
|
+
if (command.operation !== undefined)
|
|
46331
|
+
throw new Error("reuse the original call lease attempt");
|
|
46332
|
+
const revision = command.type === "claim" ? command.expectedRevision + 1 : command.revision;
|
|
46333
|
+
const sequence = command.type === "renew" ? command.sequence : 0;
|
|
46334
|
+
if (!/^[a-f0-9]{32}$/u.test(command.holder || "") || !Number.isSafeInteger(revision) || revision < 1 || !Number.isSafeInteger(sequence) || sequence < (command.type === "renew" ? 1 : 0))
|
|
46335
|
+
throw new Error("invalid call lease command");
|
|
46336
|
+
const started = clock();
|
|
46337
|
+
if (!validTime(started?.wall) || !validTime(started?.monotonic))
|
|
46338
|
+
throw new Error("call lease clock unavailable");
|
|
46339
|
+
const deadline = Object.freeze({
|
|
46340
|
+
sessionId: command.holder,
|
|
46341
|
+
revision,
|
|
46342
|
+
wallDeadline: started.wall + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS,
|
|
46343
|
+
monotonicDeadline: started.monotonic + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS
|
|
46344
|
+
});
|
|
46345
|
+
const startWall = started.wall;
|
|
46346
|
+
const startMonotonic = started.monotonic;
|
|
46347
|
+
const operation = toHex(randomBytes3(16));
|
|
46348
|
+
const request = Object.freeze({ ...command, ...command.payload ? { payload: command.payload.slice() } : {}, operation });
|
|
46349
|
+
let cancelled2 = false;
|
|
46350
|
+
return Object.freeze({
|
|
46351
|
+
command: request,
|
|
46352
|
+
cancel() {
|
|
46353
|
+
cancelled2 = true;
|
|
46354
|
+
},
|
|
46355
|
+
grant(record) {
|
|
46356
|
+
if (cancelled2)
|
|
46357
|
+
return null;
|
|
46358
|
+
const now = clock();
|
|
46359
|
+
if (!validTime(now?.wall) || !validTime(now?.monotonic) || now.wall < startWall || now.monotonic < startMonotonic || now.wall >= deadline.wallDeadline || now.monotonic >= deadline.monotonicDeadline) {
|
|
46360
|
+
cancelled2 = true;
|
|
46361
|
+
return null;
|
|
46362
|
+
}
|
|
46363
|
+
const active = record?.active;
|
|
46364
|
+
if (record?.pending || record?.revision !== revision || active?.revision !== revision || active.holder !== deadline.sessionId || active.sequence !== sequence || active.operation !== operation)
|
|
46365
|
+
return null;
|
|
46366
|
+
return deadline;
|
|
46367
|
+
}
|
|
46368
|
+
});
|
|
46369
|
+
}
|
|
46370
|
+
|
|
46371
|
+
// ../../core/calls/accountlease.js
|
|
46372
|
+
var transient = (error) => ["calls/connection", "calls/timeout"].includes(error?.code);
|
|
46373
|
+
function openAccountCallLease({ cloud, capability: accountCapability, endpoint, clock = callLeaseClock, onRecord, onGrant, onLost, onError }) {
|
|
46374
|
+
const capability = accountCapability.retain();
|
|
46375
|
+
let closed = false;
|
|
46376
|
+
let record = null;
|
|
46377
|
+
let owned = null;
|
|
46378
|
+
let ownedDeadline = null;
|
|
46379
|
+
let retiring = null;
|
|
46380
|
+
let renewal = null;
|
|
46381
|
+
let expiry = null;
|
|
46382
|
+
let generation = 0;
|
|
46383
|
+
let acquiring = false;
|
|
46384
|
+
let rotating = Promise.resolve();
|
|
46385
|
+
const attempts = new Set;
|
|
46386
|
+
const waiters = new Set;
|
|
46387
|
+
const current = () => !closed;
|
|
46388
|
+
const wake = () => {
|
|
46389
|
+
for (const resolve of [...waiters])
|
|
46390
|
+
resolve();
|
|
46391
|
+
};
|
|
46392
|
+
function waitForChange(milliseconds) {
|
|
46393
|
+
return new Promise((resolve) => {
|
|
46394
|
+
const finish = () => {
|
|
46395
|
+
clearTimeout(timer);
|
|
46396
|
+
waiters.delete(finish);
|
|
46397
|
+
resolve();
|
|
46398
|
+
};
|
|
46399
|
+
const timer = setTimeout(finish, milliseconds);
|
|
46400
|
+
waiters.add(finish);
|
|
46401
|
+
});
|
|
46402
|
+
}
|
|
46403
|
+
function stop() {
|
|
46404
|
+
generation++;
|
|
46405
|
+
clearTimeout(renewal);
|
|
46406
|
+
clearTimeout(expiry);
|
|
46407
|
+
renewal = expiry = null;
|
|
46408
|
+
for (const attempt of attempts)
|
|
46409
|
+
attempt.cancel();
|
|
46410
|
+
attempts.clear();
|
|
46411
|
+
const previous = owned;
|
|
46412
|
+
owned = ownedDeadline = null;
|
|
46413
|
+
wake();
|
|
46414
|
+
return previous;
|
|
46415
|
+
}
|
|
46416
|
+
async function accept(value) {
|
|
46417
|
+
if (closed)
|
|
46418
|
+
return;
|
|
46419
|
+
const next = decodeCallRecord(value);
|
|
46420
|
+
if (!Number.isSafeInteger(next?.version) || next.version < 0)
|
|
46421
|
+
throw new Error("invalid call ownership version");
|
|
46422
|
+
if (record && next.version <= record.version)
|
|
46423
|
+
return;
|
|
46424
|
+
record = next;
|
|
46425
|
+
wake();
|
|
46426
|
+
if (owned && (next.pending || next.active?.holder !== endpoint.holder || next.active?.revision !== owned.revision)) {
|
|
46427
|
+
retiring = stop();
|
|
46428
|
+
onLost?.();
|
|
46429
|
+
}
|
|
46430
|
+
const active = next.pending || next.active;
|
|
46431
|
+
const descriptor = active ? await capability.open(active.payload) : null;
|
|
46432
|
+
if (current() && record === next)
|
|
46433
|
+
onRecord?.(next, descriptor);
|
|
46434
|
+
}
|
|
46435
|
+
let channel;
|
|
46436
|
+
try {
|
|
46437
|
+
channel = cloud.calls.connect({
|
|
46438
|
+
capability,
|
|
46439
|
+
endpoint,
|
|
46440
|
+
onSnapshot: (value) => accept(value).catch(onError),
|
|
46441
|
+
onError(error) {
|
|
46442
|
+
if (!transient(error))
|
|
46443
|
+
onError?.(error);
|
|
46444
|
+
}
|
|
46445
|
+
});
|
|
46446
|
+
} catch (error) {
|
|
46447
|
+
capability.close();
|
|
46448
|
+
throw error;
|
|
46449
|
+
}
|
|
46450
|
+
async function request(command) {
|
|
46451
|
+
const response = await channel.request({ ...command, ...command.payload ? { payload: encodeCallBytes(command.payload) } : {} });
|
|
46452
|
+
await accept(response.record);
|
|
46453
|
+
return decodeCallRecord(response.record);
|
|
46454
|
+
}
|
|
46455
|
+
function renewalRemaining(started) {
|
|
46456
|
+
if (!current() || started !== generation || !owned || !ownedDeadline)
|
|
46457
|
+
return 0;
|
|
46458
|
+
const now = clock();
|
|
46459
|
+
if (!Number.isFinite(now?.wall) || !Number.isFinite(now?.monotonic) || now.wall < 0 || now.monotonic < 0)
|
|
46460
|
+
return 0;
|
|
46461
|
+
return Math.max(0, Math.min(ownedDeadline.wallDeadline - now.wall, ownedDeadline.monotonicDeadline - now.monotonic));
|
|
46462
|
+
}
|
|
46463
|
+
async function requestGrant(attempt, started) {
|
|
46464
|
+
let delay = 250;
|
|
46465
|
+
for (;; ) {
|
|
46466
|
+
if (attempt.command.type === "renew" && !renewalRemaining(started))
|
|
46467
|
+
throw new Error("call ownership expired");
|
|
46468
|
+
try {
|
|
46469
|
+
const value = await request(attempt.command);
|
|
46470
|
+
if (attempt.command.type === "renew" && !renewalRemaining(started))
|
|
46471
|
+
throw new Error("call ownership expired");
|
|
46472
|
+
return value;
|
|
46473
|
+
} catch (error) {
|
|
46474
|
+
const remaining = renewalRemaining(started);
|
|
46475
|
+
if (attempt.command.type !== "renew" || !transient(error) || !remaining)
|
|
46476
|
+
throw error;
|
|
46477
|
+
await waitForChange(Math.min(delay, remaining));
|
|
46478
|
+
delay = Math.min(delay * 2, 1000);
|
|
46479
|
+
}
|
|
46480
|
+
}
|
|
46481
|
+
}
|
|
46482
|
+
async function grant(command) {
|
|
46483
|
+
if (closed)
|
|
46484
|
+
throw new Error("call lease closed");
|
|
46485
|
+
const requestedAt = clock();
|
|
46486
|
+
const attempt = createCallLeaseAttempt(command, clock);
|
|
46487
|
+
const started = generation;
|
|
46488
|
+
attempts.add(attempt);
|
|
46489
|
+
try {
|
|
46490
|
+
const value = await requestGrant(attempt, started);
|
|
46491
|
+
const deadline = attempt.grant(value);
|
|
46492
|
+
if (!current() || started !== generation || !deadline || record.version !== value.version)
|
|
46493
|
+
throw new Error("call ownership unavailable");
|
|
46494
|
+
owned = value.active;
|
|
46495
|
+
await onGrant(deadline);
|
|
46496
|
+
if (!current() || owned !== value.active)
|
|
46497
|
+
throw new Error("call ownership changed");
|
|
46498
|
+
ownedDeadline = deadline;
|
|
46499
|
+
clearTimeout(expiry);
|
|
46500
|
+
expiry = setTimeout(() => {
|
|
46501
|
+
stop();
|
|
46502
|
+
onLost?.();
|
|
46503
|
+
}, Math.max(0, deadline.wallDeadline - Date.now()));
|
|
46504
|
+
clearTimeout(renewal);
|
|
46505
|
+
const now = clock();
|
|
46506
|
+
const elapsed = Math.max(now.wall - requestedAt.wall, now.monotonic - requestedAt.monotonic);
|
|
46507
|
+
renewal = setTimeout(() => {
|
|
46508
|
+
if (!owned)
|
|
46509
|
+
return;
|
|
46510
|
+
const renewing = generation;
|
|
46511
|
+
grant({ type: "renew", holder: endpoint.holder, revision: owned.revision, sequence: owned.sequence + 1 }).catch((error) => {
|
|
46512
|
+
if (closed || renewing !== generation)
|
|
46513
|
+
return;
|
|
46514
|
+
stop();
|
|
46515
|
+
onLost?.();
|
|
46516
|
+
onError?.(error);
|
|
46517
|
+
});
|
|
46518
|
+
}, Math.max(0, 5000 - elapsed));
|
|
46519
|
+
return value;
|
|
46520
|
+
} finally {
|
|
46521
|
+
attempts.delete(attempt);
|
|
46522
|
+
attempt.cancel();
|
|
46523
|
+
}
|
|
46524
|
+
}
|
|
46525
|
+
async function rotateMedia() {
|
|
46526
|
+
if (closed || acquiring || !owned || !renewalRemaining(generation))
|
|
46527
|
+
throw new Error("call ownership unavailable");
|
|
46528
|
+
const previous = stop();
|
|
46529
|
+
const started = generation;
|
|
46530
|
+
await request({ type: "release", holder: endpoint.holder, revision: previous.revision });
|
|
46531
|
+
if (closed || started !== generation)
|
|
46532
|
+
throw new Error("call transfer cancelled");
|
|
46533
|
+
return grant({
|
|
46534
|
+
type: "claim",
|
|
46535
|
+
holder: endpoint.holder,
|
|
46536
|
+
expectedRevision: previous.revision,
|
|
46537
|
+
payload: previous.payload,
|
|
46538
|
+
takeover: false
|
|
46539
|
+
});
|
|
46540
|
+
}
|
|
46541
|
+
return Object.freeze({
|
|
46542
|
+
rotateMedia() {
|
|
46543
|
+
const next = rotating.then(rotateMedia);
|
|
46544
|
+
rotating = next.catch(() => {});
|
|
46545
|
+
return next;
|
|
46546
|
+
},
|
|
46547
|
+
async acquire(descriptor) {
|
|
46548
|
+
if (closed)
|
|
46549
|
+
throw new Error("call lease closed");
|
|
46550
|
+
if (owned || acquiring)
|
|
46551
|
+
throw new Error("already in a call");
|
|
46552
|
+
acquiring = true;
|
|
46553
|
+
const started = generation;
|
|
46554
|
+
try {
|
|
46555
|
+
await accept((await channel.ready()).record);
|
|
46556
|
+
const payload = await capability.seal(descriptor);
|
|
46557
|
+
if (closed || started !== generation)
|
|
46558
|
+
throw new Error("call transfer cancelled");
|
|
46559
|
+
const claim = { type: "claim", holder: endpoint.holder, expectedRevision: record.revision, payload, takeover: true };
|
|
46560
|
+
if (!record.active && !record.pending)
|
|
46561
|
+
return await grant(claim);
|
|
46562
|
+
const pending = createCallLeaseAttempt(claim, clock);
|
|
46563
|
+
attempts.add(pending);
|
|
46564
|
+
try {
|
|
46565
|
+
await request(pending.command);
|
|
46566
|
+
const until = Date.now() + 20000;
|
|
46567
|
+
while (current() && started === generation && record.pending?.holder === endpoint.holder && record.active && Date.now() < until) {
|
|
46568
|
+
await waitForChange(Math.max(1, Math.min(until - Date.now(), record.active.expiresAt - Date.now() + 50)));
|
|
46569
|
+
if (current() && started === generation && record.active?.expiresAt <= Date.now())
|
|
46570
|
+
await request({ type: "read" });
|
|
46571
|
+
}
|
|
46572
|
+
if (!current() || started !== generation || record.pending?.holder !== endpoint.holder)
|
|
46573
|
+
throw new Error("call transfer cancelled");
|
|
46574
|
+
return await grant({ type: "activate", holder: endpoint.holder, revision: record.pending.revision });
|
|
46575
|
+
} finally {
|
|
46576
|
+
attempts.delete(pending);
|
|
46577
|
+
pending.cancel();
|
|
46578
|
+
}
|
|
46579
|
+
} finally {
|
|
46580
|
+
acquiring = false;
|
|
46581
|
+
}
|
|
46582
|
+
},
|
|
46583
|
+
provider(action, payload) {
|
|
46584
|
+
if (!owned || closed)
|
|
46585
|
+
return Promise.reject(new Error("call ownership required"));
|
|
46586
|
+
return channel.provider({ holder: endpoint.holder, revision: owned.revision, action, payload }).then((result) => result.record);
|
|
46587
|
+
},
|
|
46588
|
+
async release() {
|
|
46589
|
+
const previous = stop() || retiring;
|
|
46590
|
+
retiring = null;
|
|
46591
|
+
if (previous && !closed)
|
|
46592
|
+
await request({ type: "release", holder: endpoint.holder, revision: previous.revision });
|
|
46593
|
+
else if (record?.pending?.holder === endpoint.holder && !closed)
|
|
46594
|
+
await request({ type: "cancel", holder: endpoint.holder, revision: record.pending.revision });
|
|
46595
|
+
},
|
|
46596
|
+
retire() {
|
|
46597
|
+
retiring = stop() || retiring;
|
|
46598
|
+
},
|
|
46599
|
+
close() {
|
|
46600
|
+
closed = true;
|
|
46601
|
+
stop();
|
|
46602
|
+
channel.close();
|
|
46603
|
+
capability.close();
|
|
46604
|
+
}
|
|
46605
|
+
});
|
|
46606
|
+
}
|
|
46607
|
+
|
|
46608
|
+
// ../../core/calls/room.js
|
|
46609
|
+
function openCallRoom({
|
|
46610
|
+
cloud,
|
|
46611
|
+
capability,
|
|
46612
|
+
protocol,
|
|
46613
|
+
endpoint,
|
|
46614
|
+
mls,
|
|
46615
|
+
member,
|
|
46616
|
+
admission,
|
|
46617
|
+
callId,
|
|
46618
|
+
onKeys,
|
|
46619
|
+
onParticipants,
|
|
46620
|
+
onSignal,
|
|
46621
|
+
onRemoved,
|
|
46622
|
+
onError
|
|
46623
|
+
}) {
|
|
46624
|
+
let closed = false;
|
|
46625
|
+
let state = null;
|
|
46626
|
+
let head = null;
|
|
46627
|
+
let verifiedHead = null;
|
|
46628
|
+
let admittedIdentities = new Map;
|
|
46629
|
+
let pulseTimer = null;
|
|
46630
|
+
let changing = false;
|
|
46631
|
+
let reconcileAgain = false;
|
|
46632
|
+
let joined = false;
|
|
46633
|
+
let departing = false;
|
|
46634
|
+
let pendingCandidate = null;
|
|
46635
|
+
let ownMedia = null;
|
|
46636
|
+
let audio = { muted: false, deafened: false };
|
|
46637
|
+
let startTask = null;
|
|
46638
|
+
let startPromise = null;
|
|
46639
|
+
let admissionTimer = null;
|
|
46640
|
+
let admissionResult = null;
|
|
46641
|
+
let admissionFailure = null;
|
|
46642
|
+
let pendingInitial = null;
|
|
46643
|
+
let submittedJoin = false;
|
|
46644
|
+
let predecessorHolder = null;
|
|
46645
|
+
let eventTail = Promise.resolve();
|
|
46646
|
+
let retryWait = null;
|
|
46647
|
+
const pendingJoins = new Map;
|
|
46648
|
+
const leaving = new Set;
|
|
46649
|
+
const seen = new Map;
|
|
46650
|
+
const published = new Map;
|
|
46651
|
+
const localHolder = endpoint.holder;
|
|
46652
|
+
function disposeState(value) {
|
|
46653
|
+
cleanBytes(value?.snapshot, value?.baseKey);
|
|
46654
|
+
}
|
|
46655
|
+
const alive = (holder) => !leaving.has(holder) && Date.now() - (seen.get(holder) || 0) < 30000;
|
|
46656
|
+
const replacementFor = (item) => [...pendingJoins.values()].find((packet) => packet.actor === item.actor && packet.holder !== item.holder && packet.data.replaces === item.holder && alive(packet.holder));
|
|
46657
|
+
const leader = () => {
|
|
46658
|
+
const candidates = head?.participants.filter((item) => alive(item.holder)) || [];
|
|
46659
|
+
return (candidates.filter((item) => !replacementFor(item)).length ? candidates.filter((item) => !replacementFor(item)) : candidates).map((item) => item.holder).sort()[0];
|
|
46660
|
+
};
|
|
46661
|
+
function admissionError(code, message) {
|
|
46662
|
+
return Object.assign(new Error(message), { code });
|
|
46663
|
+
}
|
|
46664
|
+
const cancelled2 = () => admissionError("calls/cancelled", "call cancelled");
|
|
46665
|
+
const empty = () => admissionError("calls/room-empty", "this call is empty");
|
|
46666
|
+
function settleAdmission(error) {
|
|
46667
|
+
if (error)
|
|
46668
|
+
admissionFailure ||= error;
|
|
46669
|
+
if (!admissionResult)
|
|
46670
|
+
return;
|
|
46671
|
+
const result = admissionResult;
|
|
46672
|
+
admissionResult = null;
|
|
46673
|
+
clearTimeout(admissionTimer);
|
|
46674
|
+
admissionTimer = null;
|
|
46675
|
+
if (error)
|
|
46676
|
+
result.reject(error);
|
|
46677
|
+
else
|
|
46678
|
+
result.resolve();
|
|
46679
|
+
}
|
|
46680
|
+
function checkAdmission() {
|
|
46681
|
+
if (closed || departing || !head)
|
|
46682
|
+
return;
|
|
46683
|
+
if (!joined && head.participants.every((item) => leaving.has(item.holder)))
|
|
46684
|
+
settleAdmission(empty());
|
|
46685
|
+
else if (joined && head.participants.some((item) => item.holder === localHolder && item.actor === admission.actor))
|
|
46686
|
+
settleAdmission();
|
|
46687
|
+
else if (!joined && head.participants.length >= CALL_MAX_PARTICIPANTS && !head.participants.some((item) => item.actor === admission.actor)) {
|
|
46688
|
+
settleAdmission(admissionError("calls/full", "this call is full"));
|
|
46689
|
+
} else if (!joined && submittedJoin && head.participants.some((item) => item.actor === admission.actor && item.holder !== localHolder && item.holder !== predecessorHolder)) {
|
|
46690
|
+
settleAdmission(admissionError("calls/taken-over", "call moved to another device"));
|
|
46691
|
+
}
|
|
46692
|
+
}
|
|
46693
|
+
async function adopt(next, admissions) {
|
|
46694
|
+
verifyCallMembers(next, admissions, protocol);
|
|
46695
|
+
if (closed) {
|
|
46696
|
+
disposeState(next);
|
|
46697
|
+
return;
|
|
46698
|
+
}
|
|
46699
|
+
const previous = state;
|
|
46700
|
+
state = next;
|
|
46701
|
+
disposeState(previous);
|
|
46702
|
+
const self = next.members.find((item) => toHex(item.leafId) === localHolder);
|
|
46703
|
+
if (!self) {
|
|
46704
|
+
onRemoved?.();
|
|
46705
|
+
return;
|
|
46706
|
+
}
|
|
46707
|
+
if (!admissions.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey))
|
|
46708
|
+
throw new Error("call endpoint identity mismatch");
|
|
46709
|
+
joined = true;
|
|
46710
|
+
await onKeys({ baseKey: next.baseKey, epoch: next.epoch, members: next.members, leafIndex: self.leafIndex });
|
|
46711
|
+
}
|
|
46712
|
+
function participants() {
|
|
46713
|
+
return (head?.participants || []).filter((item) => !leaving.has(item.holder)).map((item) => ({
|
|
46714
|
+
...admittedIdentities.get(item.holder),
|
|
46715
|
+
holder: item.holder,
|
|
46716
|
+
muted: published.get(item.holder)?.muted === true || published.get(item.holder)?.deafened === true,
|
|
46717
|
+
deafened: published.get(item.holder)?.deafened === true,
|
|
46718
|
+
media: published.get(item.holder)?.media || null
|
|
46719
|
+
}));
|
|
46720
|
+
}
|
|
46721
|
+
function prune() {
|
|
46722
|
+
const admitted = new Set(head?.participants.map((item) => item.holder));
|
|
46723
|
+
for (const [holder, packet] of pendingJoins) {
|
|
46724
|
+
const predecessor = head?.participants.find((item) => item.actor === packet.actor);
|
|
46725
|
+
if (admitted.has(holder) || Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS || predecessor && packet.data.replaces !== predecessor.holder || !predecessor && packet.data.replaces)
|
|
46726
|
+
pendingJoins.delete(holder);
|
|
46727
|
+
}
|
|
46728
|
+
for (const holder of seen.keys())
|
|
46729
|
+
if (!admitted.has(holder) && !pendingJoins.has(holder))
|
|
46730
|
+
seen.delete(holder);
|
|
46731
|
+
for (const holder of published.keys())
|
|
46732
|
+
if (!admitted.has(holder))
|
|
46733
|
+
published.delete(holder);
|
|
46734
|
+
for (const holder of leaving)
|
|
46735
|
+
if (!admitted.has(holder))
|
|
46736
|
+
leaving.delete(holder);
|
|
46737
|
+
}
|
|
46738
|
+
async function consume(record) {
|
|
46739
|
+
if (closed)
|
|
46740
|
+
return;
|
|
46741
|
+
prune();
|
|
46742
|
+
const nextHead = record.head?.data;
|
|
46743
|
+
const signals = [];
|
|
46744
|
+
let membershipChanged = false;
|
|
46745
|
+
if (nextHead && (!Array.isArray(nextHead.participants) || nextHead.participants.length > CALL_MAX_PARTICIPANTS))
|
|
46746
|
+
throw new Error("invalid call room");
|
|
46747
|
+
for (const packet of record.events) {
|
|
46748
|
+
const roster = head?.participants || nextHead?.participants || [];
|
|
46749
|
+
const bound = roster.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint);
|
|
46750
|
+
const pending = pendingJoins.get(packet.holder);
|
|
46751
|
+
const pendingBound = pending?.actor === packet.actor && pending?.endpoint === packet.endpoint;
|
|
46752
|
+
if (!["join", "replace"].includes(packet.kind) && !bound && !(packet.kind === "leave" && pendingBound))
|
|
46753
|
+
continue;
|
|
46754
|
+
if (packet.kind === "join" && roster.some((item) => item.holder === packet.holder) && !bound)
|
|
46755
|
+
continue;
|
|
46756
|
+
if (packet.kind === "join") {
|
|
46757
|
+
if (Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS)
|
|
46758
|
+
continue;
|
|
46759
|
+
const predecessor = roster.find((item) => item.actor === packet.actor && item.holder !== packet.holder);
|
|
46760
|
+
if (predecessor && packet.data.replaces !== predecessor.holder)
|
|
46761
|
+
continue;
|
|
46762
|
+
if (!predecessor && packet.data.replaces && !bound)
|
|
46763
|
+
continue;
|
|
46764
|
+
const previous = [...pendingJoins.values()].find((item) => item.actor === packet.actor && item.holder !== packet.holder);
|
|
46765
|
+
if (previous) {
|
|
46766
|
+
pendingJoins.delete(previous.holder);
|
|
46767
|
+
seen.delete(previous.holder);
|
|
46768
|
+
}
|
|
46769
|
+
if (!pendingJoins.has(packet.holder) && pendingJoins.size >= CALL_MAX_PARTICIPANTS)
|
|
46770
|
+
continue;
|
|
46771
|
+
pendingJoins.set(packet.holder, packet);
|
|
46772
|
+
} else if (packet.kind === "leave") {
|
|
46773
|
+
if (pendingBound)
|
|
46774
|
+
pendingJoins.delete(packet.holder);
|
|
46775
|
+
if (bound)
|
|
46776
|
+
leaving.add(packet.holder);
|
|
46777
|
+
} else if (packet.kind === "pulse") {
|
|
46778
|
+
published.set(packet.holder, packet.data);
|
|
46779
|
+
} else if (packet.kind === "signal") {
|
|
46780
|
+
if (packet.data.to === localHolder && head?.participants.some((item) => item.holder === packet.holder && item.actor === packet.actor)) {
|
|
46781
|
+
signals.push(packet);
|
|
46782
|
+
}
|
|
46783
|
+
} else if (packet.kind === "replace") {
|
|
46784
|
+
const replacement = verifyCallReplacement(packet, head, protocol);
|
|
46785
|
+
membershipChanged = true;
|
|
46786
|
+
if (state && !replacement.participants.some((item) => item.holder === localHolder)) {
|
|
46787
|
+
settleAdmission(cancelled2());
|
|
46788
|
+
onRemoved?.();
|
|
46789
|
+
return;
|
|
46790
|
+
}
|
|
46791
|
+
head = replacement;
|
|
46792
|
+
} else if (packet.kind === "change") {
|
|
46793
|
+
membershipChanged = true;
|
|
46794
|
+
const change = packet.data;
|
|
46795
|
+
if (!Array.isArray(change.participants) || !Number.isSafeInteger(change.epoch))
|
|
46796
|
+
throw new Error("invalid call epoch");
|
|
46797
|
+
if (state && change.epoch === state.epoch + 1) {
|
|
46798
|
+
if (!bound)
|
|
46799
|
+
throw new Error("unadmitted call commit");
|
|
46800
|
+
const next = pendingCandidate?.commit === change.commit ? pendingCandidate.state : await mls.processCommit(state.snapshot, decodeCallBytes(change.commit));
|
|
46801
|
+
pendingCandidate = null;
|
|
46802
|
+
if (next.removed) {
|
|
46803
|
+
disposeState(next);
|
|
46804
|
+
onRemoved?.();
|
|
46805
|
+
return;
|
|
46806
|
+
}
|
|
46807
|
+
await adopt(next, change.participants);
|
|
46808
|
+
} else if (!state && change.participants.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey)) {
|
|
46809
|
+
const next = await mls.join(member.snapshot, decodeCallBytes(change.welcome), decodeCallBytes(change.tree), fromHexBytes(callId));
|
|
46810
|
+
await adopt(next, change.participants);
|
|
46811
|
+
}
|
|
46812
|
+
head = { epoch: change.epoch, participants: change.participants };
|
|
46813
|
+
for (const item of change.participants) {
|
|
46814
|
+
pendingJoins.delete(item.holder);
|
|
46815
|
+
if (!seen.has(item.holder))
|
|
46816
|
+
seen.set(item.holder, packet.at);
|
|
46817
|
+
}
|
|
46818
|
+
}
|
|
46819
|
+
seen.set(packet.holder, Math.max(seen.get(packet.holder) || 0, packet.at));
|
|
46820
|
+
}
|
|
46821
|
+
if (!state && pendingInitial && nextHead?.epoch === 0 && nextHead.participants.length === 1 && nextHead.participants[0].signature === pendingInitial.admission.signature) {
|
|
46822
|
+
const initial = pendingInitial.state;
|
|
46823
|
+
pendingInitial = null;
|
|
46824
|
+
await adopt(initial, nextHead.participants);
|
|
46825
|
+
}
|
|
46826
|
+
if (nextHead && (verifiedHead !== record.head || membershipChanged)) {
|
|
46827
|
+
admittedIdentities = state ? verifyCallMembers(state, nextHead.participants, protocol) : verifyCallAdmissions(nextHead.participants, protocol);
|
|
46828
|
+
for (const item of nextHead.participants) {
|
|
46829
|
+
if (!seen.has(item.holder))
|
|
46830
|
+
seen.set(item.holder, record.head.at);
|
|
46831
|
+
}
|
|
46832
|
+
if (state && nextHead.epoch !== state.epoch)
|
|
46833
|
+
throw new Error("call epoch history unavailable");
|
|
46834
|
+
head = nextHead;
|
|
46835
|
+
verifiedHead = record.head;
|
|
46836
|
+
} else if (!nextHead) {
|
|
46837
|
+
head = null;
|
|
46838
|
+
verifiedHead = null;
|
|
46839
|
+
admittedIdentities.clear();
|
|
46840
|
+
if (joined) {
|
|
46841
|
+
onRemoved?.();
|
|
46842
|
+
return;
|
|
46843
|
+
}
|
|
46844
|
+
if (submittedJoin)
|
|
46845
|
+
settleAdmission(empty());
|
|
46846
|
+
}
|
|
46847
|
+
prune();
|
|
46848
|
+
if (closed)
|
|
46849
|
+
return;
|
|
46850
|
+
onParticipants?.(participants());
|
|
46851
|
+
checkAdmission();
|
|
46852
|
+
for (const packet of signals) {
|
|
46853
|
+
try {
|
|
46854
|
+
Promise.resolve(onSignal?.(packet.data.signal, packet.holder)).catch(fail);
|
|
46855
|
+
} catch (error) {
|
|
46856
|
+
fail(error);
|
|
46857
|
+
}
|
|
46858
|
+
}
|
|
46859
|
+
reconcile().catch(fail);
|
|
46860
|
+
}
|
|
46861
|
+
function fail(error) {
|
|
46862
|
+
if (!closed) {
|
|
46863
|
+
if (admissionResult)
|
|
46864
|
+
settleAdmission(error);
|
|
46865
|
+
onError?.(error);
|
|
46866
|
+
}
|
|
46867
|
+
}
|
|
46868
|
+
const mailbox = openCallMailbox({ cloud, capability, protocol, endpoint, onChange: consume, onError: fail });
|
|
46869
|
+
async function reconcile() {
|
|
46870
|
+
reconcileAgain = true;
|
|
46871
|
+
if (changing)
|
|
46872
|
+
return;
|
|
46873
|
+
changing = true;
|
|
46874
|
+
try {
|
|
46875
|
+
do {
|
|
46876
|
+
reconcileAgain = false;
|
|
46877
|
+
await reconcilePass();
|
|
46878
|
+
} while (reconcileAgain && !closed);
|
|
46879
|
+
} finally {
|
|
46880
|
+
changing = false;
|
|
46881
|
+
}
|
|
46882
|
+
}
|
|
46883
|
+
async function reconcilePass() {
|
|
46884
|
+
if (closed || departing || !state || leader() !== localHolder)
|
|
46885
|
+
return;
|
|
46886
|
+
const current = mailbox.getSnapshot();
|
|
46887
|
+
if (!current || !head)
|
|
46888
|
+
return;
|
|
46889
|
+
const removals = head.participants.filter((item) => item.holder !== localHolder && (!alive(item.holder) || replacementFor(item)));
|
|
46890
|
+
const retained = head.participants.filter((item) => !removals.includes(item));
|
|
46891
|
+
const admittedActors = new Set(retained.map((item) => item.actor));
|
|
46892
|
+
const additions = [...pendingJoins.values()].filter((item) => {
|
|
46893
|
+
if (head.participants.some((existing) => existing.holder === item.holder) || admittedActors.has(item.actor) || !alive(item.holder))
|
|
46894
|
+
return false;
|
|
46895
|
+
admittedActors.add(item.actor);
|
|
46896
|
+
return true;
|
|
46897
|
+
}).slice(0, CALL_MAX_PARTICIPANTS - retained.length);
|
|
46898
|
+
if (!removals.length && !additions.length)
|
|
46899
|
+
return;
|
|
46900
|
+
let candidate;
|
|
46901
|
+
try {
|
|
46902
|
+
for (const item of additions)
|
|
46903
|
+
protocol.verify(item);
|
|
46904
|
+
candidate = await mls.stageChange(state.snapshot, {
|
|
46905
|
+
adds: additions.map((item) => decodeCallBytes(item.data.keyPackage)),
|
|
46906
|
+
removes: removals.map((item) => fromHexBytes(item.holder))
|
|
46907
|
+
});
|
|
46908
|
+
if (closed || departing) {
|
|
46909
|
+
disposeState(candidate);
|
|
46910
|
+
return;
|
|
46911
|
+
}
|
|
46912
|
+
const admissions = [...retained, ...additions];
|
|
46913
|
+
verifyCallMembers(candidate, admissions, protocol);
|
|
46914
|
+
const change = {
|
|
46915
|
+
epoch: candidate.epoch,
|
|
46916
|
+
participants: admissions,
|
|
46917
|
+
commit: encodeCallBytes(candidate.commit),
|
|
46918
|
+
welcome: encodeCallBytes(candidate.welcome),
|
|
46919
|
+
tree: encodeCallBytes(candidate.tree)
|
|
46920
|
+
};
|
|
46921
|
+
pendingCandidate = { commit: change.commit, state: candidate };
|
|
46922
|
+
await mailbox.commit({ epoch: candidate.epoch, participants: admissions }, [protocol.sign("change", change)], { expectedRevision: current.revision });
|
|
46923
|
+
candidate = null;
|
|
46924
|
+
} catch (error) {
|
|
46925
|
+
if (pendingCandidate?.state === candidate)
|
|
46926
|
+
pendingCandidate = null;
|
|
46927
|
+
disposeState(candidate);
|
|
46928
|
+
if (String(error.code).endsWith("conflict")) {
|
|
46929
|
+
await mailbox.read();
|
|
46930
|
+
return;
|
|
46931
|
+
}
|
|
46932
|
+
throw error;
|
|
46933
|
+
}
|
|
46934
|
+
}
|
|
46935
|
+
function event(kind, data) {
|
|
46936
|
+
const result = eventTail.then(() => appendEvent(kind, data));
|
|
46937
|
+
eventTail = result.catch(() => {});
|
|
46938
|
+
return result;
|
|
46939
|
+
}
|
|
46940
|
+
async function appendEvent(kind, data) {
|
|
46941
|
+
const deadline = performance.now() + CALL_ADMISSION_TIMEOUT_MS;
|
|
46942
|
+
for (let attempt = 0;!closed && performance.now() < deadline; attempt += 1) {
|
|
46943
|
+
if (kind === "join" && departing)
|
|
46944
|
+
throw cancelled2();
|
|
46945
|
+
const current = mailbox.getSnapshot();
|
|
46946
|
+
if (!current?.head)
|
|
46947
|
+
throw kind === "join" ? empty() : new Error("call ended");
|
|
46948
|
+
if (kind === "join") {
|
|
46949
|
+
checkAdmission();
|
|
46950
|
+
if (admissionFailure)
|
|
46951
|
+
throw admissionFailure;
|
|
46952
|
+
}
|
|
46953
|
+
try {
|
|
46954
|
+
if (kind === "join")
|
|
46955
|
+
submittedJoin = true;
|
|
46956
|
+
return await mailbox.append([protocol.sign(kind, data)], { expectedRevision: current.revision });
|
|
46957
|
+
} catch (error) {
|
|
46958
|
+
if (closed)
|
|
46959
|
+
throw cancelled2();
|
|
46960
|
+
if (!String(error.code).endsWith("conflict"))
|
|
46961
|
+
throw error;
|
|
46962
|
+
const delay = Math.min(250, 25 * 2 ** Math.min(attempt, 4)) * (0.5 + Math.random() / 2);
|
|
46963
|
+
await new Promise((resolve) => {
|
|
46964
|
+
const timer = setTimeout(() => {
|
|
46965
|
+
retryWait = null;
|
|
46966
|
+
resolve();
|
|
46967
|
+
}, delay);
|
|
46968
|
+
retryWait = () => {
|
|
46969
|
+
clearTimeout(timer);
|
|
46970
|
+
retryWait = null;
|
|
46971
|
+
resolve();
|
|
46972
|
+
};
|
|
46973
|
+
});
|
|
46974
|
+
if (closed)
|
|
46975
|
+
throw cancelled2();
|
|
46976
|
+
await mailbox.read();
|
|
46977
|
+
}
|
|
46978
|
+
}
|
|
46979
|
+
if (closed)
|
|
46980
|
+
throw cancelled2();
|
|
46981
|
+
throw admissionError("calls/signaling-conflict", "call changed, try again");
|
|
46982
|
+
}
|
|
46983
|
+
async function pulse() {
|
|
46984
|
+
if (closed || departing || !joined)
|
|
46985
|
+
return;
|
|
46986
|
+
try {
|
|
46987
|
+
await event("pulse", { ...audio, media: ownMedia });
|
|
46988
|
+
} catch (error) {
|
|
46989
|
+
fail(error);
|
|
46990
|
+
}
|
|
46991
|
+
if (!closed && !departing)
|
|
46992
|
+
pulseTimer = setTimeout(pulse, 1e4);
|
|
46993
|
+
}
|
|
46994
|
+
async function beginStart(create) {
|
|
46995
|
+
const record = await mailbox.ready();
|
|
46996
|
+
if (closed || departing)
|
|
46997
|
+
throw cancelled2();
|
|
46998
|
+
if (!create && !record?.head)
|
|
46999
|
+
throw empty();
|
|
47000
|
+
checkAdmission();
|
|
47001
|
+
if (admissionFailure)
|
|
47002
|
+
throw admissionFailure;
|
|
47003
|
+
const solePredecessor = record?.head?.data.participants.length === 1 && record.head.data.participants[0].actor === admission.actor && record.head.data.participants[0].holder !== localHolder;
|
|
47004
|
+
if (create || solePredecessor) {
|
|
47005
|
+
if (create && record?.head)
|
|
47006
|
+
throw new Error("call already exists");
|
|
47007
|
+
let initialMember = member;
|
|
47008
|
+
let initialAdmission = admission;
|
|
47009
|
+
if (solePredecessor)
|
|
47010
|
+
initialMember = await mls.createMember(fromHexBytes(localHolder));
|
|
47011
|
+
let initial;
|
|
47012
|
+
try {
|
|
47013
|
+
if (closed || departing)
|
|
47014
|
+
throw cancelled2();
|
|
47015
|
+
if (solePredecessor)
|
|
47016
|
+
initialAdmission = protocol.sign("join", {
|
|
47017
|
+
signaturePK: toHex(initialMember.signaturePK),
|
|
47018
|
+
keyPackage: "",
|
|
47019
|
+
replaces: record.head.data.participants[0].holder
|
|
47020
|
+
});
|
|
47021
|
+
initial = await mls.createGroup(initialMember.snapshot, fromHexBytes(callId));
|
|
47022
|
+
} finally {
|
|
47023
|
+
if (solePredecessor)
|
|
47024
|
+
disposeState(initialMember);
|
|
47025
|
+
}
|
|
47026
|
+
if (closed || departing) {
|
|
47027
|
+
disposeState(initial);
|
|
47028
|
+
throw cancelled2();
|
|
47029
|
+
}
|
|
47030
|
+
if (admissionFailure) {
|
|
47031
|
+
disposeState(initial);
|
|
47032
|
+
throw admissionFailure;
|
|
47033
|
+
}
|
|
47034
|
+
pendingInitial = { state: initial, admission: initialAdmission };
|
|
47035
|
+
const initialHead = { epoch: initial.epoch, participants: [initialAdmission] };
|
|
47036
|
+
const packet = solePredecessor ? protocol.sign("replace", { previous: record.head, participant: initialAdmission }) : protocol.sign("pulse", { ...audio, media: null });
|
|
47037
|
+
submittedJoin = true;
|
|
47038
|
+
await mailbox.commit(initialHead, [packet], { expectedRevision: record.revision });
|
|
47039
|
+
} else {
|
|
47040
|
+
const predecessor = head.participants.find((item) => item.actor === admission.actor);
|
|
47041
|
+
predecessorHolder = predecessor?.holder || null;
|
|
47042
|
+
await event("join", { ...admission.data, replaces: predecessor?.holder || null });
|
|
47043
|
+
checkAdmission();
|
|
47044
|
+
}
|
|
47045
|
+
}
|
|
47046
|
+
return Object.freeze({
|
|
47047
|
+
start({ create = false } = {}) {
|
|
47048
|
+
if (startPromise)
|
|
47049
|
+
return startPromise;
|
|
47050
|
+
const admitted = new Promise((resolve, reject) => {
|
|
47051
|
+
admissionResult = { resolve, reject };
|
|
47052
|
+
});
|
|
47053
|
+
admissionTimer = setTimeout(() => settleAdmission(admissionError("calls/admission-timeout", "could not join the call")), CALL_ADMISSION_TIMEOUT_MS);
|
|
47054
|
+
startTask = beginStart(create);
|
|
47055
|
+
startPromise = Promise.race([startTask.then(() => admitted), admitted]).then(() => {
|
|
47056
|
+
if (closed || departing)
|
|
47057
|
+
throw cancelled2();
|
|
47058
|
+
pulseTimer = setTimeout(pulse, 1e4);
|
|
47059
|
+
}).catch((error) => {
|
|
47060
|
+
settleAdmission(error);
|
|
47061
|
+
throw error;
|
|
47062
|
+
});
|
|
47063
|
+
return startPromise;
|
|
47064
|
+
},
|
|
47065
|
+
signal(to, signal) {
|
|
47066
|
+
return event("signal", { to, signal });
|
|
47067
|
+
},
|
|
47068
|
+
async publishMedia(media) {
|
|
47069
|
+
ownMedia = media;
|
|
47070
|
+
await event("pulse", { ...audio, media });
|
|
47071
|
+
},
|
|
47072
|
+
async setAudio(value) {
|
|
47073
|
+
audio = { muted: value.muted === true || value.deafened === true, deafened: value.deafened === true };
|
|
47074
|
+
if (joined && !departing && !closed)
|
|
47075
|
+
await event("pulse", { ...audio, media: ownMedia });
|
|
47076
|
+
},
|
|
47077
|
+
async leave() {
|
|
47078
|
+
if (closed)
|
|
47079
|
+
return { ended: false };
|
|
47080
|
+
departing = true;
|
|
47081
|
+
settleAdmission(cancelled2());
|
|
47082
|
+
clearTimeout(pulseTimer);
|
|
47083
|
+
await startTask?.catch(() => {});
|
|
47084
|
+
if (closed || !joined && !submittedJoin || !mailbox.getSnapshot()?.head)
|
|
47085
|
+
return { ended: false };
|
|
47086
|
+
await event("leave", {});
|
|
47087
|
+
await mailbox.read();
|
|
47088
|
+
return { ended: !!head && head.participants.every((item) => !alive(item.holder)) };
|
|
47089
|
+
},
|
|
47090
|
+
close() {
|
|
47091
|
+
if (closed)
|
|
47092
|
+
return;
|
|
47093
|
+
closed = true;
|
|
47094
|
+
retryWait?.();
|
|
47095
|
+
settleAdmission(cancelled2());
|
|
47096
|
+
clearTimeout(pulseTimer);
|
|
47097
|
+
mailbox.close();
|
|
47098
|
+
disposeState(state);
|
|
47099
|
+
disposeState(member);
|
|
47100
|
+
disposeState(pendingCandidate?.state);
|
|
47101
|
+
disposeState(pendingInitial?.state);
|
|
47102
|
+
pendingJoins.clear();
|
|
47103
|
+
published.clear();
|
|
47104
|
+
seen.clear();
|
|
47105
|
+
}
|
|
47106
|
+
});
|
|
47107
|
+
}
|
|
47108
|
+
|
|
47109
|
+
// ../../core/calls/activity.js
|
|
47110
|
+
var positive = (value) => Number.isFinite(value) && value >= CALL_SPEAKING_THRESHOLD && value <= 1;
|
|
47111
|
+
function createCallActivity({ holder, onSpeaking, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
47112
|
+
let closed = false;
|
|
47113
|
+
let timer = null;
|
|
47114
|
+
let local = false;
|
|
47115
|
+
let direct = null;
|
|
47116
|
+
let routes = new Map;
|
|
47117
|
+
let sources = new Map;
|
|
47118
|
+
let published = [];
|
|
47119
|
+
const speaking = new Map;
|
|
47120
|
+
function publish() {
|
|
47121
|
+
const next = [...speaking.keys()].sort();
|
|
47122
|
+
if (published.length === next.length && published.every((value, index) => value === next[index]))
|
|
47123
|
+
return;
|
|
47124
|
+
published = next;
|
|
47125
|
+
onSpeaking?.(next);
|
|
47126
|
+
}
|
|
47127
|
+
function schedule() {
|
|
47128
|
+
clearTimer(timer);
|
|
47129
|
+
timer = null;
|
|
47130
|
+
if (!closed && speaking.size)
|
|
47131
|
+
timer = setTimer(expire, Math.max(0, Math.min(...speaking.values()) - now()));
|
|
47132
|
+
}
|
|
47133
|
+
function expire() {
|
|
47134
|
+
timer = null;
|
|
47135
|
+
if (closed)
|
|
47136
|
+
return;
|
|
47137
|
+
const time = now();
|
|
47138
|
+
for (const [id, deadline] of speaking)
|
|
47139
|
+
if (deadline <= time)
|
|
47140
|
+
speaking.delete(id);
|
|
47141
|
+
publish();
|
|
47142
|
+
schedule();
|
|
47143
|
+
}
|
|
47144
|
+
function update({ peers, received, directPeer, mode, muted, deafened }) {
|
|
47145
|
+
if (closed)
|
|
47146
|
+
return;
|
|
47147
|
+
const admitted = new Map(peers.map((peer) => [peer.holder, peer]));
|
|
47148
|
+
local = admitted.has(holder) && !muted && !deafened;
|
|
47149
|
+
direct = mode === "direct" && directPeer !== holder && admitted.has(directPeer) ? directPeer : null;
|
|
47150
|
+
const nextSources = new Map(local ? [[holder, "local"]] : []);
|
|
47151
|
+
const nextRoutes2 = new Map;
|
|
47152
|
+
const ambiguous = new Set;
|
|
47153
|
+
if (direct)
|
|
47154
|
+
nextSources.set(direct, "direct");
|
|
47155
|
+
if (mode === "group") {
|
|
47156
|
+
for (const [id, mid] of received) {
|
|
47157
|
+
if (id === holder || !admitted.has(id) || typeof mid !== "string" || !mid)
|
|
47158
|
+
continue;
|
|
47159
|
+
if (nextRoutes2.has(mid)) {
|
|
47160
|
+
nextSources.delete(nextRoutes2.get(mid));
|
|
47161
|
+
nextRoutes2.delete(mid);
|
|
47162
|
+
ambiguous.add(mid);
|
|
47163
|
+
}
|
|
47164
|
+
if (ambiguous.has(mid))
|
|
47165
|
+
continue;
|
|
47166
|
+
nextRoutes2.set(mid, id);
|
|
47167
|
+
nextSources.set(id, mid);
|
|
47168
|
+
}
|
|
47169
|
+
}
|
|
47170
|
+
const time = now();
|
|
47171
|
+
for (const [id, deadline] of speaking) {
|
|
47172
|
+
if (deadline <= time || !nextSources.has(id) || nextSources.get(id) !== sources.get(id))
|
|
47173
|
+
speaking.delete(id);
|
|
47174
|
+
}
|
|
47175
|
+
routes = nextRoutes2;
|
|
47176
|
+
sources = nextSources;
|
|
47177
|
+
publish();
|
|
47178
|
+
schedule();
|
|
47179
|
+
}
|
|
47180
|
+
function sample(value) {
|
|
47181
|
+
if (closed)
|
|
47182
|
+
return;
|
|
47183
|
+
const time = now();
|
|
47184
|
+
for (const [id, deadline] of speaking)
|
|
47185
|
+
if (deadline <= time)
|
|
47186
|
+
speaking.delete(id);
|
|
47187
|
+
if (local && positive(value.local))
|
|
47188
|
+
speaking.set(holder, time + CALL_SPEAKING_HOLD_MS);
|
|
47189
|
+
for (const item of value.receive) {
|
|
47190
|
+
if (typeof item.mid !== "string" || !item.mid || !positive(item.level))
|
|
47191
|
+
continue;
|
|
47192
|
+
const id = direct || routes.get(item.mid);
|
|
47193
|
+
if (id)
|
|
47194
|
+
speaking.set(id, time + CALL_SPEAKING_HOLD_MS);
|
|
47195
|
+
}
|
|
47196
|
+
publish();
|
|
47197
|
+
schedule();
|
|
47198
|
+
}
|
|
47199
|
+
function reset() {
|
|
47200
|
+
clearTimer(timer);
|
|
47201
|
+
timer = null;
|
|
47202
|
+
speaking.clear();
|
|
47203
|
+
publish();
|
|
47204
|
+
}
|
|
47205
|
+
function close() {
|
|
47206
|
+
if (closed)
|
|
47207
|
+
return;
|
|
47208
|
+
closed = true;
|
|
47209
|
+
reset();
|
|
47210
|
+
local = false;
|
|
47211
|
+
direct = null;
|
|
47212
|
+
routes.clear();
|
|
47213
|
+
sources.clear();
|
|
47214
|
+
}
|
|
47215
|
+
return Object.freeze({ update, sample, reset, close });
|
|
47216
|
+
}
|
|
47217
|
+
|
|
47218
|
+
// ../../core/calls/media.js
|
|
47219
|
+
var sdp = (value) => ({ type: value.type, sdp: value.sdp });
|
|
47220
|
+
var validMid = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,32}$/u.test(value);
|
|
47221
|
+
var MAX_PENDING_SIGNALS = 128;
|
|
47222
|
+
var CONNECTION_TIMEOUT_MS = 15000;
|
|
47223
|
+
var retiredConnection = new Error("call connection retired");
|
|
47224
|
+
function createCallMediaSession({ port, mode, holder, preparation, provider, signal, publish, onState, onError, onExpired, onSpeaking, diag }) {
|
|
47225
|
+
let media = null;
|
|
47226
|
+
let closed = false;
|
|
47227
|
+
let deadline = null;
|
|
47228
|
+
let keys = null;
|
|
47229
|
+
let peers = [];
|
|
47230
|
+
let sessionId = null;
|
|
47231
|
+
let published = false;
|
|
47232
|
+
let started = false;
|
|
47233
|
+
let directPeer = null;
|
|
47234
|
+
let offered = false;
|
|
47235
|
+
let muted = false;
|
|
47236
|
+
let deafened = false;
|
|
47237
|
+
let microphoneVolume = 1;
|
|
47238
|
+
let closing = null;
|
|
47239
|
+
let connectionState = "new";
|
|
47240
|
+
let connectionWait = null;
|
|
47241
|
+
let tail = Promise.resolve();
|
|
47242
|
+
let connectionGeneration = 0;
|
|
47243
|
+
let openingConnection = null;
|
|
47244
|
+
let retiringConnection = Promise.resolve();
|
|
47245
|
+
let iceServers = null;
|
|
47246
|
+
const received = new Map;
|
|
47247
|
+
const peerAudio = new Map;
|
|
47248
|
+
const pendingPeerAudio = new Map;
|
|
47249
|
+
const activity = createCallActivity({ holder, onSpeaking });
|
|
47250
|
+
const updateActivity = () => activity.update({ peers, received, directPeer, mode, muted, deafened });
|
|
47251
|
+
const pendingSignals = [];
|
|
47252
|
+
const incomingSignals = [];
|
|
47253
|
+
function settlePeerAudio(id, error) {
|
|
47254
|
+
const pending = pendingPeerAudio.get(id);
|
|
47255
|
+
if (!pending)
|
|
47256
|
+
return;
|
|
47257
|
+
pendingPeerAudio.delete(id);
|
|
47258
|
+
if (error)
|
|
47259
|
+
pending.reject(error);
|
|
47260
|
+
else
|
|
47261
|
+
pending.resolve();
|
|
47262
|
+
}
|
|
47263
|
+
function current(generation = connectionGeneration) {
|
|
47264
|
+
if (closed)
|
|
47265
|
+
throw new Error("call media closed");
|
|
47266
|
+
if (generation !== connectionGeneration)
|
|
47267
|
+
throw retiredConnection;
|
|
47268
|
+
}
|
|
47269
|
+
async function disposeConnection(previous) {
|
|
47270
|
+
const results = await Promise.allSettled([() => previous?.revoke(), () => previous?.close()].map((operation) => {
|
|
47271
|
+
try {
|
|
47272
|
+
return Promise.resolve(operation());
|
|
47273
|
+
} catch (error) {
|
|
47274
|
+
return Promise.reject(error);
|
|
47275
|
+
}
|
|
47276
|
+
}));
|
|
47277
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
47278
|
+
if (failed)
|
|
47279
|
+
throw failed.reason;
|
|
47280
|
+
}
|
|
47281
|
+
function retireDirectConnection(nextPeer) {
|
|
47282
|
+
connectionGeneration++;
|
|
47283
|
+
const previous = media;
|
|
47284
|
+
media = null;
|
|
47285
|
+
received.clear();
|
|
47286
|
+
directPeer = nextPeer;
|
|
47287
|
+
activity.reset();
|
|
47288
|
+
updateActivity();
|
|
47289
|
+
offered = false;
|
|
47290
|
+
connectionState = "new";
|
|
47291
|
+
pendingSignals.length = 0;
|
|
47292
|
+
const opening = openingConnection;
|
|
47293
|
+
retiringConnection = Promise.all([
|
|
47294
|
+
retiringConnection,
|
|
47295
|
+
disposeConnection(previous),
|
|
47296
|
+
opening?.then(disposeConnection, () => {})
|
|
47297
|
+
]).then(() => {});
|
|
47298
|
+
retiringConnection.catch(() => {});
|
|
47299
|
+
onState?.({ state: "connecting" });
|
|
47300
|
+
}
|
|
47301
|
+
function settleConnection(error) {
|
|
47302
|
+
if (!connectionWait)
|
|
47303
|
+
return;
|
|
47304
|
+
const wait2 = connectionWait;
|
|
47305
|
+
connectionWait = null;
|
|
47306
|
+
clearTimeout(wait2.timer);
|
|
47307
|
+
if (error)
|
|
47308
|
+
wait2.reject(error);
|
|
47309
|
+
else
|
|
47310
|
+
wait2.resolve();
|
|
47311
|
+
}
|
|
47312
|
+
function connected() {
|
|
47313
|
+
current();
|
|
47314
|
+
if (connectionState === "connected")
|
|
47315
|
+
return Promise.resolve();
|
|
47316
|
+
if (["failed", "closed"].includes(connectionState))
|
|
47317
|
+
return Promise.reject(new Error("call connection failed"));
|
|
47318
|
+
if (!connectionWait) {
|
|
47319
|
+
const wait2 = {};
|
|
47320
|
+
wait2.promise = new Promise((resolve, reject) => {
|
|
47321
|
+
wait2.resolve = resolve;
|
|
47322
|
+
wait2.reject = reject;
|
|
47323
|
+
});
|
|
47324
|
+
wait2.timer = setTimeout(() => settleConnection(new Error("call connection timed out")), CONNECTION_TIMEOUT_MS);
|
|
47325
|
+
connectionWait = wait2;
|
|
47326
|
+
}
|
|
47327
|
+
return connectionWait.promise;
|
|
47328
|
+
}
|
|
47329
|
+
function serialize(work) {
|
|
47330
|
+
const result = tail.then(() => {
|
|
47331
|
+
current();
|
|
47332
|
+
return work();
|
|
47333
|
+
});
|
|
47334
|
+
tail = result.catch((error) => {
|
|
47335
|
+
if (!closed)
|
|
47336
|
+
onError?.(error);
|
|
47337
|
+
});
|
|
47338
|
+
return result;
|
|
47339
|
+
}
|
|
47340
|
+
function close() {
|
|
47341
|
+
if (closed)
|
|
47342
|
+
return closing;
|
|
47343
|
+
closed = true;
|
|
47344
|
+
activity.close();
|
|
47345
|
+
connectionGeneration++;
|
|
47346
|
+
settleConnection(new Error("call media closed"));
|
|
47347
|
+
const previous = media;
|
|
47348
|
+
media = keys = deadline = iceServers = null;
|
|
47349
|
+
received.clear();
|
|
47350
|
+
peerAudio.clear();
|
|
47351
|
+
for (const id of pendingPeerAudio.keys())
|
|
47352
|
+
settlePeerAudio(id, new Error("call media closed"));
|
|
47353
|
+
pendingSignals.length = incomingSignals.length = 0;
|
|
47354
|
+
const operations = [
|
|
47355
|
+
disposeConnection(previous),
|
|
47356
|
+
retiringConnection,
|
|
47357
|
+
openingConnection?.then((opened) => opened !== previous ? disposeConnection(opened) : undefined, () => {})
|
|
47358
|
+
];
|
|
47359
|
+
closing = Promise.allSettled(operations).then((results) => {
|
|
47360
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
47361
|
+
if (failed)
|
|
47362
|
+
throw failed.reason;
|
|
47363
|
+
});
|
|
47364
|
+
return closing;
|
|
47365
|
+
}
|
|
47366
|
+
async function openConnection() {
|
|
47367
|
+
const generation = connectionGeneration;
|
|
47368
|
+
const live = () => !closed && generation === connectionGeneration;
|
|
47369
|
+
await retiringConnection;
|
|
47370
|
+
current(generation);
|
|
47371
|
+
const startedAt = Date.now();
|
|
47372
|
+
const prepared = preparation;
|
|
47373
|
+
preparation = null;
|
|
47374
|
+
const opening = port.open({
|
|
47375
|
+
id: holder,
|
|
47376
|
+
mode,
|
|
47377
|
+
preparation: prepared,
|
|
47378
|
+
onState: (value) => {
|
|
47379
|
+
if (!live())
|
|
47380
|
+
return;
|
|
47381
|
+
connectionState = value.state;
|
|
47382
|
+
if (connectionState === "connected") {
|
|
47383
|
+
settleConnection();
|
|
47384
|
+
serialize(async () => {
|
|
47385
|
+
if (live())
|
|
47386
|
+
await media?.startSource?.();
|
|
47387
|
+
});
|
|
47388
|
+
} else if (["failed", "closed"].includes(connectionState))
|
|
47389
|
+
settleConnection(new Error("call connection failed"));
|
|
47390
|
+
onState?.(value);
|
|
47391
|
+
},
|
|
47392
|
+
onExpired: () => {
|
|
47393
|
+
if (live())
|
|
47394
|
+
onExpired?.();
|
|
47395
|
+
},
|
|
47396
|
+
onSignal(value) {
|
|
47397
|
+
if (mode !== "direct" || !live())
|
|
47398
|
+
return;
|
|
47399
|
+
if (directPeer)
|
|
47400
|
+
signal(directPeer, value).catch((error) => {
|
|
47401
|
+
if (live())
|
|
47402
|
+
onError?.(error);
|
|
47403
|
+
});
|
|
47404
|
+
else if (pendingSignals.length < MAX_PENDING_SIGNALS)
|
|
47405
|
+
pendingSignals.push(value);
|
|
47406
|
+
else
|
|
47407
|
+
onError?.(new Error("too many pending media signals"));
|
|
47408
|
+
},
|
|
47409
|
+
onTrack({ mid }) {
|
|
47410
|
+
if (!live())
|
|
47411
|
+
return;
|
|
47412
|
+
if (mode === "direct" && directPeer && validMid(mid))
|
|
47413
|
+
received.set(directPeer, mid);
|
|
47414
|
+
Promise.all([updateKeys(), applyPeerAudio()]).catch((error) => {
|
|
47415
|
+
if (live())
|
|
47416
|
+
onError?.(error);
|
|
47417
|
+
});
|
|
47418
|
+
},
|
|
47419
|
+
onActivity(value) {
|
|
47420
|
+
if (live())
|
|
47421
|
+
activity.sample(value);
|
|
47422
|
+
}
|
|
47423
|
+
});
|
|
47424
|
+
openingConnection = opening;
|
|
47425
|
+
const preparing = opening.then(async (opened) => {
|
|
47426
|
+
current(generation);
|
|
47427
|
+
media = opened;
|
|
47428
|
+
await opened.mute(muted);
|
|
47429
|
+
current(generation);
|
|
47430
|
+
await opened.deafen(deafened);
|
|
47431
|
+
current(generation);
|
|
47432
|
+
if (microphoneVolume !== 1) {
|
|
47433
|
+
await opened.setMicrophoneVolume(microphoneVolume);
|
|
47434
|
+
current(generation);
|
|
47435
|
+
}
|
|
47436
|
+
await opened.prepare();
|
|
47437
|
+
current(generation);
|
|
47438
|
+
markDone(diag, "call.media.setup", startedAt, { stage: "local" });
|
|
47439
|
+
return opened;
|
|
47440
|
+
});
|
|
47441
|
+
try {
|
|
47442
|
+
const [opened, configuration] = await Promise.all([
|
|
47443
|
+
preparing,
|
|
47444
|
+
Promise.resolve().then(() => iceServers ? { iceServers, sessionId } : provider(mode === "group" ? "session/new" : "turn", {})).then((value) => {
|
|
47445
|
+
current(generation);
|
|
47446
|
+
markDone(diag, "call.media.setup", startedAt, { stage: "provider" });
|
|
47447
|
+
return value;
|
|
47448
|
+
})
|
|
47449
|
+
]);
|
|
47450
|
+
current(generation);
|
|
47451
|
+
iceServers = configuration.iceServers;
|
|
47452
|
+
if (mode === "group")
|
|
47453
|
+
sessionId = configuration.sessionId;
|
|
47454
|
+
await opened.configure(iceServers);
|
|
47455
|
+
current(generation);
|
|
47456
|
+
if (deadline) {
|
|
47457
|
+
await opened.grant(deadline);
|
|
47458
|
+
current(generation);
|
|
47459
|
+
}
|
|
47460
|
+
await updateKeys();
|
|
47461
|
+
current(generation);
|
|
47462
|
+
markDone(diag, "call.media.setup", startedAt, { stage: "ready" });
|
|
47463
|
+
} catch (error) {
|
|
47464
|
+
await disposeConnection(await opening.catch(() => null));
|
|
47465
|
+
throw error;
|
|
47466
|
+
} finally {
|
|
47467
|
+
if (openingConnection === opening)
|
|
47468
|
+
openingConnection = null;
|
|
47469
|
+
}
|
|
47470
|
+
}
|
|
47471
|
+
async function applyDescription(result, required = false) {
|
|
47472
|
+
current();
|
|
47473
|
+
if (!result || result.errorCode || result.tracks?.some((track) => track.errorCode))
|
|
47474
|
+
throw new Error("call negotiation failed");
|
|
47475
|
+
const description = result.sessionDescription;
|
|
47476
|
+
if (!description) {
|
|
47477
|
+
if (required || result.requiresImmediateRenegotiation)
|
|
47478
|
+
throw new Error("missing call description");
|
|
47479
|
+
return;
|
|
47480
|
+
}
|
|
47481
|
+
if (!["offer", "answer"].includes(description.type) || typeof description.sdp !== "string" || !description.sdp.length)
|
|
47482
|
+
throw new Error("invalid call description");
|
|
47483
|
+
if (description.type === "offer") {
|
|
47484
|
+
const answer = await media.answer(description);
|
|
47485
|
+
current();
|
|
47486
|
+
await provider("renegotiate", { sessionId, sessionDescription: sdp(answer) });
|
|
47487
|
+
current();
|
|
47488
|
+
} else {
|
|
47489
|
+
await media.remote(description);
|
|
47490
|
+
current();
|
|
47491
|
+
}
|
|
47492
|
+
}
|
|
47493
|
+
async function updateKeys() {
|
|
47494
|
+
if (!media || !keys || mode !== "group")
|
|
47495
|
+
return;
|
|
47496
|
+
const receive = [];
|
|
47497
|
+
for (const peer of peers) {
|
|
47498
|
+
const mid = received.get(peer.holder);
|
|
47499
|
+
const member = keys.members.find((item) => toHex(item.leafId) === peer.holder);
|
|
47500
|
+
if (mid != null && member)
|
|
47501
|
+
receive.push({
|
|
47502
|
+
mid,
|
|
47503
|
+
baseKey: keys.baseKey,
|
|
47504
|
+
epoch: keys.epoch,
|
|
47505
|
+
leafIndex: member.leafIndex,
|
|
47506
|
+
contextId: 0
|
|
47507
|
+
});
|
|
47508
|
+
}
|
|
47509
|
+
await media.setKeys({ send: { baseKey: keys.baseKey, epoch: keys.epoch, leafIndex: keys.leafIndex, contextId: 0 }, receive });
|
|
47510
|
+
}
|
|
47511
|
+
function applyPeerAudio() {
|
|
47512
|
+
return Promise.all([...peerAudio].map(async ([id, entry]) => {
|
|
47513
|
+
const mid = received.get(id);
|
|
47514
|
+
if (!media || mid == null)
|
|
47515
|
+
return;
|
|
47516
|
+
const value = entry.value;
|
|
47517
|
+
try {
|
|
47518
|
+
await media.setPeerAudio({ mid, ...value });
|
|
47519
|
+
settlePeerAudio(id);
|
|
47520
|
+
} catch (error) {
|
|
47521
|
+
if (pendingPeerAudio.has(id))
|
|
47522
|
+
settlePeerAudio(id, error);
|
|
47523
|
+
else
|
|
47524
|
+
throw error;
|
|
47525
|
+
}
|
|
47526
|
+
}));
|
|
47527
|
+
}
|
|
47528
|
+
async function negotiate() {
|
|
47529
|
+
if (!started || !keys)
|
|
47530
|
+
return;
|
|
47531
|
+
if (mode === "group") {
|
|
47532
|
+
if (!published) {
|
|
47533
|
+
const offer = await media.offer();
|
|
47534
|
+
current();
|
|
47535
|
+
const sendMid = media.sendMid;
|
|
47536
|
+
if (!validMid(sendMid))
|
|
47537
|
+
throw new Error("microphone track unavailable");
|
|
47538
|
+
const trackName = toHex(randomBytes3(16));
|
|
47539
|
+
const result = await provider("tracks/new", {
|
|
47540
|
+
sessionId,
|
|
47541
|
+
sessionDescription: sdp(offer),
|
|
47542
|
+
tracks: [{ location: "local", mid: sendMid, trackName }]
|
|
47543
|
+
});
|
|
47544
|
+
current();
|
|
47545
|
+
if (!Array.isArray(result.tracks) || result.tracks.length !== 1 || result.tracks[0].mid !== sendMid || result.tracks[0].trackName !== trackName)
|
|
47546
|
+
throw new Error("microphone publication failed");
|
|
47547
|
+
await applyDescription(result, true);
|
|
47548
|
+
current();
|
|
47549
|
+
await connected();
|
|
47550
|
+
current();
|
|
47551
|
+
await Promise.all([
|
|
47552
|
+
publish({ sessionId, trackName }).then(() => {
|
|
47553
|
+
current();
|
|
47554
|
+
published = true;
|
|
47555
|
+
}),
|
|
47556
|
+
receiveGroupTracks()
|
|
47557
|
+
]);
|
|
47558
|
+
} else {
|
|
47559
|
+
await receiveGroupTracks();
|
|
47560
|
+
}
|
|
47561
|
+
} else {
|
|
47562
|
+
const peer = peers.find((item) => item.holder !== holder);
|
|
47563
|
+
if (!peer)
|
|
47564
|
+
return;
|
|
47565
|
+
directPeer = peer.holder;
|
|
47566
|
+
updateActivity();
|
|
47567
|
+
const generation = connectionGeneration;
|
|
47568
|
+
try {
|
|
47569
|
+
if (!media)
|
|
47570
|
+
await openConnection();
|
|
47571
|
+
current(generation);
|
|
47572
|
+
if (holder < peer.holder && !offered) {
|
|
47573
|
+
offered = true;
|
|
47574
|
+
const offer = await media.offer();
|
|
47575
|
+
current(generation);
|
|
47576
|
+
await signal(peer.holder, { type: "description", description: sdp(offer) });
|
|
47577
|
+
current(generation);
|
|
47578
|
+
}
|
|
47579
|
+
} catch (error) {
|
|
47580
|
+
if (!closed && generation !== connectionGeneration)
|
|
47581
|
+
return;
|
|
47582
|
+
await close();
|
|
47583
|
+
throw error;
|
|
47584
|
+
}
|
|
47585
|
+
}
|
|
47586
|
+
}
|
|
47587
|
+
async function receiveGroupTracks() {
|
|
47588
|
+
const additions = peers.filter((peer) => peer.holder !== holder && peer.media?.sessionId && peer.media?.trackName && !received.has(peer.holder));
|
|
47589
|
+
if (additions.length) {
|
|
47590
|
+
await connected();
|
|
47591
|
+
current();
|
|
47592
|
+
const result = await provider("tracks/new", { sessionId, tracks: additions.map((peer) => ({
|
|
47593
|
+
location: "remote",
|
|
47594
|
+
sessionId: peer.media.sessionId,
|
|
47595
|
+
trackName: peer.media.trackName
|
|
47596
|
+
})) });
|
|
47597
|
+
current();
|
|
47598
|
+
if (!Array.isArray(result.tracks) || result.tracks.length !== additions.length)
|
|
47599
|
+
throw new Error("peer audio subscription failed");
|
|
47600
|
+
const mappings = [];
|
|
47601
|
+
const occupied = new Set([media.sendMid, ...received.values()]);
|
|
47602
|
+
for (const peer of additions) {
|
|
47603
|
+
const track = result.tracks?.find((item) => item.sessionId === peer.media.sessionId && item.trackName === peer.media.trackName);
|
|
47604
|
+
if (!validMid(track?.mid) || track.errorCode || occupied.has(track.mid))
|
|
47605
|
+
throw new Error("peer audio subscription failed");
|
|
47606
|
+
occupied.add(track.mid);
|
|
47607
|
+
mappings.push([peer.holder, track.mid]);
|
|
47608
|
+
}
|
|
47609
|
+
for (const [peer, mid] of mappings)
|
|
47610
|
+
received.set(peer, mid);
|
|
47611
|
+
updateActivity();
|
|
47612
|
+
await applyPeerAudio();
|
|
47613
|
+
current();
|
|
47614
|
+
await updateKeys();
|
|
47615
|
+
current();
|
|
47616
|
+
await applyDescription(result);
|
|
47617
|
+
}
|
|
47618
|
+
const removals = [...received].filter(([peer]) => !peers.some((item) => item.holder === peer));
|
|
47619
|
+
if (removals.length) {
|
|
47620
|
+
for (const [peer] of removals)
|
|
47621
|
+
received.delete(peer);
|
|
47622
|
+
updateActivity();
|
|
47623
|
+
await updateKeys();
|
|
47624
|
+
current();
|
|
47625
|
+
const result = await provider("tracks/close", { sessionId, tracks: removals.map(([, mid]) => ({ mid })) });
|
|
47626
|
+
await applyDescription(result);
|
|
47627
|
+
}
|
|
47628
|
+
}
|
|
47629
|
+
return Object.freeze({
|
|
47630
|
+
async open() {
|
|
47631
|
+
current();
|
|
47632
|
+
try {
|
|
47633
|
+
await openConnection();
|
|
47634
|
+
started = true;
|
|
47635
|
+
await serialize(negotiate);
|
|
47636
|
+
for (const [value, from] of incomingSignals.splice(0))
|
|
47637
|
+
await handleSignal(value, from);
|
|
47638
|
+
} catch (error) {
|
|
47639
|
+
await close();
|
|
47640
|
+
throw error;
|
|
47641
|
+
}
|
|
47642
|
+
},
|
|
47643
|
+
grant(value) {
|
|
47644
|
+
current();
|
|
47645
|
+
deadline = value;
|
|
47646
|
+
return media?.grant(value);
|
|
47647
|
+
},
|
|
47648
|
+
async setKeys(value) {
|
|
47649
|
+
current();
|
|
47650
|
+
keys = value;
|
|
47651
|
+
await updateKeys();
|
|
47652
|
+
current();
|
|
47653
|
+
return serialize(negotiate);
|
|
47654
|
+
},
|
|
47655
|
+
async setParticipants(value) {
|
|
47656
|
+
current();
|
|
47657
|
+
peers = value;
|
|
47658
|
+
for (const id of peerAudio.keys())
|
|
47659
|
+
if (!peers.some((peer) => peer.holder === id)) {
|
|
47660
|
+
peerAudio.delete(id);
|
|
47661
|
+
settlePeerAudio(id, new Error("call participant unavailable"));
|
|
47662
|
+
}
|
|
47663
|
+
updateActivity();
|
|
47664
|
+
const nextPeer = peers.find((peer) => peer.holder !== holder)?.holder || null;
|
|
47665
|
+
if (mode === "direct" && started && directPeer && nextPeer !== directPeer)
|
|
47666
|
+
retireDirectConnection(nextPeer);
|
|
47667
|
+
await updateKeys();
|
|
47668
|
+
current();
|
|
47669
|
+
return serialize(negotiate);
|
|
47670
|
+
},
|
|
47671
|
+
signal(value, from) {
|
|
47672
|
+
if (closed)
|
|
47673
|
+
return Promise.reject(new Error("call media closed"));
|
|
47674
|
+
if (!started) {
|
|
47675
|
+
if (incomingSignals.length >= MAX_PENDING_SIGNALS)
|
|
47676
|
+
return Promise.reject(new Error("too many pending media signals"));
|
|
47677
|
+
incomingSignals.push([value, from]);
|
|
47678
|
+
return Promise.resolve();
|
|
47679
|
+
}
|
|
47680
|
+
return handleSignal(value, from);
|
|
47681
|
+
},
|
|
47682
|
+
mute(value) {
|
|
47683
|
+
current();
|
|
47684
|
+
const next = value === true;
|
|
47685
|
+
if (muted === next)
|
|
47686
|
+
return;
|
|
47687
|
+
muted = next;
|
|
47688
|
+
updateActivity();
|
|
47689
|
+
return media?.mute(muted);
|
|
47690
|
+
},
|
|
47691
|
+
deafen(value) {
|
|
47692
|
+
current();
|
|
47693
|
+
const next = value === true;
|
|
47694
|
+
if (deafened === next)
|
|
47695
|
+
return;
|
|
47696
|
+
deafened = next;
|
|
47697
|
+
updateActivity();
|
|
47698
|
+
return media?.deafen(deafened);
|
|
47699
|
+
},
|
|
47700
|
+
async setMicrophoneVolume(value) {
|
|
47701
|
+
current();
|
|
47702
|
+
const previous = microphoneVolume;
|
|
47703
|
+
microphoneVolume = value;
|
|
47704
|
+
try {
|
|
47705
|
+
if (media)
|
|
47706
|
+
await media.setMicrophoneVolume(value);
|
|
47707
|
+
} catch (error) {
|
|
47708
|
+
if (microphoneVolume === value)
|
|
47709
|
+
microphoneVolume = previous;
|
|
47710
|
+
throw error;
|
|
47711
|
+
}
|
|
47712
|
+
},
|
|
47713
|
+
async setPeerAudio(id, value) {
|
|
47714
|
+
current();
|
|
47715
|
+
const mid = received.get(id);
|
|
47716
|
+
if (id === holder || !peers.some((peer) => peer.holder === id))
|
|
47717
|
+
throw new Error("call participant unavailable");
|
|
47718
|
+
let entry = peerAudio.get(id);
|
|
47719
|
+
if (!entry) {
|
|
47720
|
+
entry = { value: null, applied: null };
|
|
47721
|
+
peerAudio.set(id, entry);
|
|
47722
|
+
}
|
|
47723
|
+
entry.value = value;
|
|
47724
|
+
try {
|
|
47725
|
+
if (media && mid != null)
|
|
47726
|
+
await media.setPeerAudio({ mid, ...value });
|
|
47727
|
+
else {
|
|
47728
|
+
let pending = pendingPeerAudio.get(id);
|
|
47729
|
+
if (!pending) {
|
|
47730
|
+
pending = {};
|
|
47731
|
+
pending.promise = new Promise((resolve, reject) => {
|
|
47732
|
+
pending.resolve = resolve;
|
|
47733
|
+
pending.reject = reject;
|
|
47734
|
+
});
|
|
47735
|
+
pendingPeerAudio.set(id, pending);
|
|
47736
|
+
}
|
|
47737
|
+
await pending.promise;
|
|
47738
|
+
}
|
|
47739
|
+
if (peerAudio.get(id) === entry && entry.value === value)
|
|
47740
|
+
entry.applied = value;
|
|
47741
|
+
} catch (error) {
|
|
47742
|
+
if (peerAudio.get(id) === entry && entry.value === value) {
|
|
47743
|
+
if (entry.applied)
|
|
47744
|
+
entry.value = entry.applied;
|
|
47745
|
+
else
|
|
47746
|
+
peerAudio.delete(id);
|
|
47747
|
+
}
|
|
47748
|
+
throw error;
|
|
47749
|
+
}
|
|
47750
|
+
},
|
|
47751
|
+
close
|
|
47752
|
+
});
|
|
47753
|
+
function handleSignal(value, from) {
|
|
47754
|
+
return serialize(async () => {
|
|
47755
|
+
if (mode !== "direct")
|
|
47756
|
+
throw new Error("unadmitted media signal");
|
|
47757
|
+
if (from === holder || !peers.some((peer) => peer.holder === from))
|
|
47758
|
+
return;
|
|
47759
|
+
const generation = connectionGeneration;
|
|
47760
|
+
try {
|
|
47761
|
+
await negotiate();
|
|
47762
|
+
current(generation);
|
|
47763
|
+
if (value.type === "description") {
|
|
47764
|
+
if (value.description.type === "offer") {
|
|
47765
|
+
if (holder < from)
|
|
47766
|
+
throw new Error("unexpected direct offer");
|
|
47767
|
+
offered = true;
|
|
47768
|
+
const answer = await media.answer(value.description);
|
|
47769
|
+
current(generation);
|
|
47770
|
+
await signal(from, { type: "description", description: sdp(answer) });
|
|
47771
|
+
} else
|
|
47772
|
+
await media.remote(value.description);
|
|
47773
|
+
} else if (value.type === "ice")
|
|
47774
|
+
await media.ice(value.candidate);
|
|
47775
|
+
else
|
|
47776
|
+
throw new Error("invalid media signal");
|
|
47777
|
+
current(generation);
|
|
47778
|
+
for (const pending of pendingSignals.splice(0)) {
|
|
47779
|
+
await signal(from, pending);
|
|
47780
|
+
current(generation);
|
|
47781
|
+
}
|
|
47782
|
+
} catch (error) {
|
|
47783
|
+
if (!closed && generation !== connectionGeneration)
|
|
47784
|
+
return;
|
|
47785
|
+
throw error;
|
|
47786
|
+
}
|
|
47787
|
+
});
|
|
47788
|
+
}
|
|
47789
|
+
}
|
|
47790
|
+
|
|
47791
|
+
// ../../core/calls/observation.js
|
|
47792
|
+
function openCallObservation({ cloud, capability, endpoint, protocol, onChange, onEmpty, onError }) {
|
|
47793
|
+
let verified = null;
|
|
47794
|
+
let identities = new Map;
|
|
47795
|
+
const audio = new Map;
|
|
47796
|
+
const departed = new Set;
|
|
47797
|
+
let last = "";
|
|
47798
|
+
let pending = null;
|
|
47799
|
+
let dirty = false;
|
|
47800
|
+
let closed = false;
|
|
47801
|
+
const mailbox = openCallMailbox({
|
|
47802
|
+
cloud,
|
|
47803
|
+
capability,
|
|
47804
|
+
endpoint,
|
|
47805
|
+
protocol,
|
|
47806
|
+
onError,
|
|
47807
|
+
stream: false,
|
|
47808
|
+
onChange(record) {
|
|
47809
|
+
const head = record.head;
|
|
47810
|
+
const admissions = head?.data?.participants || [];
|
|
47811
|
+
if (head && head !== verified) {
|
|
47812
|
+
if (!Number.isSafeInteger(head.data?.epoch) || head.data.epoch < 0)
|
|
47813
|
+
throw new Error("invalid call observation");
|
|
47814
|
+
const nextIdentities = verifyCallAdmissions(admissions, protocol);
|
|
47815
|
+
if (!admissions.some((item) => item.actor === head.actor && item.holder === head.holder && item.endpoint === head.endpoint)) {
|
|
47816
|
+
throw new Error("unadmitted call head");
|
|
47817
|
+
}
|
|
47818
|
+
identities = nextIdentities;
|
|
47819
|
+
} else if (!head)
|
|
47820
|
+
identities.clear();
|
|
47821
|
+
verified = head;
|
|
47822
|
+
if (record.historyLost) {
|
|
47823
|
+
audio.clear();
|
|
47824
|
+
departed.clear();
|
|
47825
|
+
}
|
|
47826
|
+
const holders = new Set(admissions.map((item) => item.holder));
|
|
47827
|
+
for (const holder of audio.keys())
|
|
47828
|
+
if (!holders.has(holder))
|
|
47829
|
+
audio.delete(holder);
|
|
47830
|
+
for (const holder of departed)
|
|
47831
|
+
if (!holders.has(holder))
|
|
47832
|
+
departed.delete(holder);
|
|
47833
|
+
for (const packet of record.events) {
|
|
47834
|
+
if (!admissions.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint))
|
|
47835
|
+
continue;
|
|
47836
|
+
if (packet.kind === "leave")
|
|
47837
|
+
departed.add(packet.holder);
|
|
47838
|
+
if (packet.kind === "pulse")
|
|
47839
|
+
audio.set(packet.holder, { muted: packet.data.muted === true, deafened: packet.data.deafened === true });
|
|
47840
|
+
}
|
|
47841
|
+
const roster = admissions.filter((item) => !departed.has(item.holder)).map((item) => ({
|
|
47842
|
+
...identities.get(item.holder),
|
|
47843
|
+
holder: item.holder,
|
|
47844
|
+
muted: audio.get(item.holder)?.muted === true || audio.get(item.holder)?.deafened === true,
|
|
47845
|
+
deafened: audio.get(item.holder)?.deafened === true
|
|
47846
|
+
}));
|
|
47847
|
+
const fingerprint = JSON.stringify(roster);
|
|
47848
|
+
if (fingerprint !== last) {
|
|
47849
|
+
last = fingerprint;
|
|
47850
|
+
onChange(roster);
|
|
47851
|
+
}
|
|
47852
|
+
if (!head || admissions.length && roster.length === 0)
|
|
47853
|
+
onEmpty?.();
|
|
47854
|
+
}
|
|
47855
|
+
});
|
|
47856
|
+
function read() {
|
|
47857
|
+
if (closed)
|
|
47858
|
+
return Promise.resolve();
|
|
47859
|
+
dirty = true;
|
|
47860
|
+
pending ||= (async () => {
|
|
47861
|
+
while (dirty && !closed) {
|
|
47862
|
+
dirty = false;
|
|
47863
|
+
await mailbox.read();
|
|
47864
|
+
}
|
|
47865
|
+
})().finally(() => {
|
|
47866
|
+
pending = null;
|
|
47867
|
+
});
|
|
47868
|
+
return pending;
|
|
47869
|
+
}
|
|
47870
|
+
return Object.freeze({ read, close() {
|
|
47871
|
+
closed = true;
|
|
47872
|
+
mailbox.close();
|
|
47873
|
+
} });
|
|
47874
|
+
}
|
|
47875
|
+
|
|
47876
|
+
// ../../core/calls/session.js
|
|
47877
|
+
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 });
|
|
47878
|
+
var cancelled2 = () => Object.assign(new Error("call cancelled"), { code: "calls/cancelled" });
|
|
47879
|
+
var ended = () => Object.assign(new Error("this call has ended"), { code: "calls/ended" });
|
|
47880
|
+
var callError = (chatId, error) => ({ chatId, code: error?.code || "calls/failed", message: error?.message || "could not connect the call" });
|
|
47881
|
+
function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio, saveAudio, diag }) {
|
|
47882
|
+
const listeners = new Set;
|
|
47883
|
+
const discoveries = new Map;
|
|
47884
|
+
const discoveryReads = new Map;
|
|
47885
|
+
const observers = new Map;
|
|
47886
|
+
const retiring = new Set;
|
|
47887
|
+
let state = initial();
|
|
47888
|
+
const confirmedPeerAudio = new Map;
|
|
47889
|
+
const pendingPeerAudio = new Map;
|
|
47890
|
+
let peerAudioSource = null;
|
|
47891
|
+
let audioSource = normalizeCallAudio();
|
|
47892
|
+
let pendingAudio = null;
|
|
47893
|
+
let identity = null;
|
|
47894
|
+
let endpoint = null;
|
|
47895
|
+
let leaseEndpoint = null;
|
|
47896
|
+
let lease = null;
|
|
47897
|
+
let active = null;
|
|
47898
|
+
let participation = null;
|
|
47899
|
+
let focused = null;
|
|
47900
|
+
let generation = 0;
|
|
47901
|
+
let joinIntent = 0;
|
|
47902
|
+
let joining = null;
|
|
47903
|
+
let closed = false;
|
|
47904
|
+
let foreground = true;
|
|
47905
|
+
let online = true;
|
|
47906
|
+
let leaving = Promise.resolve();
|
|
47907
|
+
let drainingSources = Promise.resolve();
|
|
47908
|
+
let closingSession = null;
|
|
47909
|
+
const emit = (patch) => {
|
|
47910
|
+
if (Object.entries(patch).every(([field, value]) => Object.is(state[field], value)))
|
|
47911
|
+
return;
|
|
47912
|
+
if (patch.phase && patch.phase !== state.phase) {
|
|
47913
|
+
diag?.("call.phase", {
|
|
47914
|
+
phase: patch.phase,
|
|
47915
|
+
previous: state.phase,
|
|
47916
|
+
mode: patch.mode || state.mode,
|
|
47917
|
+
participants: (patch.participants || state.participants).length
|
|
47918
|
+
});
|
|
47919
|
+
}
|
|
47920
|
+
state = { ...state, ...patch };
|
|
47921
|
+
for (const listener of listeners)
|
|
47922
|
+
listener();
|
|
47923
|
+
};
|
|
47924
|
+
function announce(chatId, callId, event) {
|
|
47925
|
+
chat.getSnapshot().sendChatMessage(chatId, makeCallMessage(callId, event)).catch((error) => {
|
|
47926
|
+
diag?.("call.message.error", { event, code: error?.code || "" });
|
|
47927
|
+
});
|
|
47928
|
+
}
|
|
47929
|
+
function current(token) {
|
|
47930
|
+
if (closed || !identity || token !== generation)
|
|
47931
|
+
throw cancelled2();
|
|
47932
|
+
}
|
|
47933
|
+
function fail(error) {
|
|
47934
|
+
if (!identity || closed)
|
|
47935
|
+
return;
|
|
47936
|
+
diag?.("call.error", {
|
|
47937
|
+
code: error?.code || "calls/failed",
|
|
47938
|
+
phase: state.phase,
|
|
47939
|
+
mode: state.mode,
|
|
47940
|
+
participants: state.participants.length,
|
|
47941
|
+
message: error?.message || "call failed"
|
|
47942
|
+
});
|
|
47943
|
+
const chatId = active?.chatId || state.chatId;
|
|
47944
|
+
leave();
|
|
47945
|
+
emit({ phase: "error", chatId, error: callError(chatId, error) });
|
|
47946
|
+
}
|
|
47947
|
+
function available() {
|
|
47948
|
+
const entry = discoveries.get(focused);
|
|
47949
|
+
const value = (chatId, current2) => current2?.descriptor ? {
|
|
47950
|
+
chatId,
|
|
47951
|
+
callId: current2.descriptor.callId,
|
|
47952
|
+
startedAt: current2.descriptor.startedAt,
|
|
47953
|
+
participants: current2.descriptor.participants || 1,
|
|
47954
|
+
roster: (active?.discovery === current2 ? active.participants : current2.roster).filter((peer) => ![...retiring].some((call) => call.callId === current2.descriptor.callId && call.endpoint.holder === peer.holder))
|
|
47955
|
+
} : null;
|
|
47956
|
+
for (const observer of observers.values()) {
|
|
47957
|
+
if (!observer.onAvailable)
|
|
47958
|
+
continue;
|
|
47959
|
+
const next2 = value(observer.chatId, discoveries.get(observer.chatId));
|
|
47960
|
+
const fingerprint = JSON.stringify(next2);
|
|
47961
|
+
if (observer.fingerprint === fingerprint)
|
|
47962
|
+
continue;
|
|
47963
|
+
observer.fingerprint = fingerprint;
|
|
47964
|
+
try {
|
|
47965
|
+
observer.onAvailable(next2);
|
|
47966
|
+
} catch {}
|
|
47967
|
+
}
|
|
47968
|
+
const next = value(focused, entry);
|
|
47969
|
+
if (JSON.stringify(next) !== JSON.stringify(state.available))
|
|
47970
|
+
emit({ available: next });
|
|
47971
|
+
}
|
|
47972
|
+
function closeDiscovery(entry) {
|
|
47973
|
+
if ([...retiring].some((call) => call.discovery === entry))
|
|
47974
|
+
return;
|
|
47975
|
+
entry.mailbox.close();
|
|
47976
|
+
entry.observation?.close();
|
|
47977
|
+
entry.observation = null;
|
|
47978
|
+
entry.observedCallId = null;
|
|
47979
|
+
}
|
|
47980
|
+
function observeRoster(entry, refresh = false) {
|
|
47981
|
+
const descriptor = entry.descriptor;
|
|
47982
|
+
const callId = descriptor?.callId;
|
|
47983
|
+
if (!callId || active?.discovery === entry || ![...observers.values()].some((item) => item.chatId === entry.epochState.manifest.chatId)) {
|
|
47984
|
+
entry.observation?.close();
|
|
47985
|
+
entry.observation = null;
|
|
47986
|
+
entry.observedCallId = null;
|
|
47987
|
+
return;
|
|
47988
|
+
}
|
|
47989
|
+
if (entry.observedCallId === callId) {
|
|
47990
|
+
if (refresh)
|
|
47991
|
+
entry.observation.read().catch((error) => diag?.("call.discovery.error", { code: error?.code || "calls/roster" }));
|
|
47992
|
+
return;
|
|
47993
|
+
}
|
|
47994
|
+
entry.observation?.close();
|
|
47995
|
+
entry.roster = [];
|
|
47996
|
+
entry.observedCallId = callId;
|
|
47997
|
+
const token = generation;
|
|
47998
|
+
const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint, callId });
|
|
47999
|
+
const secret = fromHexBytes(descriptor.secret);
|
|
48000
|
+
const capability = createCallCapability(secret, cloud.environment, "signaling", [callId]);
|
|
48001
|
+
cleanBytes(secret);
|
|
48002
|
+
entry.observation = openCallObservation({
|
|
48003
|
+
cloud,
|
|
48004
|
+
capability,
|
|
48005
|
+
endpoint,
|
|
48006
|
+
protocol,
|
|
48007
|
+
onChange(roster) {
|
|
48008
|
+
if (generation !== token || entry.observedCallId !== callId || discoveries.get(entry.epochState.manifest.chatId) !== entry)
|
|
48009
|
+
return;
|
|
48010
|
+
entry.roster = roster;
|
|
48011
|
+
available();
|
|
48012
|
+
},
|
|
48013
|
+
onError(error) {
|
|
48014
|
+
if (generation === token)
|
|
48015
|
+
diag?.("call.discovery.error", { code: error?.code || "calls/roster" });
|
|
48016
|
+
},
|
|
48017
|
+
onEmpty() {
|
|
48018
|
+
if (generation === token && entry.observedCallId === callId)
|
|
48019
|
+
finishDiscovery(entry, descriptor);
|
|
48020
|
+
}
|
|
48021
|
+
});
|
|
48022
|
+
entry.observation.read().catch(() => {});
|
|
48023
|
+
}
|
|
48024
|
+
function prune() {
|
|
48025
|
+
for (const [chatId, value] of discoveries) {
|
|
48026
|
+
if (![...observers.values()].some((item) => item.chatId === chatId) && chatId !== active?.chatId && chatId !== joining?.chatId && ![...retiring].some((call) => call.chatId === chatId)) {
|
|
48027
|
+
closeDiscovery(value);
|
|
48028
|
+
discoveries.delete(chatId);
|
|
48029
|
+
} else
|
|
48030
|
+
observeRoster(value);
|
|
48031
|
+
}
|
|
48032
|
+
}
|
|
48033
|
+
function discovery(chatId, epochState) {
|
|
48034
|
+
const token = generation;
|
|
48035
|
+
const existing = discoveries.get(chatId);
|
|
48036
|
+
if (existing?.epochState.manifest.epochId === epochState.manifest.epochId)
|
|
48037
|
+
return existing;
|
|
48038
|
+
if (existing)
|
|
48039
|
+
closeDiscovery(existing);
|
|
48040
|
+
const protocol = createCallProtocol({ realm: cloud.environment, epochState, identity, endpoint });
|
|
48041
|
+
const entry = {
|
|
48042
|
+
epochState,
|
|
48043
|
+
protocol,
|
|
48044
|
+
descriptor: null,
|
|
48045
|
+
mailbox: null,
|
|
48046
|
+
observation: null,
|
|
48047
|
+
observedCallId: null,
|
|
48048
|
+
roster: [],
|
|
48049
|
+
revision: null,
|
|
48050
|
+
expiredCallId: null,
|
|
48051
|
+
finishing: new Map
|
|
48052
|
+
};
|
|
48053
|
+
entry.mailbox = openCallMailbox({
|
|
48054
|
+
cloud,
|
|
48055
|
+
capability: createCallDiscovery(epochState, cloud.environment),
|
|
48056
|
+
endpoint,
|
|
48057
|
+
protocol,
|
|
48058
|
+
onChange(record) {
|
|
48059
|
+
if (token !== generation || discoveries.get(chatId) !== entry)
|
|
48060
|
+
return;
|
|
48061
|
+
const descriptor = record.head?.data || null;
|
|
48062
|
+
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))
|
|
48063
|
+
throw new Error("invalid call discovery");
|
|
48064
|
+
const previous = entry.descriptor;
|
|
48065
|
+
entry.expiredCallId = !record.head ? previous?.callId || entry.expiredCallId : null;
|
|
48066
|
+
entry.descriptor = descriptor;
|
|
48067
|
+
if (!record.head && previous)
|
|
48068
|
+
finishDiscovery(entry, previous);
|
|
48069
|
+
const changed = entry.revision !== record.revision;
|
|
48070
|
+
entry.revision = record.revision;
|
|
48071
|
+
if (active?.discovery === entry && active.published && descriptor?.callId !== active.callId) {
|
|
48072
|
+
leave();
|
|
48073
|
+
}
|
|
48074
|
+
observeRoster(entry, changed);
|
|
48075
|
+
available();
|
|
48076
|
+
},
|
|
48077
|
+
onError(error) {
|
|
48078
|
+
if (token !== generation || discoveries.get(chatId) !== entry)
|
|
48079
|
+
return;
|
|
48080
|
+
diag?.("call.discovery.error", { code: error?.code || "calls/discovery" });
|
|
48081
|
+
}
|
|
48082
|
+
});
|
|
48083
|
+
discoveries.set(chatId, entry);
|
|
48084
|
+
return entry;
|
|
48085
|
+
}
|
|
48086
|
+
function readDiscovery(chatId, preparedEpoch) {
|
|
48087
|
+
const epochState = preparedEpoch || chat.getSnapshot().getOwnerChat(chatId)?.epochState;
|
|
48088
|
+
const epochId = epochState?.manifest.epochId;
|
|
48089
|
+
const pending = discoveryReads.get(chatId);
|
|
48090
|
+
if (pending && pending.epochId === epochId)
|
|
48091
|
+
return pending.promise;
|
|
48092
|
+
const slot = { epochId, promise: null };
|
|
48093
|
+
const token = generation;
|
|
48094
|
+
const reading = Promise.resolve().then(async () => {
|
|
48095
|
+
current(token);
|
|
48096
|
+
if (!online)
|
|
48097
|
+
throw Object.assign(new Error("chat unavailable"), { code: "unavailable" });
|
|
48098
|
+
if (!epochState)
|
|
48099
|
+
throw Object.assign(new Error("chat unavailable"), { code: "calls/chat-not-ready" });
|
|
48100
|
+
if (chat.getSnapshot().getOwnerChat(chatId)?.epochState?.manifest.epochId !== epochId)
|
|
48101
|
+
throw cancelled2();
|
|
48102
|
+
const existing = discoveries.get(chatId);
|
|
48103
|
+
const entry = discovery(chatId, epochState);
|
|
48104
|
+
if (entry === existing)
|
|
48105
|
+
await entry.mailbox.read();
|
|
48106
|
+
else
|
|
48107
|
+
await entry.mailbox.ready();
|
|
48108
|
+
current(token);
|
|
48109
|
+
return entry;
|
|
48110
|
+
}).finally(() => {
|
|
48111
|
+
if (discoveryReads.get(chatId) === slot)
|
|
48112
|
+
discoveryReads.delete(chatId);
|
|
48113
|
+
prune();
|
|
48114
|
+
});
|
|
48115
|
+
slot.promise = reading;
|
|
48116
|
+
discoveryReads.set(chatId, slot);
|
|
48117
|
+
return reading;
|
|
48118
|
+
}
|
|
48119
|
+
function observe(chatId, onAvailable) {
|
|
48120
|
+
if (!chatId || closed)
|
|
48121
|
+
return () => {};
|
|
48122
|
+
const observer = { chatId, onAvailable, fingerprint: undefined };
|
|
48123
|
+
observers.set(observer, observer);
|
|
48124
|
+
focused = chatId;
|
|
48125
|
+
if (state.error?.chatId === chatId)
|
|
48126
|
+
emit({ error: null });
|
|
48127
|
+
available();
|
|
48128
|
+
const entry = discoveries.get(chatId);
|
|
48129
|
+
if (entry)
|
|
48130
|
+
observeRoster(entry);
|
|
48131
|
+
else if (identity && online)
|
|
48132
|
+
readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
|
|
48133
|
+
return () => {
|
|
48134
|
+
if (!observers.delete(observer))
|
|
48135
|
+
return;
|
|
48136
|
+
focused = [...observers.values()].at(-1)?.chatId || null;
|
|
48137
|
+
available();
|
|
48138
|
+
prune();
|
|
48139
|
+
};
|
|
48140
|
+
}
|
|
48141
|
+
async function joinAttempt(chatId, intent, expectedCallId, onlyExisting, continuation = null) {
|
|
48142
|
+
if (!identity || !mediaPort || !mls || !cloud.calls)
|
|
48143
|
+
throw new Error("calling unavailable");
|
|
48144
|
+
if (active?.chatId === chatId)
|
|
48145
|
+
return chatId;
|
|
48146
|
+
const token = generation;
|
|
48147
|
+
const startedAt = Date.now();
|
|
48148
|
+
const step = (stage) => diag?.("call.join.stage", { stage, elapsedMs: Date.now() - startedAt });
|
|
48149
|
+
let preflightActive = true;
|
|
48150
|
+
const check = () => {
|
|
48151
|
+
current(token);
|
|
48152
|
+
if (!preflightActive || intent !== joinIntent)
|
|
48153
|
+
throw cancelled2();
|
|
48154
|
+
};
|
|
48155
|
+
const operation = joining;
|
|
48156
|
+
let preparation;
|
|
48157
|
+
let call;
|
|
48158
|
+
try {
|
|
48159
|
+
emit({ ...!active && !continuation ? { phase: "joining", chatId } : {}, error: null });
|
|
48160
|
+
const permission = (async () => {
|
|
48161
|
+
const value = continuation ? undefined : await mediaPort.prepare?.();
|
|
48162
|
+
try {
|
|
48163
|
+
check();
|
|
48164
|
+
} catch (error) {
|
|
48165
|
+
value?.close();
|
|
48166
|
+
throw error;
|
|
48167
|
+
}
|
|
48168
|
+
preparation = value;
|
|
48169
|
+
operation.preparation = value;
|
|
48170
|
+
step("permission");
|
|
48171
|
+
})();
|
|
48172
|
+
const destination = (async () => {
|
|
48173
|
+
if (!onlyExisting && !continuation)
|
|
48174
|
+
chatId = await chat.getSnapshot().materializeChat(chatId);
|
|
48175
|
+
check();
|
|
48176
|
+
joining.chatId = chatId;
|
|
48177
|
+
step("chat");
|
|
48178
|
+
const epochState = continuation ? chat.getSnapshot().getOwnerChat(chatId)?.epochState : await chat.getSnapshot().prepareCall(chatId);
|
|
48179
|
+
check();
|
|
48180
|
+
if (!epochState)
|
|
48181
|
+
throw cancelled2();
|
|
48182
|
+
const entry2 = await readDiscovery(chatId, epochState);
|
|
48183
|
+
check();
|
|
48184
|
+
step("discovery");
|
|
48185
|
+
return entry2;
|
|
48186
|
+
})();
|
|
48187
|
+
const [, entry] = await Promise.all([permission, destination]);
|
|
48188
|
+
check();
|
|
48189
|
+
const discoveryRecord = entry.mailbox.getSnapshot();
|
|
48190
|
+
const create = !entry.descriptor;
|
|
48191
|
+
if (expectedCallId && (onlyExisting || entry.epochState.manifest.lineage === "direct") && entry.descriptor?.callId !== expectedCallId)
|
|
48192
|
+
throw ended();
|
|
48193
|
+
const descriptor = entry.descriptor || {
|
|
48194
|
+
callId: continuation?.callId || toHex(randomBytes3(32)),
|
|
48195
|
+
secret: toHex(randomBytes3(32)),
|
|
48196
|
+
mode: entry.epochState.manifest.lineage,
|
|
48197
|
+
participants: 1,
|
|
48198
|
+
startedAt: continuation?.startedAt ?? Date.now()
|
|
48199
|
+
};
|
|
48200
|
+
if (continuation && descriptor.callId !== continuation.callId)
|
|
48201
|
+
throw ended();
|
|
48202
|
+
const callEndpoint = continuation?.endpoint || createCallEndpoint();
|
|
48203
|
+
call = {
|
|
48204
|
+
chatId,
|
|
48205
|
+
callId: descriptor.callId,
|
|
48206
|
+
discovery: entry,
|
|
48207
|
+
descriptor,
|
|
48208
|
+
endpoint: callEndpoint,
|
|
48209
|
+
published: false,
|
|
48210
|
+
room: null,
|
|
48211
|
+
member: null,
|
|
48212
|
+
media: null,
|
|
48213
|
+
owner: null,
|
|
48214
|
+
participants: [],
|
|
48215
|
+
error: null,
|
|
48216
|
+
timer: null,
|
|
48217
|
+
joinTimer: null,
|
|
48218
|
+
mediaReady: false,
|
|
48219
|
+
leaving: false,
|
|
48220
|
+
muteTail: Promise.resolve()
|
|
48221
|
+
};
|
|
48222
|
+
joining.candidate = call;
|
|
48223
|
+
call.owner = continuation?.owner || createLease(identity, callEndpoint);
|
|
48224
|
+
const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint: callEndpoint, callId: descriptor.callId });
|
|
48225
|
+
call.protocol = protocol;
|
|
48226
|
+
const ownMember = (value) => {
|
|
48227
|
+
try {
|
|
48228
|
+
check();
|
|
48229
|
+
if (call.leaving)
|
|
48230
|
+
throw cancelled2();
|
|
48231
|
+
} catch (error) {
|
|
48232
|
+
cleanBytes(value.snapshot);
|
|
48233
|
+
throw error;
|
|
48234
|
+
}
|
|
48235
|
+
call.member = value;
|
|
48236
|
+
return value;
|
|
48237
|
+
};
|
|
48238
|
+
const callMember = ownMember(await mls.createMember(fromHexBytes(callEndpoint.holder)));
|
|
48239
|
+
const member = create ? callMember : ownMember(await mls.createKeyPackage(callMember.snapshot));
|
|
48240
|
+
if (!create)
|
|
48241
|
+
cleanBytes(callMember.snapshot);
|
|
48242
|
+
check();
|
|
48243
|
+
step("identity");
|
|
48244
|
+
const admission = protocol.sign("join", { keyPackage: member.keyPackage ? encodeCallBytes(member.keyPackage) : "", signaturePK: toHex(member.signaturePK) });
|
|
48245
|
+
const secret = fromHexBytes(descriptor.secret);
|
|
48246
|
+
const capability = createCallCapability(secret, cloud.environment, "signaling", [descriptor.callId]);
|
|
48247
|
+
cleanBytes(secret);
|
|
48248
|
+
const valid = () => token === generation && active === call && !call.leaving;
|
|
48249
|
+
const preparing = () => token === generation && intent === joinIntent && !call.leaving;
|
|
48250
|
+
const checkCall = () => {
|
|
48251
|
+
check();
|
|
48252
|
+
if (call.leaving)
|
|
48253
|
+
throw cancelled2();
|
|
48254
|
+
if (call.error)
|
|
48255
|
+
throw call.error;
|
|
48256
|
+
};
|
|
48257
|
+
const connectionProgress = () => {
|
|
48258
|
+
if (!valid() || state.phase === "connected")
|
|
48259
|
+
return;
|
|
48260
|
+
if (call.mediaReady && descriptor.mode === "direct" && state.participants.length < 2) {
|
|
48261
|
+
clearTimeout(call.joinTimer);
|
|
48262
|
+
call.joinTimer = null;
|
|
48263
|
+
emit({ phase: "waiting" });
|
|
48264
|
+
} else {
|
|
48265
|
+
emit({ phase: "connecting" });
|
|
48266
|
+
if (!call.joinTimer)
|
|
48267
|
+
call.joinTimer = setTimeout(() => {
|
|
48268
|
+
if (valid())
|
|
48269
|
+
fail(new Error("call did not connect"));
|
|
48270
|
+
}, 30000);
|
|
48271
|
+
}
|
|
48272
|
+
};
|
|
48273
|
+
call.media = createCallMediaSession({
|
|
48274
|
+
port: mediaPort,
|
|
48275
|
+
preparation,
|
|
48276
|
+
mode: descriptor.mode,
|
|
48277
|
+
holder: callEndpoint.holder,
|
|
48278
|
+
diag,
|
|
48279
|
+
provider: (...args) => call.owner.provider(...args),
|
|
48280
|
+
signal: (...args) => call.room.signal(...args),
|
|
48281
|
+
publish: (value) => call.room.publishMedia(value),
|
|
48282
|
+
onState(value) {
|
|
48283
|
+
if (!valid())
|
|
48284
|
+
return;
|
|
48285
|
+
diag?.("call.media.state", {
|
|
48286
|
+
state: value.state,
|
|
48287
|
+
reason: value.reason || "",
|
|
48288
|
+
mode: descriptor.mode,
|
|
48289
|
+
transport: value.transport || "",
|
|
48290
|
+
participants: call.participants.length
|
|
48291
|
+
});
|
|
48292
|
+
if (value.state === "connected") {
|
|
48293
|
+
if (state.phase !== "connected")
|
|
48294
|
+
step("connected");
|
|
48295
|
+
clearTimeout(call.joinTimer);
|
|
48296
|
+
call.joinTimer = null;
|
|
48297
|
+
emit({ phase: "connected", transport: value.transport || (descriptor.mode === "group" ? "relay" : null) });
|
|
48298
|
+
} else if (value.state === "connecting") {
|
|
48299
|
+
emit({ phase: "connecting" });
|
|
48300
|
+
connectionProgress();
|
|
48301
|
+
} else if (["failed", "closed"].includes(value.state))
|
|
48302
|
+
fail(Object.assign(new Error("call connection ended"), { code: "calls/media-ended" }));
|
|
48303
|
+
},
|
|
48304
|
+
onExpired() {
|
|
48305
|
+
if (valid())
|
|
48306
|
+
fail(new Error("call connection lost"));
|
|
48307
|
+
},
|
|
48308
|
+
onSpeaking(speaking) {
|
|
48309
|
+
if (valid())
|
|
48310
|
+
emit({ speaking });
|
|
48311
|
+
},
|
|
48312
|
+
onError(error) {
|
|
48313
|
+
if (valid())
|
|
48314
|
+
fail(error);
|
|
48315
|
+
else
|
|
48316
|
+
call.error = error;
|
|
48317
|
+
}
|
|
48318
|
+
});
|
|
48319
|
+
call.room = openCallRoom({
|
|
48320
|
+
cloud,
|
|
48321
|
+
capability,
|
|
48322
|
+
protocol,
|
|
48323
|
+
endpoint: callEndpoint,
|
|
48324
|
+
mls,
|
|
48325
|
+
member,
|
|
48326
|
+
admission,
|
|
48327
|
+
callId: descriptor.callId,
|
|
48328
|
+
onKeys(value) {
|
|
48329
|
+
if (!valid() && !preparing())
|
|
48330
|
+
return;
|
|
48331
|
+
call.media.setKeys(value).catch((error) => {
|
|
48332
|
+
if (valid())
|
|
48333
|
+
fail(error);
|
|
48334
|
+
else
|
|
48335
|
+
call.error = error;
|
|
48336
|
+
});
|
|
48337
|
+
},
|
|
48338
|
+
onParticipants(value) {
|
|
48339
|
+
if (!valid() && !preparing())
|
|
48340
|
+
return;
|
|
48341
|
+
call.participants = value;
|
|
48342
|
+
call.media.setParticipants(value).catch((error) => {
|
|
48343
|
+
if (valid())
|
|
48344
|
+
fail(error);
|
|
48345
|
+
else
|
|
48346
|
+
call.error = error;
|
|
48347
|
+
});
|
|
48348
|
+
if (!valid())
|
|
48349
|
+
return;
|
|
48350
|
+
for (const peer of value) {
|
|
48351
|
+
const preference = state.peerAudio[peer.chatPK];
|
|
48352
|
+
if (preference)
|
|
48353
|
+
call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
|
|
48354
|
+
}
|
|
48355
|
+
emit({ participants: value });
|
|
48356
|
+
available();
|
|
48357
|
+
call.refresh?.();
|
|
48358
|
+
if (call.mediaReady)
|
|
48359
|
+
connectionProgress();
|
|
48360
|
+
},
|
|
48361
|
+
onSignal: (value, from) => valid() || preparing() ? call.media.signal(value, from) : undefined,
|
|
48362
|
+
onRemoved() {
|
|
48363
|
+
if (valid())
|
|
48364
|
+
leave();
|
|
48365
|
+
else
|
|
48366
|
+
call.error = cancelled2();
|
|
48367
|
+
},
|
|
48368
|
+
onError(error) {
|
|
48369
|
+
if (valid())
|
|
48370
|
+
fail(error);
|
|
48371
|
+
else
|
|
48372
|
+
call.error = error;
|
|
48373
|
+
}
|
|
48374
|
+
});
|
|
48375
|
+
await applyAudio(call);
|
|
48376
|
+
checkCall();
|
|
48377
|
+
await call.media.setMicrophoneVolume(state.microphoneVolume / 100);
|
|
48378
|
+
checkCall();
|
|
48379
|
+
await call.room.start({ create });
|
|
48380
|
+
checkCall();
|
|
48381
|
+
step("admission");
|
|
48382
|
+
if (create) {
|
|
48383
|
+
try {
|
|
48384
|
+
await entry.mailbox.commit(descriptor, [], { expectedRevision: discoveryRecord.revision });
|
|
48385
|
+
} catch (error) {
|
|
48386
|
+
if (error?.code === "calls/conflict")
|
|
48387
|
+
throw Object.assign(new Error("another call started"), { code: "calls/discovery-conflict" });
|
|
48388
|
+
throw error;
|
|
48389
|
+
}
|
|
48390
|
+
checkCall();
|
|
48391
|
+
if (!continuation)
|
|
48392
|
+
announce(chatId, descriptor.callId, "started");
|
|
48393
|
+
} else {
|
|
48394
|
+
await entry.mailbox.read();
|
|
48395
|
+
checkCall();
|
|
48396
|
+
if (entry.descriptor?.callId !== descriptor.callId) {
|
|
48397
|
+
if (expectedCallId && (onlyExisting || descriptor.mode === "direct"))
|
|
48398
|
+
throw ended();
|
|
48399
|
+
throw Object.assign(new Error("the call room changed"), { code: "calls/discovery-conflict" });
|
|
48400
|
+
}
|
|
48401
|
+
}
|
|
48402
|
+
step("publication");
|
|
48403
|
+
call.published = true;
|
|
48404
|
+
if (!continuation) {
|
|
48405
|
+
await stopCall();
|
|
48406
|
+
checkCall();
|
|
48407
|
+
mountLease(identity, callEndpoint, call.owner);
|
|
48408
|
+
participation = { chatId, callId: descriptor.callId, startedAt: descriptor.startedAt, endpoint: callEndpoint, owner: lease };
|
|
48409
|
+
}
|
|
48410
|
+
step("predecessor");
|
|
48411
|
+
call.owner = participation.owner;
|
|
48412
|
+
active = call;
|
|
48413
|
+
observeRoster(entry);
|
|
48414
|
+
available();
|
|
48415
|
+
emit({
|
|
48416
|
+
phase: continuation ? "connecting" : state.elsewhere ? "transferring" : "joining",
|
|
48417
|
+
chatId,
|
|
48418
|
+
callId: descriptor.callId,
|
|
48419
|
+
mode: descriptor.mode,
|
|
48420
|
+
error: null,
|
|
48421
|
+
speaking: [],
|
|
48422
|
+
participants: call.participants
|
|
48423
|
+
});
|
|
48424
|
+
for (const peer of call.participants) {
|
|
48425
|
+
const preference = state.peerAudio[peer.chatPK];
|
|
48426
|
+
if (preference)
|
|
48427
|
+
call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
|
|
48428
|
+
}
|
|
48429
|
+
if (continuation)
|
|
48430
|
+
await participation.owner.rotateMedia();
|
|
48431
|
+
else
|
|
48432
|
+
await lease.acquire({ chatId, callId: descriptor.callId });
|
|
48433
|
+
check();
|
|
48434
|
+
step("ownership");
|
|
48435
|
+
if (!valid())
|
|
48436
|
+
throw cancelled2();
|
|
48437
|
+
connectionProgress();
|
|
48438
|
+
await call.media.open();
|
|
48439
|
+
check();
|
|
48440
|
+
step("media");
|
|
48441
|
+
call.mediaReady = true;
|
|
48442
|
+
connectionProgress();
|
|
48443
|
+
let publishedRoster = "";
|
|
48444
|
+
let refreshing = null;
|
|
48445
|
+
let requested = false;
|
|
48446
|
+
const rosterFingerprint = () => JSON.stringify(call.participants.map(({ holder, muted, deafened }) => ({ holder, muted, deafened })).sort((a, b) => a.holder.localeCompare(b.holder)));
|
|
48447
|
+
const refresh = (heartbeat = false) => {
|
|
48448
|
+
if (!valid() || !heartbeat && publishedRoster === rosterFingerprint())
|
|
48449
|
+
return;
|
|
48450
|
+
requested = true;
|
|
48451
|
+
if (refreshing)
|
|
48452
|
+
return;
|
|
48453
|
+
clearTimeout(call.timer);
|
|
48454
|
+
refreshing = (async () => {
|
|
48455
|
+
while (requested && valid()) {
|
|
48456
|
+
requested = false;
|
|
48457
|
+
const fingerprint = rosterFingerprint();
|
|
48458
|
+
const ordered = call.participants.map((item) => item.holder).sort();
|
|
48459
|
+
if (ordered[0] === callEndpoint.holder) {
|
|
48460
|
+
const snapshot = await entry.mailbox.read();
|
|
48461
|
+
if (valid() && entry.descriptor?.callId === descriptor.callId) {
|
|
48462
|
+
await entry.mailbox.commit({ ...descriptor, participants: call.participants.length }, [], { expectedRevision: snapshot.revision });
|
|
48463
|
+
}
|
|
48464
|
+
}
|
|
48465
|
+
publishedRoster = fingerprint;
|
|
48466
|
+
}
|
|
48467
|
+
})().catch((error) => {
|
|
48468
|
+
if (valid() && error?.code !== "calls/conflict")
|
|
48469
|
+
fail(error);
|
|
48470
|
+
}).finally(() => {
|
|
48471
|
+
refreshing = null;
|
|
48472
|
+
if (valid())
|
|
48473
|
+
call.timer = setTimeout(() => refresh(true), 30000);
|
|
48474
|
+
});
|
|
48475
|
+
};
|
|
48476
|
+
call.refresh = refresh;
|
|
48477
|
+
refresh(true);
|
|
48478
|
+
return chatId;
|
|
48479
|
+
} catch (error) {
|
|
48480
|
+
const cleanup = active === call ? stopCall() : call ? disposeCall(call) : null;
|
|
48481
|
+
if (["calls/room-empty", "calls/discovery-conflict"].includes(error?.code))
|
|
48482
|
+
await cleanup;
|
|
48483
|
+
if (error?.code === "calls/room-empty" && call) {
|
|
48484
|
+
check();
|
|
48485
|
+
if (expectedCallId && (onlyExisting || call.descriptor.mode === "direct"))
|
|
48486
|
+
throw ended();
|
|
48487
|
+
await retireDiscovery(call);
|
|
48488
|
+
check();
|
|
48489
|
+
throw Object.assign(new Error("the call room is restarting"), { code: "calls/discovery-conflict" });
|
|
48490
|
+
}
|
|
48491
|
+
throw error;
|
|
48492
|
+
} finally {
|
|
48493
|
+
preflightActive = false;
|
|
48494
|
+
preparation?.close();
|
|
48495
|
+
if (operation.preparation === preparation)
|
|
48496
|
+
operation.preparation = null;
|
|
48497
|
+
}
|
|
48498
|
+
}
|
|
48499
|
+
async function join(chatId, expectedCallId = null, onlyExisting = false) {
|
|
48500
|
+
if (active?.chatId === chatId) {
|
|
48501
|
+
if (expectedCallId && (onlyExisting || active.descriptor.mode === "direct") && active.callId !== expectedCallId)
|
|
48502
|
+
throw ended();
|
|
48503
|
+
return chatId;
|
|
48504
|
+
}
|
|
48505
|
+
if (!foreground)
|
|
48506
|
+
throw new Error("open veyl to join a call");
|
|
48507
|
+
return runJoin(chatId, expectedCallId, onlyExisting);
|
|
48508
|
+
}
|
|
48509
|
+
async function runJoin(chatId, expectedCallId = null, onlyExisting = false, continuation = null) {
|
|
48510
|
+
const intent = ++joinIntent;
|
|
48511
|
+
const operation = { predecessor: active, chatId, candidate: null, continuation };
|
|
48512
|
+
const previous = joining;
|
|
48513
|
+
joining = operation;
|
|
48514
|
+
previous?.preparation?.close();
|
|
48515
|
+
emit({ joiningChatId: chatId });
|
|
48516
|
+
if (previous?.candidate && previous.candidate !== active)
|
|
48517
|
+
disposeCall(previous.candidate);
|
|
48518
|
+
try {
|
|
48519
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
48520
|
+
try {
|
|
48521
|
+
return await joinAttempt(chatId, intent, expectedCallId, onlyExisting, continuation);
|
|
48522
|
+
} catch (error) {
|
|
48523
|
+
if (intent !== joinIntent || !identity || closed)
|
|
48524
|
+
throw cancelled2();
|
|
48525
|
+
if (error?.code !== "calls/discovery-conflict" || attempt === 2) {
|
|
48526
|
+
if (continuation) {
|
|
48527
|
+
fail(error);
|
|
48528
|
+
throw error;
|
|
48529
|
+
}
|
|
48530
|
+
const target = operation.chatId;
|
|
48531
|
+
emit({ ...!active ? { phase: "error", chatId: target } : {}, error: callError(chatId, error) });
|
|
48532
|
+
throw error;
|
|
48533
|
+
}
|
|
48534
|
+
}
|
|
48535
|
+
}
|
|
48536
|
+
} finally {
|
|
48537
|
+
if (joining === operation) {
|
|
48538
|
+
joining = null;
|
|
48539
|
+
emit({ joiningChatId: null });
|
|
48540
|
+
}
|
|
48541
|
+
prune();
|
|
48542
|
+
}
|
|
48543
|
+
}
|
|
48544
|
+
function leave() {
|
|
48545
|
+
joinIntent += 1;
|
|
48546
|
+
joining?.preparation?.close();
|
|
48547
|
+
const candidate = joining?.candidate;
|
|
48548
|
+
joining = null;
|
|
48549
|
+
emit({ joiningChatId: null });
|
|
48550
|
+
const pending = candidate && candidate !== active ? disposeCall(candidate) : null;
|
|
48551
|
+
const stopped = stopCall();
|
|
48552
|
+
return Promise.all([pending, stopped, ...[...retiring].map((call) => call.closing)]).then(() => {
|
|
48553
|
+
return;
|
|
48554
|
+
});
|
|
48555
|
+
}
|
|
48556
|
+
function cancelJoin() {
|
|
48557
|
+
const operation = joining;
|
|
48558
|
+
if (!operation)
|
|
48559
|
+
return Promise.resolve();
|
|
48560
|
+
joinIntent += 1;
|
|
48561
|
+
operation.preparation?.close();
|
|
48562
|
+
joining = null;
|
|
48563
|
+
emit({ joiningChatId: null });
|
|
48564
|
+
const pending = operation.candidate && operation.candidate !== active ? disposeCall(operation.candidate) : null;
|
|
48565
|
+
return Promise.all([pending, active && active === operation.predecessor ? null : stopCall()]).then(() => {
|
|
48566
|
+
return;
|
|
48567
|
+
});
|
|
48568
|
+
}
|
|
48569
|
+
function stopCall() {
|
|
48570
|
+
const call = active;
|
|
48571
|
+
const authority = participation;
|
|
48572
|
+
participation = null;
|
|
48573
|
+
if (!call && authority) {
|
|
48574
|
+
authority.owner.retire();
|
|
48575
|
+
const retired = [...retiring].filter((room) => room.endpoint === authority.endpoint);
|
|
48576
|
+
const released = Promise.all(retired.map((room) => room.stopped)).then(() => authority.owner.release());
|
|
48577
|
+
leaving = retired.length ? Promise.race([released, Promise.all(retired.map((room) => room.closing))]) : released;
|
|
48578
|
+
}
|
|
48579
|
+
if (!call) {
|
|
48580
|
+
emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
|
|
48581
|
+
return leaving;
|
|
48582
|
+
}
|
|
48583
|
+
active = null;
|
|
48584
|
+
leaving = disposeCall(call);
|
|
48585
|
+
if (identity && discoveries.get(call.chatId) === call.discovery)
|
|
48586
|
+
observeRoster(call.discovery);
|
|
48587
|
+
available();
|
|
48588
|
+
emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
|
|
48589
|
+
return leaving;
|
|
48590
|
+
}
|
|
48591
|
+
async function retireDiscovery(call) {
|
|
48592
|
+
const entry = call.discovery;
|
|
48593
|
+
const snapshot = await entry.mailbox.read();
|
|
48594
|
+
if (snapshot.head?.data?.callId !== call.callId && !(snapshot.head === null && entry.expiredCallId === call.callId))
|
|
48595
|
+
return;
|
|
48596
|
+
try {
|
|
48597
|
+
await entry.mailbox.commit(null, [], { expectedRevision: snapshot.revision });
|
|
48598
|
+
if (chat.getSnapshot().getOwnerChat(call.chatId)?.epochState?.manifest.epochId === entry.epochState.manifest.epochId) {
|
|
48599
|
+
announce(call.chatId, call.callId, "ended");
|
|
48600
|
+
}
|
|
48601
|
+
} catch (error) {
|
|
48602
|
+
if (error?.code !== "calls/conflict")
|
|
48603
|
+
throw error;
|
|
48604
|
+
}
|
|
48605
|
+
}
|
|
48606
|
+
function finishDiscovery(entry, descriptor) {
|
|
48607
|
+
if (entry.finishing.has(descriptor.callId))
|
|
48608
|
+
return;
|
|
48609
|
+
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));
|
|
48610
|
+
entry.finishing.set(descriptor.callId, finishing);
|
|
48611
|
+
}
|
|
48612
|
+
function disposeCall(call) {
|
|
48613
|
+
if (call.closing)
|
|
48614
|
+
return call.closing;
|
|
48615
|
+
call.leaving = true;
|
|
48616
|
+
retiring.add(call);
|
|
48617
|
+
clearTimeout(call.timer);
|
|
48618
|
+
clearTimeout(call.joinTimer);
|
|
48619
|
+
const stopping = call.media?.close();
|
|
48620
|
+
call.stopped = Promise.resolve(stopping);
|
|
48621
|
+
const releasedOwner = participation?.endpoint === call.endpoint ? null : call.owner;
|
|
48622
|
+
releasedOwner?.retire();
|
|
48623
|
+
const departing = call.room?.leave();
|
|
48624
|
+
const draining = (async () => {
|
|
48625
|
+
try {
|
|
48626
|
+
const [, departure] = await Promise.allSettled([Promise.resolve(stopping).then(() => releasedOwner?.release()), departing]);
|
|
48627
|
+
if (departure.status === "fulfilled" && departure.value?.ended && call.published && call.discovery.descriptor?.callId === call.callId) {
|
|
48628
|
+
await retireDiscovery(call);
|
|
48629
|
+
}
|
|
48630
|
+
} catch (error) {
|
|
48631
|
+
diag?.("call.close.error", { code: error?.code || "" });
|
|
48632
|
+
}
|
|
48633
|
+
})();
|
|
48634
|
+
let expiry;
|
|
48635
|
+
const expired = new Promise((resolve) => {
|
|
48636
|
+
expiry = setTimeout(() => {
|
|
48637
|
+
diag?.("call.close.error", { code: "calls/retirement-timeout" });
|
|
48638
|
+
resolve();
|
|
48639
|
+
}, CALL_OWNERSHIP_LEASE_MS);
|
|
48640
|
+
});
|
|
48641
|
+
call.closing = Promise.race([draining, expired]).finally(() => {
|
|
48642
|
+
clearTimeout(expiry);
|
|
48643
|
+
call.room?.close();
|
|
48644
|
+
call.protocol?.close();
|
|
48645
|
+
cleanBytes(call.member?.snapshot);
|
|
48646
|
+
if (call.owner !== lease && call.owner !== participation?.owner)
|
|
48647
|
+
call.owner?.close();
|
|
48648
|
+
if (call.endpoint !== leaseEndpoint)
|
|
48649
|
+
call.endpoint.close();
|
|
48650
|
+
retiring.delete(call);
|
|
48651
|
+
if (discoveries.get(call.chatId) !== call.discovery)
|
|
48652
|
+
closeDiscovery(call.discovery);
|
|
48653
|
+
prune();
|
|
48654
|
+
});
|
|
48655
|
+
return call.closing;
|
|
48656
|
+
}
|
|
48657
|
+
function applyAudio(call) {
|
|
48658
|
+
const audio = { muted: state.muted || state.deafened, deafened: state.deafened };
|
|
48659
|
+
const applied = Promise.all([call.media.mute(audio.muted), call.media.deafen(audio.deafened)]);
|
|
48660
|
+
const result = Promise.all([applied, call.muteTail]).then(() => {
|
|
48661
|
+
if (!call.leaving)
|
|
48662
|
+
return call.room.setAudio(audio);
|
|
48663
|
+
});
|
|
48664
|
+
call.muteTail = result.catch(() => {});
|
|
48665
|
+
return result;
|
|
48666
|
+
}
|
|
48667
|
+
async function setAudio(patch) {
|
|
48668
|
+
const value = normalizeCallAudio(patch, state);
|
|
48669
|
+
if (value.muted === state.muted && value.deafened === state.deafened && value.muted === audioSource.muted && value.deafened === audioSource.deafened)
|
|
48670
|
+
return;
|
|
48671
|
+
const token = generation;
|
|
48672
|
+
pendingAudio = value;
|
|
48673
|
+
const saving = Promise.resolve().then(() => {
|
|
48674
|
+
if (generation !== token)
|
|
48675
|
+
throw cancelled2();
|
|
48676
|
+
return saveAudio(value);
|
|
48677
|
+
}).then(() => {
|
|
48678
|
+
if (generation === token)
|
|
48679
|
+
audioSource = value;
|
|
48680
|
+
});
|
|
48681
|
+
try {
|
|
48682
|
+
await Promise.all([saving, applyAudioPreferences(value)]);
|
|
48683
|
+
} finally {
|
|
48684
|
+
if (generation === token && pendingAudio === value)
|
|
48685
|
+
pendingAudio = null;
|
|
48686
|
+
}
|
|
48687
|
+
}
|
|
48688
|
+
async function applyAudioPreferences(patch) {
|
|
48689
|
+
emit(patch);
|
|
48690
|
+
const calls = new Set([active, joining?.candidate].filter((call) => call?.media && call.room && !call.leaving));
|
|
48691
|
+
await Promise.all([...calls].map((call) => applyAudio(call).catch((error) => {
|
|
48692
|
+
if (active === call)
|
|
48693
|
+
fail(error);
|
|
48694
|
+
else if (!call.leaving)
|
|
48695
|
+
call.error = error;
|
|
48696
|
+
throw error;
|
|
48697
|
+
})));
|
|
48698
|
+
}
|
|
48699
|
+
function syncAudio(source) {
|
|
48700
|
+
audioSource = normalizeCallAudio(source);
|
|
48701
|
+
const value = pendingAudio || audioSource;
|
|
48702
|
+
if (state.muted === value.muted && state.deafened === value.deafened)
|
|
48703
|
+
return;
|
|
48704
|
+
applyAudioPreferences(value).catch((error) => diag?.("call.audio.settings.error", { code: error?.code || "calls/audio" }));
|
|
48705
|
+
}
|
|
48706
|
+
async function setPeerAudio(chatPK, patch) {
|
|
48707
|
+
const call = active;
|
|
48708
|
+
if (!identity || typeof chatPK !== "string" || !chatPK || chatPK === identity.chatPK)
|
|
48709
|
+
throw new Error("peer identity required");
|
|
48710
|
+
const token = generation;
|
|
48711
|
+
const peer = call?.participants.find((peer2) => peer2.chatPK === chatPK);
|
|
48712
|
+
const previous = state.peerAudio[chatPK];
|
|
48713
|
+
const value = { volume: 100, muted: false, ...previous, ...patch };
|
|
48714
|
+
pendingPeerAudio.set(chatPK, value);
|
|
48715
|
+
emit({ peerAudio: { ...state.peerAudio, [chatPK]: value } });
|
|
48716
|
+
try {
|
|
48717
|
+
if (peer)
|
|
48718
|
+
await call.media.setPeerAudio(peer.holder, { volume: value.volume / 100, muted: value.muted });
|
|
48719
|
+
if (generation !== token || pendingPeerAudio.get(chatPK) !== value)
|
|
48720
|
+
return;
|
|
48721
|
+
await savePeerAudio(chatPK, value);
|
|
48722
|
+
if (generation === token)
|
|
48723
|
+
confirmedPeerAudio.set(chatPK, value);
|
|
48724
|
+
} catch (error) {
|
|
48725
|
+
if (generation === token && state.peerAudio[chatPK] === value) {
|
|
48726
|
+
const peerAudio = { ...state.peerAudio };
|
|
48727
|
+
const confirmed = confirmedPeerAudio.get(chatPK);
|
|
48728
|
+
if (confirmed)
|
|
48729
|
+
peerAudio[chatPK] = confirmed;
|
|
48730
|
+
else
|
|
48731
|
+
delete peerAudio[chatPK];
|
|
48732
|
+
emit({ peerAudio });
|
|
48733
|
+
if (peer && active === call)
|
|
48734
|
+
call.media.setPeerAudio(peer.holder, {
|
|
48735
|
+
volume: (confirmed?.volume ?? 100) / 100,
|
|
48736
|
+
muted: confirmed?.muted ?? false
|
|
48737
|
+
}).catch(() => {});
|
|
48738
|
+
}
|
|
48739
|
+
throw error;
|
|
48740
|
+
} finally {
|
|
48741
|
+
if (generation === token && pendingPeerAudio.get(chatPK) === value)
|
|
48742
|
+
pendingPeerAudio.delete(chatPK);
|
|
48743
|
+
}
|
|
48744
|
+
}
|
|
48745
|
+
function syncPeerAudio(source = {}) {
|
|
48746
|
+
if (source === peerAudioSource)
|
|
48747
|
+
return;
|
|
48748
|
+
peerAudioSource = source;
|
|
48749
|
+
const peerAudio = { ...source };
|
|
48750
|
+
for (const [chatPK, value] of pendingPeerAudio)
|
|
48751
|
+
peerAudio[chatPK] = value;
|
|
48752
|
+
confirmedPeerAudio.clear();
|
|
48753
|
+
for (const [chatPK, value] of Object.entries(source))
|
|
48754
|
+
confirmedPeerAudio.set(chatPK, value);
|
|
48755
|
+
const changed = new Set([...Object.keys(state.peerAudio), ...Object.keys(peerAudio)].filter((chatPK) => state.peerAudio[chatPK]?.volume !== peerAudio[chatPK]?.volume || state.peerAudio[chatPK]?.muted !== peerAudio[chatPK]?.muted));
|
|
48756
|
+
if (!changed.size)
|
|
48757
|
+
return;
|
|
48758
|
+
emit({ peerAudio });
|
|
48759
|
+
for (const call of new Set([active, joining?.candidate].filter((call2) => call2?.media && !call2.leaving))) {
|
|
48760
|
+
for (const peer of call.participants) {
|
|
48761
|
+
if (!changed.has(peer.chatPK))
|
|
48762
|
+
continue;
|
|
48763
|
+
const audio = peerAudio[peer.chatPK];
|
|
48764
|
+
call.media.setPeerAudio(peer.holder, { volume: (audio?.volume ?? 100) / 100, muted: audio?.muted ?? false }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
|
|
48765
|
+
}
|
|
48766
|
+
}
|
|
48767
|
+
}
|
|
48768
|
+
async function setMicrophoneVolume(volume) {
|
|
48769
|
+
if (!Number.isFinite(volume) || volume < 1 || volume > 200)
|
|
48770
|
+
throw new Error("microphone volume must be between 1 and 200");
|
|
48771
|
+
const previous = state.microphoneVolume;
|
|
48772
|
+
const token = generation;
|
|
48773
|
+
emit({ microphoneVolume: volume });
|
|
48774
|
+
const calls = new Set([active, joining?.candidate].filter((call) => call?.media && !call.leaving));
|
|
48775
|
+
try {
|
|
48776
|
+
await Promise.all([...calls].map((call) => call.media.setMicrophoneVolume(volume / 100)));
|
|
48777
|
+
} catch (error) {
|
|
48778
|
+
if (generation === token && state.microphoneVolume === volume)
|
|
48779
|
+
emit({ microphoneVolume: previous });
|
|
48780
|
+
throw error;
|
|
48781
|
+
}
|
|
48782
|
+
}
|
|
48783
|
+
function createLease(session, callEndpoint) {
|
|
48784
|
+
const ownsLease = () => identity === session && leaseEndpoint === callEndpoint;
|
|
48785
|
+
return openAccountCallLease({
|
|
48786
|
+
cloud,
|
|
48787
|
+
capability: session.callIdentity,
|
|
48788
|
+
endpoint: callEndpoint,
|
|
48789
|
+
clock: mediaPort.clock,
|
|
48790
|
+
onRecord(record, descriptor) {
|
|
48791
|
+
if (!ownsLease())
|
|
48792
|
+
return;
|
|
48793
|
+
const holder = (record.pending || record.active)?.holder;
|
|
48794
|
+
const elsewhere = descriptor && holder !== callEndpoint.holder ? descriptor : null;
|
|
48795
|
+
if (elsewhere && state.elsewhere?.chatId === elsewhere.chatId && state.elsewhere.callId === elsewhere.callId)
|
|
48796
|
+
return;
|
|
48797
|
+
emit({ elsewhere });
|
|
48798
|
+
},
|
|
48799
|
+
onGrant: (value) => ownsLease() && active?.endpoint === callEndpoint ? active.media?.grant(value) : undefined,
|
|
48800
|
+
onLost: () => {
|
|
48801
|
+
if (ownsLease())
|
|
48802
|
+
leave();
|
|
48803
|
+
},
|
|
48804
|
+
onError(error) {
|
|
48805
|
+
if (ownsLease() && active)
|
|
48806
|
+
fail(error);
|
|
48807
|
+
}
|
|
48808
|
+
});
|
|
48809
|
+
}
|
|
48810
|
+
function mountLease(session, callEndpoint = endpoint, prepared = null) {
|
|
48811
|
+
lease?.close();
|
|
48812
|
+
if (leaseEndpoint && leaseEndpoint !== endpoint && leaseEndpoint !== callEndpoint)
|
|
48813
|
+
leaseEndpoint.close();
|
|
48814
|
+
leaseEndpoint = callEndpoint;
|
|
48815
|
+
lease = prepared || createLease(session, callEndpoint);
|
|
48816
|
+
}
|
|
48817
|
+
function setSources(session, settings = {}) {
|
|
48818
|
+
const peerAudio = settings.peerAudio || {};
|
|
48819
|
+
if (identity === session) {
|
|
48820
|
+
if (session) {
|
|
48821
|
+
syncPeerAudio(peerAudio);
|
|
48822
|
+
syncAudio(settings.callAudio);
|
|
48823
|
+
}
|
|
48824
|
+
return drainingSources;
|
|
48825
|
+
}
|
|
48826
|
+
const token = ++generation;
|
|
48827
|
+
const previousLease = lease;
|
|
48828
|
+
const previousEndpoint = endpoint;
|
|
48829
|
+
const previousLeaseEndpoint = leaseEndpoint;
|
|
48830
|
+
const previousDiscoveries = [...discoveries.values()];
|
|
48831
|
+
let finishDrain, rejectDrain;
|
|
48832
|
+
const drained = new Promise((resolve, reject) => {
|
|
48833
|
+
finishDrain = resolve;
|
|
48834
|
+
rejectDrain = reject;
|
|
48835
|
+
});
|
|
48836
|
+
const retirement = Promise.all([drainingSources, drained]).then(() => {
|
|
48837
|
+
return;
|
|
48838
|
+
});
|
|
48839
|
+
drainingSources = retirement;
|
|
48840
|
+
lease = null;
|
|
48841
|
+
leaseEndpoint = null;
|
|
48842
|
+
endpoint = null;
|
|
48843
|
+
discoveries.clear();
|
|
48844
|
+
discoveryReads.clear();
|
|
48845
|
+
identity = session;
|
|
48846
|
+
const stopped = leave();
|
|
48847
|
+
Promise.resolve(stopped).finally(() => {
|
|
48848
|
+
previousLease?.close();
|
|
48849
|
+
previousLeaseEndpoint?.close();
|
|
48850
|
+
previousEndpoint?.close();
|
|
48851
|
+
for (const entry of previousDiscoveries)
|
|
48852
|
+
closeDiscovery(entry);
|
|
48853
|
+
}).then(finishDrain, rejectDrain);
|
|
48854
|
+
if (generation !== token || identity !== session)
|
|
48855
|
+
return retirement;
|
|
48856
|
+
confirmedPeerAudio.clear();
|
|
48857
|
+
pendingPeerAudio.clear();
|
|
48858
|
+
pendingAudio = null;
|
|
48859
|
+
audioSource = normalizeCallAudio(settings.callAudio);
|
|
48860
|
+
peerAudioSource = session ? peerAudio : null;
|
|
48861
|
+
for (const [chatPK, value] of Object.entries(peerAudioSource || {}))
|
|
48862
|
+
confirmedPeerAudio.set(chatPK, value);
|
|
48863
|
+
emit({ ...initial(), ...session ? audioSource : {}, peerAudio: peerAudioSource || {} });
|
|
48864
|
+
available();
|
|
48865
|
+
if (generation !== token || identity !== session)
|
|
48866
|
+
return retirement;
|
|
48867
|
+
if (!session || closed || !mediaPort || !mls || !cloud.calls)
|
|
48868
|
+
return retirement;
|
|
48869
|
+
endpoint = createCallEndpoint();
|
|
48870
|
+
mountLease(session);
|
|
48871
|
+
if (online)
|
|
48872
|
+
refreshObserved();
|
|
48873
|
+
return retirement;
|
|
48874
|
+
}
|
|
48875
|
+
function refreshObserved() {
|
|
48876
|
+
for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
|
|
48877
|
+
readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
|
|
48878
|
+
}
|
|
48879
|
+
}
|
|
48880
|
+
const unsubscribe = chat.subscribe(() => {
|
|
48881
|
+
if (!identity)
|
|
48882
|
+
return;
|
|
48883
|
+
for (const [chatId, entry] of discoveries) {
|
|
48884
|
+
const epochState = chat.getSnapshot().getOwnerChat(chatId)?.epochState;
|
|
48885
|
+
if (epochState?.manifest.epochId === entry.epochState.manifest.epochId)
|
|
48886
|
+
continue;
|
|
48887
|
+
const retained = epochState?.manifest.members.some((member) => member.chatSigningPK === identity.chatSigningPK);
|
|
48888
|
+
if (participation?.chatId === chatId && retained) {
|
|
48889
|
+
const continuation = participation;
|
|
48890
|
+
const intent = ++joinIntent;
|
|
48891
|
+
const previous = active || joining?.candidate;
|
|
48892
|
+
active = null;
|
|
48893
|
+
joining = null;
|
|
48894
|
+
if (previous)
|
|
48895
|
+
disposeCall(previous);
|
|
48896
|
+
const members = new Set(epochState.manifest.members.map((member) => member.chatPK));
|
|
48897
|
+
emit({
|
|
48898
|
+
phase: "connecting",
|
|
48899
|
+
joiningChatId: chatId,
|
|
48900
|
+
speaking: [],
|
|
48901
|
+
participants: state.participants.filter((member) => members.has(member.chatPK))
|
|
48902
|
+
});
|
|
48903
|
+
diag?.("call.epoch.transition", { stage: "starting", participants: state.participants.length });
|
|
48904
|
+
Promise.resolve(previous?.stopped).then(() => {
|
|
48905
|
+
if (intent !== joinIntent || participation !== continuation)
|
|
48906
|
+
return;
|
|
48907
|
+
return runJoin(chatId, null, false, continuation);
|
|
48908
|
+
}).catch((error) => {
|
|
48909
|
+
if (intent === joinIntent && participation === continuation)
|
|
48910
|
+
fail(error);
|
|
48911
|
+
});
|
|
48912
|
+
} else if (active?.chatId === chatId || participation?.chatId === chatId)
|
|
48913
|
+
leave();
|
|
48914
|
+
else if (joining?.chatId === chatId)
|
|
48915
|
+
cancelJoin();
|
|
48916
|
+
closeDiscovery(entry);
|
|
48917
|
+
discoveries.delete(chatId);
|
|
48918
|
+
available();
|
|
48919
|
+
if (online && [...observers.values()].some((item) => item.chatId === chatId))
|
|
48920
|
+
readDiscovery(chatId).catch(() => {});
|
|
48921
|
+
}
|
|
48922
|
+
if (online)
|
|
48923
|
+
for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
|
|
48924
|
+
if (!discoveries.has(chatId) && !discoveryReads.has(chatId) && chat.getSnapshot().getOwnerChat(chatId)?.epochState) {
|
|
48925
|
+
readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
|
|
48926
|
+
}
|
|
48927
|
+
}
|
|
48928
|
+
});
|
|
48929
|
+
return Object.freeze({
|
|
48930
|
+
getSnapshot: () => state,
|
|
48931
|
+
subscribe(listener) {
|
|
48932
|
+
listeners.add(listener);
|
|
48933
|
+
return () => listeners.delete(listener);
|
|
48934
|
+
},
|
|
48935
|
+
setSources,
|
|
48936
|
+
observe,
|
|
48937
|
+
join,
|
|
48938
|
+
leave,
|
|
48939
|
+
cancelJoin,
|
|
48940
|
+
setMicrophoneVolume,
|
|
48941
|
+
async resolveActiveCall(chatId) {
|
|
48942
|
+
const entry = await readDiscovery(chatId);
|
|
48943
|
+
return entry.descriptor?.callId || null;
|
|
48944
|
+
},
|
|
48945
|
+
joinFromMessage(chatId, callId) {
|
|
48946
|
+
if (!/^[a-f0-9]{64}$/u.test(callId))
|
|
48947
|
+
return Promise.reject(ended());
|
|
48948
|
+
return join(chatId, callId);
|
|
48949
|
+
},
|
|
48950
|
+
joinExisting(chatId, callId) {
|
|
48951
|
+
if (!/^[a-f0-9]{64}$/u.test(callId))
|
|
48952
|
+
return Promise.reject(ended());
|
|
48953
|
+
return join(chatId, callId, true);
|
|
48954
|
+
},
|
|
48955
|
+
setMuted(value) {
|
|
48956
|
+
return setAudio({ muted: value === true });
|
|
48957
|
+
},
|
|
48958
|
+
setDeafened(value) {
|
|
48959
|
+
return setAudio({ deafened: value === true });
|
|
48960
|
+
},
|
|
48961
|
+
setPeerVolume(chatPK, volume) {
|
|
48962
|
+
if (!Number.isFinite(volume) || volume < 1 || volume > 200)
|
|
48963
|
+
return Promise.reject(new Error("peer volume must be between 1 and 200"));
|
|
48964
|
+
return setPeerAudio(chatPK, { volume });
|
|
48965
|
+
},
|
|
48966
|
+
setPeerMuted(chatPK, muted) {
|
|
48967
|
+
return setPeerAudio(chatPK, { muted: muted === true });
|
|
48968
|
+
},
|
|
48969
|
+
setForeground(value) {
|
|
48970
|
+
foreground = value === true;
|
|
48971
|
+
},
|
|
48972
|
+
setOnline(value) {
|
|
48973
|
+
const reconnecting = !online && value === true;
|
|
48974
|
+
online = value === true;
|
|
48975
|
+
if (reconnecting && identity && !closed)
|
|
48976
|
+
refreshObserved();
|
|
48977
|
+
},
|
|
48978
|
+
close() {
|
|
48979
|
+
if (closed)
|
|
48980
|
+
return closingSession;
|
|
48981
|
+
let finish, reject;
|
|
48982
|
+
closingSession = new Promise((resolve, fail2) => {
|
|
48983
|
+
finish = resolve;
|
|
48984
|
+
reject = fail2;
|
|
48985
|
+
});
|
|
48986
|
+
closed = true;
|
|
48987
|
+
const stopped = setSources(null);
|
|
48988
|
+
unsubscribe();
|
|
48989
|
+
listeners.clear();
|
|
48990
|
+
Promise.resolve(stopped).finally(() => Promise.all([mls?.close?.(), mediaPort?.close?.()])).then(finish, reject);
|
|
48991
|
+
return closingSession;
|
|
48992
|
+
}
|
|
48993
|
+
});
|
|
48994
|
+
}
|
|
48995
|
+
|
|
45657
48996
|
// ../../node_modules/.bun/@zxcvbn-ts+core@4.2.0/node_modules/@zxcvbn-ts/core/dist/utils/helper.mjs
|
|
45658
48997
|
var extend = (listToExtend, list) => listToExtend.push.apply(listToExtend, list);
|
|
45659
48998
|
var sorted = (matches) => matches.sort((m1, m2) => m1.i - m2.i || m1.j - m2.j);
|
|
@@ -48650,15 +51989,17 @@ var DEFAULT_BITCOIN = Object.freeze({
|
|
|
48650
51989
|
ready: false,
|
|
48651
51990
|
error: null
|
|
48652
51991
|
});
|
|
48653
|
-
function normalizeBitcoinData(data, current = DEFAULT_BITCOIN
|
|
51992
|
+
function normalizeBitcoinData(data, current = DEFAULT_BITCOIN) {
|
|
48654
51993
|
if (!data)
|
|
48655
51994
|
return current;
|
|
51995
|
+
const observedAt = timestampMs(data.priceUpdatedAt, null, { positive: true });
|
|
51996
|
+
const usePrice = hasBitcoinPrice(data.price) && observedAt != null && observedAt >= (current.priceUpdatedAt ?? 0);
|
|
48656
51997
|
return {
|
|
48657
|
-
price:
|
|
48658
|
-
priceUpdatedAt:
|
|
48659
|
-
priceFromCache:
|
|
51998
|
+
price: usePrice ? data.price : current.price,
|
|
51999
|
+
priceUpdatedAt: usePrice ? observedAt : current.priceUpdatedAt,
|
|
52000
|
+
priceFromCache: usePrice ? false : current.priceFromCache,
|
|
48660
52001
|
block: data.block ?? current?.block ?? null,
|
|
48661
|
-
fees: data.fees ??
|
|
52002
|
+
fees: data.fees ?? null,
|
|
48662
52003
|
updatedAt: data.updatedAt ?? current?.updatedAt ?? null,
|
|
48663
52004
|
ready: true,
|
|
48664
52005
|
error: null
|
|
@@ -48708,20 +52049,15 @@ function createBitcoin({ cloud, priceStorage = null, diag = null }) {
|
|
|
48708
52049
|
const priceUpdatedAt = timestampMs(cached.updatedAt, null, { positive: true });
|
|
48709
52050
|
if (!priceUpdatedAt)
|
|
48710
52051
|
return;
|
|
48711
|
-
if (bitcoin.priceUpdatedAt != null &&
|
|
52052
|
+
if (bitcoin.priceUpdatedAt != null && bitcoin.priceUpdatedAt >= priceUpdatedAt)
|
|
48712
52053
|
return;
|
|
48713
52054
|
publish({ ...bitcoin, price: cached.price, priceUpdatedAt, priceFromCache: true });
|
|
48714
52055
|
}).catch((error) => diag?.("bitcoin.price.cache.read.error", { code: error?.code || "" }));
|
|
48715
|
-
unsubscribe = cloud.bitcoin.watch((data
|
|
48716
|
-
if (session !== generation
|
|
52056
|
+
unsubscribe = cloud.bitcoin.watch((data) => {
|
|
52057
|
+
if (session !== generation)
|
|
48717
52058
|
return;
|
|
48718
|
-
const next = normalizeBitcoinData(data, bitcoin
|
|
48719
|
-
if (
|
|
48720
|
-
next.price = bitcoin.price;
|
|
48721
|
-
next.priceUpdatedAt = bitcoin.priceUpdatedAt;
|
|
48722
|
-
next.priceFromCache = bitcoin.priceFromCache;
|
|
48723
|
-
}
|
|
48724
|
-
if (priceStorage && hasBitcoinPrice(data?.price) && !info.fromCache && (next.price !== bitcoin.price || next.priceUpdatedAt !== bitcoin.priceUpdatedAt || bitcoin.priceFromCache)) {
|
|
52059
|
+
const next = normalizeBitcoinData(data, bitcoin);
|
|
52060
|
+
if (priceStorage && next.priceUpdatedAt != null && !next.priceFromCache && (next.price !== bitcoin.price || next.priceUpdatedAt !== bitcoin.priceUpdatedAt || bitcoin.priceFromCache)) {
|
|
48725
52061
|
const cached = { price: next.price, updatedAt: next.priceUpdatedAt };
|
|
48726
52062
|
priceWrite = priceWrite.catch(NOOP3).then(() => priceStorage.write(cached));
|
|
48727
52063
|
priceWrite.catch((error) => diag?.("bitcoin.price.cache.write.error", { code: error?.code || "" }));
|
|
@@ -49657,11 +52993,11 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
|
|
|
49657
52993
|
return null;
|
|
49658
52994
|
}
|
|
49659
52995
|
}
|
|
49660
|
-
async function writeCachedAvatar(uid, version,
|
|
49661
|
-
if (!uid || version == null || !
|
|
52996
|
+
async function writeCachedAvatar(uid, version, bytes2) {
|
|
52997
|
+
if (!uid || version == null || !bytes2 || typeof avatarCache?.write !== "function")
|
|
49662
52998
|
return null;
|
|
49663
52999
|
try {
|
|
49664
|
-
const cached = await avatarCache.write(uid, { version, bytes });
|
|
53000
|
+
const cached = await avatarCache.write(uid, { version, bytes: bytes2 });
|
|
49665
53001
|
return typeof cached === "string" && cached ? cached : typeof cached?.url === "string" && cached.url ? cached.url : typeof cached?.source === "string" && cached.source ? cached.source : null;
|
|
49666
53002
|
} catch {
|
|
49667
53003
|
return null;
|
|
@@ -49675,8 +53011,8 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
|
|
|
49675
53011
|
}
|
|
49676
53012
|
if (avatarVersion != null && typeof cloud.peer.avatar.read === "function") {
|
|
49677
53013
|
try {
|
|
49678
|
-
const
|
|
49679
|
-
const cachedSource = await writeCachedAvatar(uid, avatarVersion,
|
|
53014
|
+
const bytes2 = await cloud.peer.avatar.read(uid, { version: avatarVersion });
|
|
53015
|
+
const cachedSource = await writeCachedAvatar(uid, avatarVersion, bytes2);
|
|
49680
53016
|
if (cachedSource) {
|
|
49681
53017
|
return cachedSource;
|
|
49682
53018
|
}
|
|
@@ -49852,8 +53188,9 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
|
|
|
49852
53188
|
async function fetchProfileByUid(uid) {
|
|
49853
53189
|
if (!uid)
|
|
49854
53190
|
return null;
|
|
49855
|
-
|
|
49856
|
-
|
|
53191
|
+
const cached = profileCache.get(uid);
|
|
53192
|
+
if (isFullProfile(cached))
|
|
53193
|
+
return cached;
|
|
49857
53194
|
try {
|
|
49858
53195
|
const record = await cloud.peer.read(uid);
|
|
49859
53196
|
if (!record)
|
|
@@ -49901,11 +53238,11 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
|
|
|
49901
53238
|
return null;
|
|
49902
53239
|
if (field === "walletPK" && walletToUid.has(value)) {
|
|
49903
53240
|
const uid = walletToUid.get(value);
|
|
49904
|
-
return
|
|
53241
|
+
return fetchProfileByUid(uid);
|
|
49905
53242
|
}
|
|
49906
53243
|
if (field === "chatPK" && chatToUid.has(value)) {
|
|
49907
53244
|
const uid = chatToUid.get(value);
|
|
49908
|
-
return
|
|
53245
|
+
return fetchProfileByUid(uid);
|
|
49909
53246
|
}
|
|
49910
53247
|
try {
|
|
49911
53248
|
const record = field === "walletPK" ? await cloud.search.peer.byWalletPK(value, { network: walletNetwork }) : field === "chatPK" ? await cloud.search.peer.byChatPK(value) : await cloud.search.peer.byUsername(value);
|
|
@@ -55084,12 +58421,12 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
|
|
|
55084
58421
|
reason,
|
|
55085
58422
|
active: sdkActivityCountRef.current
|
|
55086
58423
|
});
|
|
55087
|
-
let
|
|
58424
|
+
let ended2 = false;
|
|
55088
58425
|
return () => {
|
|
55089
|
-
if (
|
|
58426
|
+
if (ended2) {
|
|
55090
58427
|
return;
|
|
55091
58428
|
}
|
|
55092
|
-
|
|
58429
|
+
ended2 = true;
|
|
55093
58430
|
sdkActivityCountRef.current = Math.max(0, sdkActivityCountRef.current - 1);
|
|
55094
58431
|
sdkActivityQuietUntilRef.current = Math.max(sdkActivityQuietUntilRef.current, Date.now() + SDK_BACKGROUND_QUIET_MS);
|
|
55095
58432
|
markDiag(diag, "wallet.sdkActivity.done", {
|
|
@@ -57304,8 +60641,17 @@ function openAccount(options = {}) {
|
|
|
57304
60641
|
getPeerProfile: getChatPeerProfile,
|
|
57305
60642
|
refreshPeerProfile: refreshChatPeerProfile,
|
|
57306
60643
|
resolvePeerProfile: resolveChatPeerProfile,
|
|
57307
|
-
maintenance: messageMaintenance
|
|
60644
|
+
maintenance: messageMaintenance,
|
|
60645
|
+
resolveActiveCall: (chatId) => calls.resolveActiveCall(chatId)
|
|
57308
60646
|
}, chatSources());
|
|
60647
|
+
const calls = createCallSession({
|
|
60648
|
+
cloud,
|
|
60649
|
+
chat,
|
|
60650
|
+
...options.calls,
|
|
60651
|
+
diag,
|
|
60652
|
+
savePeerAudio: (chatPK, audio) => user.getSnapshot().updateSettings({ peerAudio: { [chatPK]: audio } }),
|
|
60653
|
+
saveAudio: (callAudio) => user.getSnapshot().updateSettings({ callAudio })
|
|
60654
|
+
});
|
|
57309
60655
|
const wallet = openWallet({
|
|
57310
60656
|
...options.wallet || {},
|
|
57311
60657
|
network: state.network,
|
|
@@ -57378,6 +60724,8 @@ function openAccount(options = {}) {
|
|
|
57378
60724
|
syncWallet();
|
|
57379
60725
|
syncPresence();
|
|
57380
60726
|
syncPeers();
|
|
60727
|
+
calls.setOnline(state.online);
|
|
60728
|
+
calls.setSources(state.lockState === "unlocked" ? state.session : null, state.user?.settings);
|
|
57381
60729
|
}
|
|
57382
60730
|
function syncPresence() {
|
|
57383
60731
|
const session = state.session;
|
|
@@ -57413,7 +60761,7 @@ function openAccount(options = {}) {
|
|
|
57413
60761
|
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
60762
|
}),
|
|
57415
60763
|
preview: (value) => user.previewAvatar(value),
|
|
57416
|
-
write: (uid,
|
|
60764
|
+
write: (uid, bytes2) => cloud.user.profile.avatar.set(uid, bytes2),
|
|
57417
60765
|
commit: (change) => user.confirmAvatar(change),
|
|
57418
60766
|
onError: (error) => diag?.("account.avatar.update.error", { code: error?.code || "" })
|
|
57419
60767
|
});
|
|
@@ -57427,11 +60775,11 @@ function openAccount(options = {}) {
|
|
|
57427
60775
|
await cloud.user.username.get(username);
|
|
57428
60776
|
return { username };
|
|
57429
60777
|
},
|
|
57430
|
-
async setAvatar(
|
|
60778
|
+
async setAvatar(bytes2) {
|
|
57431
60779
|
profileUid();
|
|
57432
|
-
if (!
|
|
60780
|
+
if (!bytes2)
|
|
57433
60781
|
throw new Error("avatar data required");
|
|
57434
|
-
return avatarUpdate.set(sanitizeAvatar(
|
|
60782
|
+
return avatarUpdate.set(sanitizeAvatar(bytes2));
|
|
57435
60783
|
},
|
|
57436
60784
|
async clearAvatar() {
|
|
57437
60785
|
profileUid();
|
|
@@ -57562,13 +60910,32 @@ function openAccount(options = {}) {
|
|
|
57562
60910
|
return;
|
|
57563
60911
|
foreground = value === true;
|
|
57564
60912
|
syncPresence();
|
|
60913
|
+
calls.setForeground(foreground);
|
|
57565
60914
|
}
|
|
57566
60915
|
function closeSession({ notify: notify2 = true } = {}) {
|
|
57567
|
-
avatarUpdate.reset();
|
|
57568
|
-
attempt += 1;
|
|
57569
|
-
chat.setInboxEnabled(false);
|
|
57570
60916
|
const session = state.session;
|
|
60917
|
+
attempt += 1;
|
|
57571
60918
|
chatSession = null;
|
|
60919
|
+
const patch = {
|
|
60920
|
+
session: null,
|
|
60921
|
+
wallet: null,
|
|
60922
|
+
walletError: null,
|
|
60923
|
+
lockState: "locked",
|
|
60924
|
+
online: false,
|
|
60925
|
+
onlineError: null,
|
|
60926
|
+
agreementSession: null
|
|
60927
|
+
};
|
|
60928
|
+
state = { ...state, ...patch };
|
|
60929
|
+
const callRetirement = calls.setSources(null);
|
|
60930
|
+
if (callRetirement) {
|
|
60931
|
+
const work = Promise.resolve(callRetirement).catch((error) => {
|
|
60932
|
+
diag?.("account.calls.close.error", { code: error?.code || "" });
|
|
60933
|
+
});
|
|
60934
|
+
sessionCloseWork.add(work);
|
|
60935
|
+
work.finally(() => sessionCloseWork.delete(work));
|
|
60936
|
+
}
|
|
60937
|
+
avatarUpdate.reset();
|
|
60938
|
+
chat.setInboxEnabled(false);
|
|
57572
60939
|
closeAccountSession(session);
|
|
57573
60940
|
if (session && typeof options.onSessionClosed === "function") {
|
|
57574
60941
|
let result;
|
|
@@ -57588,19 +60955,9 @@ function openAccount(options = {}) {
|
|
|
57588
60955
|
}
|
|
57589
60956
|
state.user.lockSettings?.();
|
|
57590
60957
|
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
60958
|
if (notify2) {
|
|
57601
|
-
emitDomains(
|
|
60959
|
+
emitDomains({});
|
|
57602
60960
|
} else {
|
|
57603
|
-
state = { ...state, ...patch };
|
|
57604
60961
|
syncDomains();
|
|
57605
60962
|
}
|
|
57606
60963
|
return session;
|
|
@@ -58162,12 +61519,14 @@ function openAccount(options = {}) {
|
|
|
58162
61519
|
stopPeerAdmissionProjection();
|
|
58163
61520
|
stopPresence();
|
|
58164
61521
|
presence.close();
|
|
61522
|
+
const callClosing = calls.close();
|
|
58165
61523
|
clearMissingPeers();
|
|
58166
61524
|
wallet.close();
|
|
58167
61525
|
chat.close();
|
|
58168
61526
|
peers.close();
|
|
58169
61527
|
listeners.clear();
|
|
58170
61528
|
operationBarrier.finishClose();
|
|
61529
|
+
return callClosing;
|
|
58171
61530
|
});
|
|
58172
61531
|
return closePromise;
|
|
58173
61532
|
}
|
|
@@ -58183,6 +61542,7 @@ function openAccount(options = {}) {
|
|
|
58183
61542
|
messageMaintenance,
|
|
58184
61543
|
peers,
|
|
58185
61544
|
presence,
|
|
61545
|
+
calls,
|
|
58186
61546
|
closeBarrier: Object.freeze({
|
|
58187
61547
|
acquire: operationBarrier.acquireClose,
|
|
58188
61548
|
release: operationBarrier.releaseClose,
|