@glyphteck/veyl 0.73.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/index.js CHANGED
@@ -5855,7 +5855,7 @@ var init_canonical = __esm(() => {
5855
5855
  });
5856
5856
 
5857
5857
  // ../../core/config.js
5858
- var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, ACCOUNT_CONNECTION_SLOW_MS, ACCOUNT_CONNECTION_UNAVAILABLE_MS, ACCOUNT_CONNECTION_RETRY_MIN_MS, ACCOUNT_CONNECTION_RETRY_MAX_MS, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, BTC_PRICE_FALLBACK = 80000, CHAT_MESSAGE_EDIT_WINDOW_MS, CHAT_SEND_CONNECTION_WAIT_MS, CALL_SPEAKING_THRESHOLD = 0.02, CALL_SPEAKING_HOLD_MS = 320, LOCAL_CACHE_WRITE_DELAY_MS = 350, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, AVATAR_IMAGE_QUALITY_ATTEMPTS, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS = 256, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_MESSAGE_MUTATION_MAX_ITEMS = 256, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_ADMISSION_CACHE_PEERS = 1024, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_DISCOVERY_PARALLEL_CHATS, CHAT_INBOX_SETTLEMENT_PARALLEL_CHATS = 4, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_COMPLETE_SYNC_DELAY_MS = 250, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, LIVE_RECONNECT_MIN_MS = 500, LIVE_RECONNECT_MAX_MS, LIVE_RECONNECT_STABLE_MS, LIVE_READY_TIMEOUT_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_TITLE_STATE_MAX_CHARS = 128, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_LARGE_GROUP_MENTION_MAX_TARGETS = 16, CHAT_MAX_TEXT_CHARS = 2048, CHAT_TITLE_INPUT_MAX_CHARS = 16, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_ACTIVE_TRANSFER_REFRESH_MS, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS;
5858
+ var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, ACCOUNT_CONNECTION_SLOW_MS, ACCOUNT_CONNECTION_UNAVAILABLE_MS, ACCOUNT_CONNECTION_RETRY_MIN_MS, ACCOUNT_CONNECTION_RETRY_MAX_MS, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, BTC_PRICE_FALLBACK = 80000, CHAT_MESSAGE_EDIT_WINDOW_MS, CHAT_SEND_CONNECTION_WAIT_MS, CALL_SPEAKING_THRESHOLD = 0.02, CALL_SPEAKING_HOLD_MS = 320, LOCAL_CACHE_WRITE_DELAY_MS = 350, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, AVATAR_IMAGE_QUALITY_ATTEMPTS, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS = 256, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_MESSAGE_MUTATION_MAX_ITEMS = 256, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_ADMISSION_CACHE_PEERS = 1024, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_DISCOVERY_PARALLEL_CHATS, CHAT_INBOX_SETTLEMENT_PARALLEL_CHATS = 4, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_COMPLETE_SYNC_DELAY_MS = 250, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, LIVE_RECONNECT_MIN_MS = 500, LIVE_RECONNECT_MAX_MS, LIVE_RECONNECT_STABLE_MS, LIVE_READY_TIMEOUT_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_TITLE_STATE_MAX_CHARS = 128, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_LARGE_GROUP_MENTION_MAX_TARGETS = 16, CHAT_MAX_TEXT_CHARS = 2048, CHAT_TITLE_INPUT_MAX_CHARS = 16, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_ACTIVE_TRANSFER_REFRESH_MS, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS, BITCOIN_POLL_MS = 60000, BITCOIN_FETCH_TIMEOUT_MS = 8000, BITCOIN_FEES_MAX_AGE_MS;
5859
5859
  var init_config = __esm(() => {
5860
5860
  MINUTE_MS = 60 * MS_PER_SECOND;
5861
5861
  HOUR_MS = 60 * MINUTE_MS;
@@ -5927,6 +5927,7 @@ var init_config = __esm(() => {
5927
5927
  WALLET_PENDING_TRANSFER_STUCK_RETRY_MS = 10 * MINUTE_MS;
5928
5928
  WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS = HOUR_MS;
5929
5929
  WALLET_TRANSFER_CACHE_WRITE_DELAY_MS = 3 * MS_PER_SECOND;
5930
+ BITCOIN_FEES_MAX_AGE_MS = 5 * 60000;
5930
5931
  });
5931
5932
 
5932
5933
  // ../../core/calls/wire.js
@@ -6835,7 +6836,7 @@ function getVeylDevWebOrigin(port) {
6835
6836
  }
6836
6837
  return `https://${domains.veylDev}:${value}`;
6837
6838
  }
6838
- var ROOT_DOMAIN = "glyphteck.com", VEYL_DEV_WEB_PORT_MIN = 3000, VEYL_DEV_WEB_PORT_MAX = 3015, domains, veylDevWebOrigins, origins, liveEndpoints, appDomains, appLinkDomains;
6839
+ var ROOT_DOMAIN = "glyphteck.com", VEYL_DEV_WEB_PORT_MIN = 3000, VEYL_DEV_WEB_PORT_MAX = 3015, domains, veylDevWebOrigins, origins, liveEndpoints, bitcoinEndpoint, appDomains, appLinkDomains;
6839
6840
  var init_origins = __esm(() => {
6840
6841
  domains = Object.freeze({
6841
6842
  root: ROOT_DOMAIN,
@@ -6843,7 +6844,8 @@ var init_origins = __esm(() => {
6843
6844
  veyl: `veyl.${ROOT_DOMAIN}`,
6844
6845
  veylDev: `dev.veyl.${ROOT_DOMAIN}`,
6845
6846
  live: `live.veyl.${ROOT_DOMAIN}`,
6846
- liveDev: `live.dev.veyl.${ROOT_DOMAIN}`
6847
+ liveDev: `live.dev.veyl.${ROOT_DOMAIN}`,
6848
+ bitcoin: `bitcoin.veyl.${ROOT_DOMAIN}`
6847
6849
  });
6848
6850
  veylDevWebOrigins = Object.freeze(Array.from({ length: VEYL_DEV_WEB_PORT_MAX - VEYL_DEV_WEB_PORT_MIN + 1 }, (_, index) => getVeylDevWebOrigin(VEYL_DEV_WEB_PORT_MIN + index)));
6849
6851
  origins = Object.freeze({
@@ -6857,6 +6859,7 @@ var init_origins = __esm(() => {
6857
6859
  prod: `wss://${domains.live}`,
6858
6860
  dev: `wss://${domains.liveDev}`
6859
6861
  });
6862
+ bitcoinEndpoint = `https://${domains.bitcoin}/current`;
6860
6863
  appDomains = Object.freeze([
6861
6864
  domains.veyl
6862
6865
  ]);
@@ -6950,9 +6953,14 @@ function normalizeSettings(settings, base = defaultSettings) {
6950
6953
  };
6951
6954
  next.autolock = normalizeAutolock(settings.autolock, current.autolock);
6952
6955
  next.walletNetwork = normalizeWalletNetworkSetting(next.walletNetwork);
6956
+ next.peerAudio = normalizePeerAudio(settings.peerAudio, current.peerAudio);
6957
+ next.callAudio = normalizeCallAudio(settings.callAudio, current.callAudio);
6953
6958
  if (!MONEY_FORMATS.includes(next.moneyFormat)) {
6954
6959
  throw new Error("bad moneyFormat");
6955
6960
  }
6961
+ if (!WEB_LAYOUTS.includes(next.webLayout)) {
6962
+ throw new Error("bad webLayout");
6963
+ }
6956
6964
  if (typeof next.glass !== "boolean") {
6957
6965
  throw new Error("glass must be boolean");
6958
6966
  }
@@ -6976,14 +6984,42 @@ function normalizeSettings(settings, base = defaultSettings) {
6976
6984
  }
6977
6985
  return next;
6978
6986
  }
6979
- var SEND_ON_SCAN_ENABLED = false, WALLET_NETWORKS, defaultSettings;
6987
+ function normalizeCallAudio(patch, base = defaultSettings.callAudio) {
6988
+ if (patch !== undefined && (!patch || typeof patch !== "object" || Array.isArray(patch)))
6989
+ throw new Error("bad call audio settings");
6990
+ const { muted, deafened } = { ...defaultSettings.callAudio, ...base, ...patch };
6991
+ if (typeof muted !== "boolean" || typeof deafened !== "boolean")
6992
+ throw new Error("bad call audio settings");
6993
+ return { muted, deafened };
6994
+ }
6995
+ function normalizePeerAudio(patch, base = {}) {
6996
+ if (patch === undefined)
6997
+ return base;
6998
+ if (!patch || typeof patch !== "object" || Array.isArray(patch))
6999
+ throw new Error("bad peer audio settings");
7000
+ const entries = new Map(Object.entries(base));
7001
+ for (const [chatPK, audio] of Object.entries(patch)) {
7002
+ if (!/^[a-f0-9]{64}$/u.test(chatPK) || !audio || typeof audio !== "object" || Array.isArray(audio)) {
7003
+ throw new Error("bad peer audio settings");
7004
+ }
7005
+ const { volume = 100, muted = false } = { ...entries.get(chatPK), ...audio };
7006
+ if (!Number.isFinite(volume) || volume < 1 || volume > 200 || typeof muted !== "boolean") {
7007
+ throw new Error("bad peer audio settings");
7008
+ }
7009
+ entries.set(chatPK, { volume, muted });
7010
+ }
7011
+ return Object.fromEntries(entries);
7012
+ }
7013
+ var SEND_ON_SCAN_ENABLED = false, WEB_LAYOUTS, WALLET_NETWORKS, defaultSettings;
6980
7014
  var init_settings = __esm(() => {
6981
7015
  init_config();
6982
7016
  init_money();
6983
7017
  init_network();
7018
+ WEB_LAYOUTS = ["floating", "sidebar"];
6984
7019
  WALLET_NETWORKS = new Set([MAINNET_NETWORK, REGTEST_NETWORK]);
6985
7020
  defaultSettings = {
6986
7021
  glass: true,
7022
+ webLayout: "floating",
6987
7023
  moneyFormat: "usd",
6988
7024
  ghostWallet: true,
6989
7025
  showChatPreviews: true,
@@ -6991,6 +7027,8 @@ var init_settings = __esm(() => {
6991
7027
  confirmSend: false,
6992
7028
  faceID: null,
6993
7029
  walletNetwork: null,
7030
+ peerAudio: {},
7031
+ callAudio: { muted: false, deafened: false },
6994
7032
  autolock: {
6995
7033
  timer: "never",
6996
7034
  onHide: false,
@@ -8109,6 +8147,9 @@ function normalizeBitcoinPaymentFeeQuote(feeQuote) {
8109
8147
  };
8110
8148
  }
8111
8149
  function getFeeRateSatsPerVbyte(bitcoin, speed = DEFAULT_FEE_RATE_SPEED) {
8150
+ const observedAt = Date.parse(bitcoin?.fees?.updatedAtIso);
8151
+ if (!Number.isFinite(observedAt) || Date.now() - observedAt > BITCOIN_FEES_MAX_AGE_MS || observedAt > Date.now() + 60000)
8152
+ return null;
8112
8153
  const rates = bitcoin?.fees?.satPerVbyte;
8113
8154
  const key = String(speed || DEFAULT_FEE_RATE_SPEED);
8114
8155
  const keys = FEE_RATE_FALLBACKS[key] ?? [key, ...FEE_RATE_FALLBACKS.default];
@@ -8209,6 +8250,7 @@ function normalizeLightningPaymentResult(result) {
8209
8250
  }
8210
8251
  var DEFAULT_EXIT_SPEED = "MEDIUM", EXIT_SPEEDS, UNILATERAL_EXIT_PARENT_TX_FALLBACK_VBYTES = 191, UNILATERAL_EXIT_FEE_BUMP_TX_VBYTES = 151, UNILATERAL_EXIT_PACKAGES_PER_LEAF = 2, UNILATERAL_EXIT_PREVIEW_VBYTES, DEFAULT_FEE_RATE_SPEED = "medium", FEE_RATE_FALLBACKS, BITCOIN_PAYMENT_FEE_FIELDS;
8211
8252
  var init_fees = __esm(() => {
8253
+ init_config();
8212
8254
  EXIT_SPEEDS = Object.freeze(["SLOW", "MEDIUM", "FAST"]);
8213
8255
  UNILATERAL_EXIT_PREVIEW_VBYTES = UNILATERAL_EXIT_PACKAGES_PER_LEAF * (UNILATERAL_EXIT_PARENT_TX_FALLBACK_VBYTES + UNILATERAL_EXIT_FEE_BUMP_TX_VBYTES);
8214
8256
  FEE_RATE_FALLBACKS = Object.freeze({
@@ -9375,6 +9417,9 @@ function readAccountType(profile) {
9375
9417
  function hasPeerKeys(profile) {
9376
9418
  return !!(profile?.walletPK || profile?.chatPK);
9377
9419
  }
9420
+ function hasChatIdentity(profile) {
9421
+ return !!(profile?.uid && profile?.chatPK && profile?.chatSigningPK && profile?.notificationPK);
9422
+ }
9378
9423
  function readProfileChatIdentity(profile) {
9379
9424
  const chat = profile?.identities?.chat;
9380
9425
  return {
@@ -9389,7 +9434,7 @@ function peerUid(peer) {
9389
9434
  return typeof peer === "string" ? cleanText(peer) : cleanText(peer?.uid);
9390
9435
  }
9391
9436
  function isFullProfile(profile) {
9392
- return !!(profile?.uid && (("username" in profile) || ("walletPK" in profile) && ("chatPK" in profile)));
9437
+ return !!(profile?.uid && hasPeerKeys(profile) && ["username", "walletPK", "chatPK", "chatSigningPK", "notificationPK"].every((key) => (key in profile)) && (!profile.chatPK || hasChatIdentity(profile)));
9393
9438
  }
9394
9439
  function normalizeProfile(profile, uid = profile?.uid || null) {
9395
9440
  return {
@@ -9464,6 +9509,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
9464
9509
  let authSession = 0;
9465
9510
  let userUid = null;
9466
9511
  let settingsKey = null;
9512
+ let settingsWriter = null;
9513
+ let openedSettingsBody;
9467
9514
  let agreementStored = null;
9468
9515
  let agreementOverride = null;
9469
9516
  let agreementAcceptance = null;
@@ -9852,6 +9899,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
9852
9899
  settingsBody: privateData.settings ?? null,
9853
9900
  settingsFromCache: false
9854
9901
  }));
9902
+ refreshUnlockedSettings();
9855
9903
  }), current((error) => {
9856
9904
  markError(diag, "user.settings.snapshot", authStartedAt, error);
9857
9905
  if (revokeAuthenticationError(authUser, error, "settings-listener"))
@@ -10067,10 +10115,12 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
10067
10115
  throw new Error("settings key required");
10068
10116
  if (state.settingsBody === undefined)
10069
10117
  throw new Error("settings not available");
10070
- const nextSettings = state.settingsBody === null ? settingsState() : await openSettings(key, uid, state.settingsBody);
10118
+ const body = state.settingsBody;
10119
+ const nextSettings = body === null ? settingsState() : await openSettings(key, uid, body);
10071
10120
  if (!isCurrentUser(uid, session))
10072
10121
  return nextSettings;
10073
10122
  setSettingsKey(key);
10123
+ openedSettingsBody = body;
10074
10124
  setState((user) => ({
10075
10125
  ...user,
10076
10126
  settings: settingsState(nextSettings)
@@ -10091,15 +10141,59 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
10091
10141
  throw new Error("auth");
10092
10142
  if (!settingsKey)
10093
10143
  throw new Error("settings locked");
10094
- const nextSettings = await cloud.user.settings.write(uid, patch, { currentSettings: state.settings, key: settingsKey });
10095
- if (!isCurrentUser(uid, session))
10096
- return nextSettings;
10097
- setState((user) => ({
10098
- ...user,
10099
- settingsReady: true,
10100
- settings: settingsState(nextSettings)
10101
- }));
10102
- return nextSettings;
10144
+ normalizeSettings(patch, state.settings);
10145
+ if (!settingsWriter || settingsWriter.key !== settingsKey) {
10146
+ settingsWriter = { key: settingsKey, uid, session, pending: [], running: false };
10147
+ }
10148
+ const writer = settingsWriter;
10149
+ const result = new Promise((resolve, reject) => writer.pending.push({ patch, resolve, reject }));
10150
+ writeSettings(writer);
10151
+ return result;
10152
+ }
10153
+ async function writeSettings(writer) {
10154
+ if (writer.running)
10155
+ return;
10156
+ writer.running = true;
10157
+ const current = () => settingsKey === writer.key && isCurrentUser(writer.uid, writer.session);
10158
+ while (writer.pending.length) {
10159
+ const batch = writer.pending.splice(0);
10160
+ try {
10161
+ if (!current())
10162
+ throw new Error("settings locked");
10163
+ const desired = batch.reduce((settings2, { patch }) => normalizeSettings(patch, settings2), state.settings);
10164
+ const { settings, body } = await cloud.user.settings.write(writer.uid, desired, { currentSettings: state.settings, key: writer.key });
10165
+ if (!current())
10166
+ throw new Error("settings locked");
10167
+ openedSettingsBody = body;
10168
+ setState((user) => ({ ...user, settingsReady: true, settings: settingsState(settings), settingsBody: body }));
10169
+ for (const request of batch)
10170
+ request.resolve(settings);
10171
+ } catch (error) {
10172
+ for (const request of batch)
10173
+ request.reject(error);
10174
+ }
10175
+ }
10176
+ writer.running = false;
10177
+ if (settingsWriter === writer)
10178
+ settingsWriter = null;
10179
+ refreshUnlockedSettings();
10180
+ }
10181
+ async function refreshUnlockedSettings() {
10182
+ const key = settingsKey;
10183
+ const body = state.settingsBody;
10184
+ if (!key || body === undefined || body === openedSettingsBody || settingsWriter?.key === key)
10185
+ return;
10186
+ const uid = state.uid;
10187
+ try {
10188
+ const settings = body === null ? settingsState() : await openSettings(key, uid, body);
10189
+ if (settingsKey !== key || state.settingsBody !== body || settingsWriter?.key === key)
10190
+ return;
10191
+ openedSettingsBody = body;
10192
+ setState((user) => ({ ...user, settings: settingsState(settings) }));
10193
+ } catch (error) {
10194
+ if (settingsKey === key && state.settingsBody === body)
10195
+ markError(diag, "user.settings.open", Date.now(), error);
10196
+ }
10103
10197
  }
10104
10198
  function isBlocked(peer) {
10105
10199
  const nextPeerUid = peerUid(peer);
@@ -26097,7 +26191,7 @@ function groupEvidence(messages, byKey, memberStatesByEpoch) {
26097
26191
  const target = byKey.get(reference);
26098
26192
  addEvidence(evidence, messageEpochId(target), actor, messageOrderMs(target), seenAt);
26099
26193
  }
26100
- if (canShowMsg(message) && !isSystemMsg(message)) {
26194
+ if (canShowMsg(message)) {
26101
26195
  addEvidence(evidence, messageEpochId(message), actor, seenAt, seenAt);
26102
26196
  }
26103
26197
  }
@@ -26125,11 +26219,12 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
26125
26219
  const shorten = [];
26126
26220
  let nextExpiryAt = null;
26127
26221
  const latestStart = latestCallStartKey(projectedMessages.filter(isServerConfirmedMsg));
26222
+ const endedCalls = new Set(projectedMessages.filter(isServerConfirmedMsg).filter((message) => readCallMessage(message)?.event === "ended").map((message) => message.callId));
26128
26223
  for (const message of projectedMessages) {
26129
26224
  if (!isServerConfirmedMsg(message) || !canShowMsg(message) || message.ttl == null)
26130
26225
  continue;
26131
26226
  const call = readCallMessage(message);
26132
- if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined || options.activeCallId === call.callId))
26227
+ if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined && !endedCalls.has(call.callId) || options.activeCallId === call.callId))
26133
26228
  continue;
26134
26229
  const messageMs = messageOrderMs(message);
26135
26230
  const epochId = messageEpochId(message);
@@ -27700,7 +27795,7 @@ function partitionExpiredMessageRecords(records, expiredKeys, cache, now = Date.
27700
27795
  return { activeRecords, expiredRecords };
27701
27796
  }
27702
27797
  function canHideExpiredMessage(message, chatPK) {
27703
- return isServerConfirmedMsg(message) && isPeerMsg(message, chatPK) && canShowMsg(message) && !isControlMsg(message) && !isSystemMsg(message) && message.ttl != null && message.permanent !== true;
27798
+ return isServerConfirmedMsg(message) && isPeerMsg(message, chatPK) && canShowMsg(message) && message.ttl != null && message.permanent !== true;
27704
27799
  }
27705
27800
  function hiddenMessageKeys(messages, chatPK, peerChatPK, options = {}) {
27706
27801
  if (!chatPK || !peerChatPK || !Array.isArray(messages) || !messages.length)
@@ -44129,27 +44224,23 @@ function createChatSessionLaunches({
44129
44224
  const openDirectChat = async (profile) => {
44130
44225
  const operation = operationContext();
44131
44226
  const { chatPK, blocked } = operation.identity;
44132
- const member = profileMember(profile);
44133
- if (member.chatPK === chatPK)
44227
+ if (!profile?.uid || !profile?.chatPK)
44228
+ throw new Error("peer identity required");
44229
+ if (profile.chatPK === chatPK)
44134
44230
  return openNotesChat(operation);
44135
- if (new Set(blocked).has(member.uid))
44231
+ if (new Set(blocked).has(profile.uid))
44136
44232
  throw new Error("blocked peer cannot be opened");
44137
44233
  assertDirectAdmission(profile, operation);
44138
- const existingChatId = getPeerChatId(member.chatPK, operation);
44234
+ const existingChatId = getPeerChatId(profile.chatPK, operation);
44139
44235
  if (existingChatId) {
44140
44236
  setSelectedChat(existingChatId);
44141
44237
  return existingChatId;
44142
44238
  }
44239
+ const member = { uid: profile.uid, chatPK: profile.chatPK };
44143
44240
  const routeId = directRouteIdForPeer(member.chatPK, operation);
44144
44241
  if (operation.pendingOwner.getPendingLaunch()?.id === routeId)
44145
44242
  return routeId;
44146
- operation.pendingOwner.showEmpty(routeId, [{
44147
- ...member,
44148
- username: profile?.username || null,
44149
- avatar: profile?.avatar || null,
44150
- accountType: profile?.accountType || null,
44151
- chatAdmission: normalizeChatAdmission(profile?.chatAdmission)
44152
- }], [member], "direct");
44243
+ operation.pendingOwner.showEmpty(routeId, [profile], [member], "direct");
44153
44244
  const materialization = directMaterializations.get(routeId);
44154
44245
  if (materialization && isCurrent(materialization.operation)) {
44155
44246
  materialization.promise.then((chatId) => {
@@ -49837,6 +49928,11 @@ function openCallMailbox({ cloud, capability, endpoint, protocol, onChange, onEr
49837
49928
  }
49838
49929
  return Object.freeze({
49839
49930
  getSnapshot: () => candidate || record,
49931
+ async ready() {
49932
+ const response = await channel.ready();
49933
+ await consume(response.record);
49934
+ return record;
49935
+ },
49840
49936
  async read() {
49841
49937
  const response = await channel.request({ type: "read", cursor, headDigest: record?.headDigest ?? null });
49842
49938
  await consume(response.record);
@@ -49943,6 +50039,7 @@ function openAccountCallLease({ cloud, capability: accountCapability, endpoint,
49943
50039
  let expiry2 = null;
49944
50040
  let generation = 0;
49945
50041
  let acquiring = false;
50042
+ let rotating = Promise.resolve();
49946
50043
  const attempts = new Set;
49947
50044
  const waiters = new Set;
49948
50045
  const current = () => !closed;
@@ -50043,6 +50140,7 @@ function openAccountCallLease({ cloud, capability: accountCapability, endpoint,
50043
50140
  async function grant(command) {
50044
50141
  if (closed)
50045
50142
  throw new Error("call lease closed");
50143
+ const requestedAt = clock();
50046
50144
  const attempt = createCallLeaseAttempt(command, clock);
50047
50145
  const started = generation;
50048
50146
  attempts.add(attempt);
@@ -50062,6 +50160,8 @@ function openAccountCallLease({ cloud, capability: accountCapability, endpoint,
50062
50160
  onLost?.();
50063
50161
  }, Math.max(0, deadline.wallDeadline - Date.now()));
50064
50162
  clearTimeout(renewal);
50163
+ const now = clock();
50164
+ const elapsed = Math.max(now.wall - requestedAt.wall, now.monotonic - requestedAt.monotonic);
50065
50165
  renewal = setTimeout(() => {
50066
50166
  if (!owned)
50067
50167
  return;
@@ -50073,14 +50173,35 @@ function openAccountCallLease({ cloud, capability: accountCapability, endpoint,
50073
50173
  onLost?.();
50074
50174
  onError?.(error);
50075
50175
  });
50076
- }, 5000);
50176
+ }, Math.max(0, 5000 - elapsed));
50077
50177
  return value;
50078
50178
  } finally {
50079
50179
  attempts.delete(attempt);
50080
50180
  attempt.cancel();
50081
50181
  }
50082
50182
  }
50183
+ async function rotateMedia() {
50184
+ if (closed || acquiring || !owned || !renewalRemaining(generation))
50185
+ throw new Error("call ownership unavailable");
50186
+ const previous = stop();
50187
+ const started = generation;
50188
+ await request2({ type: "release", holder: endpoint.holder, revision: previous.revision });
50189
+ if (closed || started !== generation)
50190
+ throw new Error("call transfer cancelled");
50191
+ return grant({
50192
+ type: "claim",
50193
+ holder: endpoint.holder,
50194
+ expectedRevision: previous.revision,
50195
+ payload: previous.payload,
50196
+ takeover: false
50197
+ });
50198
+ }
50083
50199
  return Object.freeze({
50200
+ rotateMedia() {
50201
+ const next = rotating.then(rotateMedia);
50202
+ rotating = next.catch(() => {});
50203
+ return next;
50204
+ },
50084
50205
  async acquire(descriptor) {
50085
50206
  if (closed)
50086
50207
  throw new Error("call lease closed");
@@ -50089,7 +50210,7 @@ function openAccountCallLease({ cloud, capability: accountCapability, endpoint,
50089
50210
  acquiring = true;
50090
50211
  const started = generation;
50091
50212
  try {
50092
- await request2({ type: "read" });
50213
+ await accept((await channel.ready()).record);
50093
50214
  const payload = await capability.seal(descriptor);
50094
50215
  if (closed || started !== generation)
50095
50216
  throw new Error("call transfer cancelled");
@@ -50534,7 +50655,7 @@ function openCallRoom({
50534
50655
  pulseTimer = setTimeout(pulse, 1e4);
50535
50656
  }
50536
50657
  async function beginStart(create) {
50537
- const record = await mailbox.read();
50658
+ const record = await mailbox.ready();
50538
50659
  if (closed || departing)
50539
50660
  throw cancelled2();
50540
50661
  if (!create && !record?.head)
@@ -50693,7 +50814,7 @@ function createCallActivity({ holder, onSpeaking, now = Date.now, setTimer = set
50693
50814
  return;
50694
50815
  const admitted = new Map(peers.map((peer) => [peer.holder, peer]));
50695
50816
  local = admitted.has(holder) && !muted && !deafened;
50696
- direct = mode === "direct" && directPeer !== holder && admitted.has(directPeer) && !admitted.get(directPeer).muted ? directPeer : null;
50817
+ direct = mode === "direct" && directPeer !== holder && admitted.has(directPeer) ? directPeer : null;
50697
50818
  const nextSources = new Map(local ? [[holder, "local"]] : []);
50698
50819
  const nextRoutes2 = new Map;
50699
50820
  const ambiguous = new Set;
@@ -50701,7 +50822,7 @@ function createCallActivity({ holder, onSpeaking, now = Date.now, setTimer = set
50701
50822
  nextSources.set(direct, "direct");
50702
50823
  if (mode === "group") {
50703
50824
  for (const [id, mid] of received) {
50704
- if (id === holder || !admitted.has(id) || admitted.get(id).muted || typeof mid !== "string" || !mid)
50825
+ if (id === holder || !admitted.has(id) || typeof mid !== "string" || !mid)
50705
50826
  continue;
50706
50827
  if (nextRoutes2.has(mid)) {
50707
50828
  nextSources.delete(nextRoutes2.get(mid));
@@ -50767,13 +50888,14 @@ var init_activity = __esm(() => {
50767
50888
  });
50768
50889
 
50769
50890
  // ../../core/calls/media.js
50770
- function createCallMediaSession({ port, mode, holder, provider, signal, publish, onState, onError, onExpired, onSpeaking }) {
50891
+ function createCallMediaSession({ port, mode, holder, preparation, provider, signal, publish, onState, onError, onExpired, onSpeaking, diag }) {
50771
50892
  let media = null;
50772
50893
  let closed = false;
50773
50894
  let deadline = null;
50774
50895
  let keys = null;
50775
50896
  let peers = [];
50776
50897
  let sessionId = null;
50898
+ let published = false;
50777
50899
  let started = false;
50778
50900
  let directPeer = null;
50779
50901
  let offered = false;
@@ -50896,7 +51018,11 @@ function createCallMediaSession({ port, mode, holder, provider, signal, publish,
50896
51018
  for (const id of pendingPeerAudio.keys())
50897
51019
  settlePeerAudio(id, new Error("call media closed"));
50898
51020
  pendingSignals.length = incomingSignals.length = 0;
50899
- const operations = [disposeConnection(previous), retiringConnection];
51021
+ const operations = [
51022
+ disposeConnection(previous),
51023
+ retiringConnection,
51024
+ openingConnection?.then((opened) => opened !== previous ? disposeConnection(opened) : undefined, () => {})
51025
+ ];
50900
51026
  closing = Promise.allSettled(operations).then((results) => {
50901
51027
  const failed = results.find((result) => result.status === "rejected");
50902
51028
  if (failed)
@@ -50909,22 +51035,24 @@ function createCallMediaSession({ port, mode, holder, provider, signal, publish,
50909
51035
  const live = () => !closed && generation === connectionGeneration;
50910
51036
  await retiringConnection;
50911
51037
  current(generation);
50912
- if (!iceServers) {
50913
- const configuration = await provider("turn", {});
50914
- current(generation);
50915
- iceServers = configuration.iceServers;
50916
- }
51038
+ const startedAt = Date.now();
51039
+ const prepared = preparation;
51040
+ preparation = null;
50917
51041
  const opening = port.open({
50918
51042
  id: holder,
50919
51043
  mode,
50920
- iceServers,
51044
+ preparation: prepared,
50921
51045
  onState: (value) => {
50922
51046
  if (!live())
50923
51047
  return;
50924
51048
  connectionState = value.state;
50925
- if (connectionState === "connected")
51049
+ if (connectionState === "connected") {
50926
51050
  settleConnection();
50927
- else if (["failed", "closed"].includes(connectionState))
51051
+ serialize(async () => {
51052
+ if (live())
51053
+ await media?.startSource?.();
51054
+ });
51055
+ } else if (["failed", "closed"].includes(connectionState))
50928
51056
  settleConnection(new Error("call connection failed"));
50929
51057
  onState?.(value);
50930
51058
  },
@@ -50961,34 +51089,51 @@ function createCallMediaSession({ port, mode, holder, provider, signal, publish,
50961
51089
  }
50962
51090
  });
50963
51091
  openingConnection = opening;
50964
- let opened;
51092
+ const preparing = opening.then(async (opened) => {
51093
+ current(generation);
51094
+ media = opened;
51095
+ await opened.mute(muted);
51096
+ current(generation);
51097
+ await opened.deafen(deafened);
51098
+ current(generation);
51099
+ if (microphoneVolume !== 1) {
51100
+ await opened.setMicrophoneVolume(microphoneVolume);
51101
+ current(generation);
51102
+ }
51103
+ await opened.prepare();
51104
+ current(generation);
51105
+ markDone(diag, "call.media.setup", startedAt, { stage: "local" });
51106
+ return opened;
51107
+ });
50965
51108
  try {
50966
- opened = await opening;
50967
- } finally {
50968
- if (openingConnection === opening)
50969
- openingConnection = null;
50970
- }
50971
- if (!live()) {
50972
- await disposeConnection(opened);
51109
+ const [opened, configuration] = await Promise.all([
51110
+ preparing,
51111
+ Promise.resolve().then(() => iceServers ? { iceServers, sessionId } : provider(mode === "group" ? "session/new" : "turn", {})).then((value) => {
51112
+ current(generation);
51113
+ markDone(diag, "call.media.setup", startedAt, { stage: "provider" });
51114
+ return value;
51115
+ })
51116
+ ]);
50973
51117
  current(generation);
50974
- }
50975
- media = opened;
50976
- await opened.mute(muted);
50977
- current(generation);
50978
- await opened.deafen(deafened);
50979
- current(generation);
50980
- if (microphoneVolume !== 1) {
50981
- await opened.setMicrophoneVolume(microphoneVolume);
51118
+ iceServers = configuration.iceServers;
51119
+ if (mode === "group")
51120
+ sessionId = configuration.sessionId;
51121
+ await opened.configure(iceServers);
50982
51122
  current(generation);
50983
- }
50984
- await opened.prepare();
50985
- current(generation);
50986
- if (deadline) {
50987
- await opened.grant(deadline);
51123
+ if (deadline) {
51124
+ await opened.grant(deadline);
51125
+ current(generation);
51126
+ }
51127
+ await updateKeys();
50988
51128
  current(generation);
51129
+ markDone(diag, "call.media.setup", startedAt, { stage: "ready" });
51130
+ } catch (error) {
51131
+ await disposeConnection(await opening.catch(() => null));
51132
+ throw error;
51133
+ } finally {
51134
+ if (openingConnection === opening)
51135
+ openingConnection = null;
50989
51136
  }
50990
- await updateKeys();
50991
- current(generation);
50992
51137
  }
50993
51138
  async function applyDescription(result, required = false) {
50994
51139
  current();
@@ -51051,10 +51196,7 @@ function createCallMediaSession({ port, mode, holder, provider, signal, publish,
51051
51196
  if (!started || !keys)
51052
51197
  return;
51053
51198
  if (mode === "group") {
51054
- if (!sessionId) {
51055
- const session = await provider("session/new", {});
51056
- current();
51057
- sessionId = session.sessionId;
51199
+ if (!published) {
51058
51200
  const offer = await media.offer();
51059
51201
  current();
51060
51202
  const sendMid = media.sendMid;
@@ -51073,48 +51215,15 @@ function createCallMediaSession({ port, mode, holder, provider, signal, publish,
51073
51215
  current();
51074
51216
  await connected();
51075
51217
  current();
51076
- await publish({ sessionId, trackName });
51077
- current();
51078
- }
51079
- const additions = peers.filter((peer) => peer.holder !== holder && peer.media?.sessionId && peer.media?.trackName && !received.has(peer.holder));
51080
- if (additions.length) {
51081
- await connected();
51082
- current();
51083
- const result = await provider("tracks/new", { sessionId, tracks: additions.map((peer) => ({
51084
- location: "remote",
51085
- sessionId: peer.media.sessionId,
51086
- trackName: peer.media.trackName
51087
- })) });
51088
- current();
51089
- if (!Array.isArray(result.tracks) || result.tracks.length !== additions.length)
51090
- throw new Error("peer audio subscription failed");
51091
- const mappings = [];
51092
- const occupied = new Set([media.sendMid, ...received.values()]);
51093
- for (const peer of additions) {
51094
- const track = result.tracks?.find((item) => item.sessionId === peer.media.sessionId && item.trackName === peer.media.trackName);
51095
- if (!validMid(track?.mid) || track.errorCode || occupied.has(track.mid))
51096
- throw new Error("peer audio subscription failed");
51097
- occupied.add(track.mid);
51098
- mappings.push([peer.holder, track.mid]);
51099
- }
51100
- for (const [peer, mid] of mappings)
51101
- received.set(peer, mid);
51102
- updateActivity();
51103
- await applyPeerAudio();
51104
- current();
51105
- await updateKeys();
51106
- current();
51107
- await applyDescription(result);
51108
- }
51109
- const removals = [...received].filter(([peer]) => !peers.some((item) => item.holder === peer));
51110
- if (removals.length) {
51111
- for (const [peer] of removals)
51112
- received.delete(peer);
51113
- updateActivity();
51114
- await updateKeys();
51115
- current();
51116
- const result = await provider("tracks/close", { sessionId, tracks: removals.map(([, mid]) => ({ mid })) });
51117
- await applyDescription(result);
51218
+ await Promise.all([
51219
+ publish({ sessionId, trackName }).then(() => {
51220
+ current();
51221
+ published = true;
51222
+ }),
51223
+ receiveGroupTracks()
51224
+ ]);
51225
+ } else {
51226
+ await receiveGroupTracks();
51118
51227
  }
51119
51228
  } else {
51120
51229
  const peer = peers.find((item) => item.holder !== holder);
@@ -51142,6 +51251,48 @@ function createCallMediaSession({ port, mode, holder, provider, signal, publish,
51142
51251
  }
51143
51252
  }
51144
51253
  }
51254
+ async function receiveGroupTracks() {
51255
+ const additions = peers.filter((peer) => peer.holder !== holder && peer.media?.sessionId && peer.media?.trackName && !received.has(peer.holder));
51256
+ if (additions.length) {
51257
+ await connected();
51258
+ current();
51259
+ const result = await provider("tracks/new", { sessionId, tracks: additions.map((peer) => ({
51260
+ location: "remote",
51261
+ sessionId: peer.media.sessionId,
51262
+ trackName: peer.media.trackName
51263
+ })) });
51264
+ current();
51265
+ if (!Array.isArray(result.tracks) || result.tracks.length !== additions.length)
51266
+ throw new Error("peer audio subscription failed");
51267
+ const mappings = [];
51268
+ const occupied = new Set([media.sendMid, ...received.values()]);
51269
+ for (const peer of additions) {
51270
+ const track = result.tracks?.find((item) => item.sessionId === peer.media.sessionId && item.trackName === peer.media.trackName);
51271
+ if (!validMid(track?.mid) || track.errorCode || occupied.has(track.mid))
51272
+ throw new Error("peer audio subscription failed");
51273
+ occupied.add(track.mid);
51274
+ mappings.push([peer.holder, track.mid]);
51275
+ }
51276
+ for (const [peer, mid] of mappings)
51277
+ received.set(peer, mid);
51278
+ updateActivity();
51279
+ await applyPeerAudio();
51280
+ current();
51281
+ await updateKeys();
51282
+ current();
51283
+ await applyDescription(result);
51284
+ }
51285
+ const removals = [...received].filter(([peer]) => !peers.some((item) => item.holder === peer));
51286
+ if (removals.length) {
51287
+ for (const [peer] of removals)
51288
+ received.delete(peer);
51289
+ updateActivity();
51290
+ await updateKeys();
51291
+ current();
51292
+ const result = await provider("tracks/close", { sessionId, tracks: removals.map(([, mid]) => ({ mid })) });
51293
+ await applyDescription(result);
51294
+ }
51295
+ }
51145
51296
  return Object.freeze({
51146
51297
  async open() {
51147
51298
  current();
@@ -51197,13 +51348,19 @@ function createCallMediaSession({ port, mode, holder, provider, signal, publish,
51197
51348
  },
51198
51349
  mute(value) {
51199
51350
  current();
51200
- muted = value === true;
51351
+ const next = value === true;
51352
+ if (muted === next)
51353
+ return;
51354
+ muted = next;
51201
51355
  updateActivity();
51202
51356
  return media?.mute(muted);
51203
51357
  },
51204
51358
  deafen(value) {
51205
51359
  current();
51206
- deafened = value === true;
51360
+ const next = value === true;
51361
+ if (deafened === next)
51362
+ return;
51363
+ deafened = next;
51207
51364
  updateActivity();
51208
51365
  return media?.deafen(deafened);
51209
51366
  },
@@ -51394,7 +51551,7 @@ var init_observation = __esm(() => {
51394
51551
  });
51395
51552
 
51396
51553
  // ../../core/calls/session.js
51397
- function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51554
+ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio, saveAudio, diag }) {
51398
51555
  const listeners = new Set;
51399
51556
  const discoveries = new Map;
51400
51557
  const discoveryReads = new Map;
@@ -51402,11 +51559,16 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51402
51559
  const retiring = new Set;
51403
51560
  let state = initial();
51404
51561
  const confirmedPeerAudio = new Map;
51562
+ const pendingPeerAudio = new Map;
51563
+ let peerAudioSource = null;
51564
+ let audioSource = normalizeCallAudio();
51565
+ let pendingAudio = null;
51405
51566
  let identity = null;
51406
51567
  let endpoint = null;
51407
51568
  let leaseEndpoint = null;
51408
51569
  let lease = null;
51409
51570
  let active = null;
51571
+ let participation = null;
51410
51572
  let focused = null;
51411
51573
  let generation = 0;
51412
51574
  let joinIntent = 0;
@@ -51481,6 +51643,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51481
51643
  emit({ available: next });
51482
51644
  }
51483
51645
  function closeDiscovery(entry) {
51646
+ if ([...retiring].some((call2) => call2.discovery === entry))
51647
+ return;
51484
51648
  entry.mailbox.close();
51485
51649
  entry.observation?.close();
51486
51650
  entry.observation = null;
@@ -51606,8 +51770,14 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51606
51770
  throw Object.assign(new Error("chat unavailable"), { code: "unavailable" });
51607
51771
  if (!epochState)
51608
51772
  throw Object.assign(new Error("chat unavailable"), { code: "calls/chat-not-ready" });
51773
+ if (chat.getSnapshot().getOwnerChat(chatId)?.epochState?.manifest.epochId !== epochId)
51774
+ throw cancelled2();
51775
+ const existing = discoveries.get(chatId);
51609
51776
  const entry = discovery(chatId, epochState);
51610
- await entry.mailbox.read();
51777
+ if (entry === existing)
51778
+ await entry.mailbox.read();
51779
+ else
51780
+ await entry.mailbox.ready();
51611
51781
  current(token);
51612
51782
  return entry;
51613
51783
  }).finally(() => {
@@ -51641,49 +51811,68 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51641
51811
  prune();
51642
51812
  };
51643
51813
  }
51644
- async function joinAttempt(chatId, intent, expectedCallId, onlyExisting) {
51814
+ async function joinAttempt(chatId, intent, expectedCallId, onlyExisting, continuation = null) {
51645
51815
  if (!identity || !mediaPort || !mls || !cloud.calls)
51646
51816
  throw new Error("calling unavailable");
51647
- if (!foreground)
51648
- throw new Error("open veyl to join a call");
51649
51817
  if (active?.chatId === chatId)
51650
51818
  return chatId;
51651
51819
  const token = generation;
51652
51820
  const startedAt = Date.now();
51653
51821
  const step = (stage) => diag?.("call.join.stage", { stage, elapsedMs: Date.now() - startedAt });
51822
+ let preflightActive = true;
51654
51823
  const check = () => {
51655
51824
  current(token);
51656
- if (intent !== joinIntent || !foreground)
51825
+ if (!preflightActive || intent !== joinIntent)
51657
51826
  throw cancelled2();
51658
51827
  };
51659
- emit({ ...!active ? { phase: "joining", chatId } : {}, error: null });
51660
- await mediaPort.prepare?.();
51661
- check();
51662
- step("permission");
51663
- if (!onlyExisting)
51664
- chatId = await chat.getSnapshot().materializeChat(chatId);
51665
- check();
51666
- joining.chatId = chatId;
51667
- step("chat");
51828
+ const operation = joining;
51829
+ let preparation;
51668
51830
  let call2;
51669
51831
  try {
51670
- const epochState = await chat.getSnapshot().prepareCall(chatId);
51671
- check();
51672
- const entry = await readDiscovery(chatId, epochState);
51832
+ emit({ ...!active && !continuation ? { phase: "joining", chatId } : {}, error: null });
51833
+ const permission = (async () => {
51834
+ const value = continuation ? undefined : await mediaPort.prepare?.();
51835
+ try {
51836
+ check();
51837
+ } catch (error) {
51838
+ value?.close();
51839
+ throw error;
51840
+ }
51841
+ preparation = value;
51842
+ operation.preparation = value;
51843
+ step("permission");
51844
+ })();
51845
+ const destination = (async () => {
51846
+ if (!onlyExisting && !continuation)
51847
+ chatId = await chat.getSnapshot().materializeChat(chatId);
51848
+ check();
51849
+ joining.chatId = chatId;
51850
+ step("chat");
51851
+ const epochState = continuation ? chat.getSnapshot().getOwnerChat(chatId)?.epochState : await chat.getSnapshot().prepareCall(chatId);
51852
+ check();
51853
+ if (!epochState)
51854
+ throw cancelled2();
51855
+ const entry2 = await readDiscovery(chatId, epochState);
51856
+ check();
51857
+ step("discovery");
51858
+ return entry2;
51859
+ })();
51860
+ const [, entry] = await Promise.all([permission, destination]);
51673
51861
  check();
51674
51862
  const discoveryRecord = entry.mailbox.getSnapshot();
51675
- step("discovery");
51676
51863
  const create = !entry.descriptor;
51677
51864
  if (expectedCallId && (onlyExisting || entry.epochState.manifest.lineage === "direct") && entry.descriptor?.callId !== expectedCallId)
51678
51865
  throw ended();
51679
51866
  const descriptor = entry.descriptor || {
51680
- callId: toHex(randomBytes3(32)),
51867
+ callId: continuation?.callId || toHex(randomBytes3(32)),
51681
51868
  secret: toHex(randomBytes3(32)),
51682
51869
  mode: entry.epochState.manifest.lineage,
51683
51870
  participants: 1,
51684
- startedAt: Date.now()
51871
+ startedAt: continuation?.startedAt ?? Date.now()
51685
51872
  };
51686
- const callEndpoint = createCallEndpoint();
51873
+ if (continuation && descriptor.callId !== continuation.callId)
51874
+ throw ended();
51875
+ const callEndpoint = continuation?.endpoint || createCallEndpoint();
51687
51876
  call2 = {
51688
51877
  chatId,
51689
51878
  callId: descriptor.callId,
@@ -51704,6 +51893,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51704
51893
  muteTail: Promise.resolve()
51705
51894
  };
51706
51895
  joining.candidate = call2;
51896
+ call2.owner = continuation?.owner || createLease(identity, callEndpoint);
51707
51897
  const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint: callEndpoint, callId: descriptor.callId });
51708
51898
  call2.protocol = protocol;
51709
51899
  const ownMember = (value) => {
@@ -51723,6 +51913,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51723
51913
  if (!create)
51724
51914
  cleanBytes(callMember.snapshot);
51725
51915
  check();
51916
+ step("identity");
51726
51917
  const admission = protocol.sign("join", { keyPackage: member.keyPackage ? encodeCallBytes(member.keyPackage) : "", signaturePK: toHex(member.signaturePK) });
51727
51918
  const secret = fromHexBytes(descriptor.secret);
51728
51919
  const capability = createCallCapability(secret, cloud.environment, "signaling", [descriptor.callId]);
@@ -51754,8 +51945,10 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51754
51945
  };
51755
51946
  call2.media = createCallMediaSession({
51756
51947
  port: mediaPort,
51948
+ preparation,
51757
51949
  mode: descriptor.mode,
51758
51950
  holder: callEndpoint.holder,
51951
+ diag,
51759
51952
  provider: (...args) => call2.owner.provider(...args),
51760
51953
  signal: (...args) => call2.room.signal(...args),
51761
51954
  publish: (value) => call2.room.publishMedia(value),
@@ -51770,6 +51963,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51770
51963
  participants: call2.participants.length
51771
51964
  });
51772
51965
  if (value.state === "connected") {
51966
+ if (state.phase !== "connected")
51967
+ step("connected");
51773
51968
  clearTimeout(call2.joinTimer);
51774
51969
  call2.joinTimer = null;
51775
51970
  emit({ phase: "connected", transport: value.transport || (descriptor.mode === "group" ? "relay" : null) });
@@ -51866,7 +52061,8 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51866
52061
  throw error;
51867
52062
  }
51868
52063
  checkCall();
51869
- announce(chatId, descriptor.callId, "started");
52064
+ if (!continuation)
52065
+ announce(chatId, descriptor.callId, "started");
51870
52066
  } else {
51871
52067
  await entry.mailbox.read();
51872
52068
  checkCall();
@@ -51876,16 +52072,21 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51876
52072
  throw Object.assign(new Error("the call room changed"), { code: "calls/discovery-conflict" });
51877
52073
  }
51878
52074
  }
52075
+ step("publication");
51879
52076
  call2.published = true;
51880
- await stopCall();
51881
- checkCall();
51882
- mountLease(identity, callEndpoint);
51883
- call2.owner = lease;
52077
+ if (!continuation) {
52078
+ await stopCall();
52079
+ checkCall();
52080
+ mountLease(identity, callEndpoint, call2.owner);
52081
+ participation = { chatId, callId: descriptor.callId, startedAt: descriptor.startedAt, endpoint: callEndpoint, owner: lease };
52082
+ }
52083
+ step("predecessor");
52084
+ call2.owner = participation.owner;
51884
52085
  active = call2;
51885
52086
  observeRoster(entry);
51886
52087
  available();
51887
52088
  emit({
51888
- phase: state.elsewhere ? "transferring" : "joining",
52089
+ phase: continuation ? "connecting" : state.elsewhere ? "transferring" : "joining",
51889
52090
  chatId,
51890
52091
  callId: descriptor.callId,
51891
52092
  mode: descriptor.mode,
@@ -51898,7 +52099,10 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51898
52099
  if (preference)
51899
52100
  call2.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
51900
52101
  }
51901
- await lease.acquire({ chatId, callId: descriptor.callId });
52102
+ if (continuation)
52103
+ await participation.owner.rotateMedia();
52104
+ else
52105
+ await lease.acquire({ chatId, callId: descriptor.callId });
51902
52106
  check();
51903
52107
  step("ownership");
51904
52108
  if (!valid())
@@ -51958,6 +52162,11 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51958
52162
  throw Object.assign(new Error("the call room is restarting"), { code: "calls/discovery-conflict" });
51959
52163
  }
51960
52164
  throw error;
52165
+ } finally {
52166
+ preflightActive = false;
52167
+ preparation?.close();
52168
+ if (operation.preparation === preparation)
52169
+ operation.preparation = null;
51961
52170
  }
51962
52171
  }
51963
52172
  async function join(chatId, expectedCallId = null, onlyExisting = false) {
@@ -51966,21 +52175,31 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51966
52175
  throw ended();
51967
52176
  return chatId;
51968
52177
  }
52178
+ if (!foreground)
52179
+ throw new Error("open veyl to join a call");
52180
+ return runJoin(chatId, expectedCallId, onlyExisting);
52181
+ }
52182
+ async function runJoin(chatId, expectedCallId = null, onlyExisting = false, continuation = null) {
51969
52183
  const intent = ++joinIntent;
51970
- const operation = { predecessor: active, chatId, candidate: null };
52184
+ const operation = { predecessor: active, chatId, candidate: null, continuation };
51971
52185
  const previous = joining;
51972
52186
  joining = operation;
52187
+ previous?.preparation?.close();
51973
52188
  emit({ joiningChatId: chatId });
51974
52189
  if (previous?.candidate && previous.candidate !== active)
51975
52190
  disposeCall(previous.candidate);
51976
52191
  try {
51977
52192
  for (let attempt = 0;attempt < 3; attempt += 1) {
51978
52193
  try {
51979
- return await joinAttempt(chatId, intent, expectedCallId, onlyExisting);
52194
+ return await joinAttempt(chatId, intent, expectedCallId, onlyExisting, continuation);
51980
52195
  } catch (error) {
51981
52196
  if (intent !== joinIntent || !identity || closed)
51982
52197
  throw cancelled2();
51983
52198
  if (error?.code !== "calls/discovery-conflict" || attempt === 2) {
52199
+ if (continuation) {
52200
+ fail(error);
52201
+ throw error;
52202
+ }
51984
52203
  const target = operation.chatId;
51985
52204
  emit({ ...!active ? { phase: "error", chatId: target } : {}, error: callError(chatId, error) });
51986
52205
  throw error;
@@ -51997,6 +52216,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
51997
52216
  }
51998
52217
  function leave() {
51999
52218
  joinIntent += 1;
52219
+ joining?.preparation?.close();
52000
52220
  const candidate = joining?.candidate;
52001
52221
  joining = null;
52002
52222
  emit({ joiningChatId: null });
@@ -52011,6 +52231,7 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52011
52231
  if (!operation)
52012
52232
  return Promise.resolve();
52013
52233
  joinIntent += 1;
52234
+ operation.preparation?.close();
52014
52235
  joining = null;
52015
52236
  emit({ joiningChatId: null });
52016
52237
  const pending = operation.candidate && operation.candidate !== active ? disposeCall(operation.candidate) : null;
@@ -52020,6 +52241,14 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52020
52241
  }
52021
52242
  function stopCall() {
52022
52243
  const call2 = active;
52244
+ const authority = participation;
52245
+ participation = null;
52246
+ if (!call2 && authority) {
52247
+ authority.owner.retire();
52248
+ const retired = [...retiring].filter((room) => room.endpoint === authority.endpoint);
52249
+ const released = Promise.all(retired.map((room) => room.stopped)).then(() => authority.owner.release());
52250
+ leaving = retired.length ? Promise.race([released, Promise.all(retired.map((room) => room.closing))]) : released;
52251
+ }
52023
52252
  if (!call2) {
52024
52253
  emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
52025
52254
  return leaving;
@@ -52039,7 +52268,9 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52039
52268
  return;
52040
52269
  try {
52041
52270
  await entry.mailbox.commit(null, [], { expectedRevision: snapshot.revision });
52042
- announce(call2.chatId, call2.callId, "ended");
52271
+ if (chat.getSnapshot().getOwnerChat(call2.chatId)?.epochState?.manifest.epochId === entry.epochState.manifest.epochId) {
52272
+ announce(call2.chatId, call2.callId, "ended");
52273
+ }
52043
52274
  } catch (error) {
52044
52275
  if (error?.code !== "calls/conflict")
52045
52276
  throw error;
@@ -52059,11 +52290,13 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52059
52290
  clearTimeout(call2.timer);
52060
52291
  clearTimeout(call2.joinTimer);
52061
52292
  const stopping = call2.media?.close();
52062
- call2.owner?.retire();
52293
+ call2.stopped = Promise.resolve(stopping);
52294
+ const releasedOwner = participation?.endpoint === call2.endpoint ? null : call2.owner;
52295
+ releasedOwner?.retire();
52063
52296
  const departing = call2.room?.leave();
52064
52297
  const draining = (async () => {
52065
52298
  try {
52066
- const [, departure] = await Promise.allSettled([Promise.resolve(stopping).then(() => call2.owner?.release()), departing]);
52299
+ const [, departure] = await Promise.allSettled([Promise.resolve(stopping).then(() => releasedOwner?.release()), departing]);
52067
52300
  if (departure.status === "fulfilled" && departure.value?.ended && call2.published && call2.discovery.descriptor?.callId === call2.callId) {
52068
52301
  await retireDiscovery(call2);
52069
52302
  }
@@ -52083,9 +52316,13 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52083
52316
  call2.room?.close();
52084
52317
  call2.protocol?.close();
52085
52318
  cleanBytes(call2.member?.snapshot);
52319
+ if (call2.owner !== lease && call2.owner !== participation?.owner)
52320
+ call2.owner?.close();
52086
52321
  if (call2.endpoint !== leaseEndpoint)
52087
52322
  call2.endpoint.close();
52088
52323
  retiring.delete(call2);
52324
+ if (discoveries.get(call2.chatId) !== call2.discovery)
52325
+ closeDiscovery(call2.discovery);
52089
52326
  prune();
52090
52327
  });
52091
52328
  return call2.closing;
@@ -52101,6 +52338,27 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52101
52338
  return result;
52102
52339
  }
52103
52340
  async function setAudio(patch) {
52341
+ const value = normalizeCallAudio(patch, state);
52342
+ if (value.muted === state.muted && value.deafened === state.deafened && value.muted === audioSource.muted && value.deafened === audioSource.deafened)
52343
+ return;
52344
+ const token = generation;
52345
+ pendingAudio = value;
52346
+ const saving = Promise.resolve().then(() => {
52347
+ if (generation !== token)
52348
+ throw cancelled2();
52349
+ return saveAudio(value);
52350
+ }).then(() => {
52351
+ if (generation === token)
52352
+ audioSource = value;
52353
+ });
52354
+ try {
52355
+ await Promise.all([saving, applyAudioPreferences(value)]);
52356
+ } finally {
52357
+ if (generation === token && pendingAudio === value)
52358
+ pendingAudio = null;
52359
+ }
52360
+ }
52361
+ async function applyAudioPreferences(patch) {
52104
52362
  emit(patch);
52105
52363
  const calls = new Set([active, joining?.candidate].filter((call2) => call2?.media && call2.room && !call2.leaving));
52106
52364
  await Promise.all([...calls].map((call2) => applyAudio(call2).catch((error) => {
@@ -52111,6 +52369,13 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52111
52369
  throw error;
52112
52370
  })));
52113
52371
  }
52372
+ function syncAudio(source) {
52373
+ audioSource = normalizeCallAudio(source);
52374
+ const value = pendingAudio || audioSource;
52375
+ if (state.muted === value.muted && state.deafened === value.deafened)
52376
+ return;
52377
+ applyAudioPreferences(value).catch((error) => diag?.("call.audio.settings.error", { code: error?.code || "calls/audio" }));
52378
+ }
52114
52379
  async function setPeerAudio(chatPK, patch) {
52115
52380
  const call2 = active;
52116
52381
  if (!identity || typeof chatPK !== "string" || !chatPK || chatPK === identity.chatPK)
@@ -52119,11 +52384,15 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52119
52384
  const peer = call2?.participants.find((peer2) => peer2.chatPK === chatPK);
52120
52385
  const previous = state.peerAudio[chatPK];
52121
52386
  const value = { volume: 100, muted: false, ...previous, ...patch };
52387
+ pendingPeerAudio.set(chatPK, value);
52122
52388
  emit({ peerAudio: { ...state.peerAudio, [chatPK]: value } });
52123
52389
  try {
52124
52390
  if (peer)
52125
52391
  await call2.media.setPeerAudio(peer.holder, { volume: value.volume / 100, muted: value.muted });
52126
- if (generation === token && state.peerAudio[chatPK] === value)
52392
+ if (generation !== token || pendingPeerAudio.get(chatPK) !== value)
52393
+ return;
52394
+ await savePeerAudio(chatPK, value);
52395
+ if (generation === token)
52127
52396
  confirmedPeerAudio.set(chatPK, value);
52128
52397
  } catch (error) {
52129
52398
  if (generation === token && state.peerAudio[chatPK] === value) {
@@ -52134,8 +52403,39 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52134
52403
  else
52135
52404
  delete peerAudio[chatPK];
52136
52405
  emit({ peerAudio });
52406
+ if (peer && active === call2)
52407
+ call2.media.setPeerAudio(peer.holder, {
52408
+ volume: (confirmed?.volume ?? 100) / 100,
52409
+ muted: confirmed?.muted ?? false
52410
+ }).catch(() => {});
52137
52411
  }
52138
52412
  throw error;
52413
+ } finally {
52414
+ if (generation === token && pendingPeerAudio.get(chatPK) === value)
52415
+ pendingPeerAudio.delete(chatPK);
52416
+ }
52417
+ }
52418
+ function syncPeerAudio(source = {}) {
52419
+ if (source === peerAudioSource)
52420
+ return;
52421
+ peerAudioSource = source;
52422
+ const peerAudio = { ...source };
52423
+ for (const [chatPK, value] of pendingPeerAudio)
52424
+ peerAudio[chatPK] = value;
52425
+ confirmedPeerAudio.clear();
52426
+ for (const [chatPK, value] of Object.entries(source))
52427
+ confirmedPeerAudio.set(chatPK, value);
52428
+ 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));
52429
+ if (!changed.size)
52430
+ return;
52431
+ emit({ peerAudio });
52432
+ for (const call2 of new Set([active, joining?.candidate].filter((call3) => call3?.media && !call3.leaving))) {
52433
+ for (const peer of call2.participants) {
52434
+ if (!changed.has(peer.chatPK))
52435
+ continue;
52436
+ const audio = peerAudio[peer.chatPK];
52437
+ call2.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" }));
52438
+ }
52139
52439
  }
52140
52440
  }
52141
52441
  async function setMicrophoneVolume(volume) {
@@ -52153,13 +52453,9 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52153
52453
  throw error;
52154
52454
  }
52155
52455
  }
52156
- function mountLease(session, callEndpoint = endpoint) {
52157
- lease?.close();
52158
- if (leaseEndpoint && leaseEndpoint !== endpoint && leaseEndpoint !== callEndpoint)
52159
- leaseEndpoint.close();
52160
- leaseEndpoint = callEndpoint;
52456
+ function createLease(session, callEndpoint) {
52161
52457
  const ownsLease = () => identity === session && leaseEndpoint === callEndpoint;
52162
- lease = openAccountCallLease({
52458
+ return openAccountCallLease({
52163
52459
  cloud,
52164
52460
  capability: session.callIdentity,
52165
52461
  endpoint: callEndpoint,
@@ -52184,9 +52480,22 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52184
52480
  }
52185
52481
  });
52186
52482
  }
52187
- function setSources(session) {
52188
- if (identity === session)
52483
+ function mountLease(session, callEndpoint = endpoint, prepared = null) {
52484
+ lease?.close();
52485
+ if (leaseEndpoint && leaseEndpoint !== endpoint && leaseEndpoint !== callEndpoint)
52486
+ leaseEndpoint.close();
52487
+ leaseEndpoint = callEndpoint;
52488
+ lease = prepared || createLease(session, callEndpoint);
52489
+ }
52490
+ function setSources(session, settings = {}) {
52491
+ const peerAudio = settings.peerAudio || {};
52492
+ if (identity === session) {
52493
+ if (session) {
52494
+ syncPeerAudio(peerAudio);
52495
+ syncAudio(settings.callAudio);
52496
+ }
52189
52497
  return drainingSources;
52498
+ }
52190
52499
  const token = ++generation;
52191
52500
  const previousLease = lease;
52192
52501
  const previousEndpoint = endpoint;
@@ -52218,7 +52527,13 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52218
52527
  if (generation !== token || identity !== session)
52219
52528
  return retirement;
52220
52529
  confirmedPeerAudio.clear();
52221
- emit(initial());
52530
+ pendingPeerAudio.clear();
52531
+ pendingAudio = null;
52532
+ audioSource = normalizeCallAudio(settings.callAudio);
52533
+ peerAudioSource = session ? peerAudio : null;
52534
+ for (const [chatPK, value] of Object.entries(peerAudioSource || {}))
52535
+ confirmedPeerAudio.set(chatPK, value);
52536
+ emit({ ...initial(), ...session ? audioSource : {}, peerAudio: peerAudioSource || {} });
52222
52537
  available();
52223
52538
  if (generation !== token || identity !== session)
52224
52539
  return retirement;
@@ -52239,10 +52554,35 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52239
52554
  if (!identity)
52240
52555
  return;
52241
52556
  for (const [chatId, entry] of discoveries) {
52242
- const epoch = chat.getSnapshot().getOwnerChat(chatId)?.epochState?.manifest.epochId;
52243
- if (epoch === entry.epochState.manifest.epochId)
52557
+ const epochState = chat.getSnapshot().getOwnerChat(chatId)?.epochState;
52558
+ if (epochState?.manifest.epochId === entry.epochState.manifest.epochId)
52244
52559
  continue;
52245
- if (active?.chatId === chatId)
52560
+ const retained = epochState?.manifest.members.some((member) => member.chatSigningPK === identity.chatSigningPK);
52561
+ if (participation?.chatId === chatId && retained) {
52562
+ const continuation = participation;
52563
+ const intent = ++joinIntent;
52564
+ const previous = active || joining?.candidate;
52565
+ active = null;
52566
+ joining = null;
52567
+ if (previous)
52568
+ disposeCall(previous);
52569
+ const members = new Set(epochState.manifest.members.map((member) => member.chatPK));
52570
+ emit({
52571
+ phase: "connecting",
52572
+ joiningChatId: chatId,
52573
+ speaking: [],
52574
+ participants: state.participants.filter((member) => members.has(member.chatPK))
52575
+ });
52576
+ diag?.("call.epoch.transition", { stage: "starting", participants: state.participants.length });
52577
+ Promise.resolve(previous?.stopped).then(() => {
52578
+ if (intent !== joinIntent || participation !== continuation)
52579
+ return;
52580
+ return runJoin(chatId, null, false, continuation);
52581
+ }).catch((error) => {
52582
+ if (intent === joinIntent && participation === continuation)
52583
+ fail(error);
52584
+ });
52585
+ } else if (active?.chatId === chatId || participation?.chatId === chatId)
52246
52586
  leave();
52247
52587
  else if (joining?.chatId === chatId)
52248
52588
  cancelJoin();
@@ -52301,8 +52641,6 @@ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
52301
52641
  },
52302
52642
  setForeground(value) {
52303
52643
  foreground = value === true;
52304
- if (!foreground && joining)
52305
- cancelJoin();
52306
52644
  },
52307
52645
  setOnline(value) {
52308
52646
  const reconnecting = !online && value === true;
@@ -52340,6 +52678,7 @@ var init_session5 = __esm(() => {
52340
52678
  init_wire();
52341
52679
  init_call();
52342
52680
  init_ownership();
52681
+ init_settings();
52343
52682
  });
52344
52683
 
52345
52684
  // ../../node_modules/.bun/@zxcvbn-ts+core@4.2.0/node_modules/@zxcvbn-ts/core/dist/utils/helper.cjs
@@ -55759,15 +56098,17 @@ var init_paymentrequest = __esm(() => {
55759
56098
  });
55760
56099
 
55761
56100
  // ../../core/wallet/bitcoin.js
55762
- function normalizeBitcoinData(data, current = DEFAULT_BITCOIN, info = {}) {
56101
+ function normalizeBitcoinData(data, current = DEFAULT_BITCOIN) {
55763
56102
  if (!data)
55764
56103
  return current;
56104
+ const observedAt = timestampMs(data.priceUpdatedAt, null, { positive: true });
56105
+ const usePrice = hasBitcoinPrice(data.price) && observedAt != null && observedAt >= (current.priceUpdatedAt ?? 0);
55765
56106
  return {
55766
- price: hasBitcoinPrice(data.price) ? data.price : current.price,
55767
- priceUpdatedAt: hasBitcoinPrice(data.price) ? timestampMs(data.updatedAt, Date.now()) : current.priceUpdatedAt,
55768
- priceFromCache: hasBitcoinPrice(data.price) ? info.fromCache === true : current.priceFromCache,
56107
+ price: usePrice ? data.price : current.price,
56108
+ priceUpdatedAt: usePrice ? observedAt : current.priceUpdatedAt,
56109
+ priceFromCache: usePrice ? false : current.priceFromCache,
55769
56110
  block: data.block ?? current?.block ?? null,
55770
- fees: data.fees ?? current?.fees ?? null,
56111
+ fees: data.fees ?? null,
55771
56112
  updatedAt: data.updatedAt ?? current?.updatedAt ?? null,
55772
56113
  ready: true,
55773
56114
  error: null
@@ -55817,20 +56158,15 @@ function createBitcoin({ cloud, priceStorage = null, diag = null }) {
55817
56158
  const priceUpdatedAt = timestampMs(cached.updatedAt, null, { positive: true });
55818
56159
  if (!priceUpdatedAt)
55819
56160
  return;
55820
- if (bitcoin.priceUpdatedAt != null && (!bitcoin.priceFromCache || bitcoin.priceUpdatedAt >= priceUpdatedAt))
56161
+ if (bitcoin.priceUpdatedAt != null && bitcoin.priceUpdatedAt >= priceUpdatedAt)
55821
56162
  return;
55822
56163
  publish({ ...bitcoin, price: cached.price, priceUpdatedAt, priceFromCache: true });
55823
56164
  }).catch((error) => diag?.("bitcoin.price.cache.read.error", { code: error?.code || "" }));
55824
- unsubscribe = cloud.bitcoin.watch((data, info = {}) => {
55825
- if (session !== generation || info.pending)
56165
+ unsubscribe = cloud.bitcoin.watch((data) => {
56166
+ if (session !== generation)
55826
56167
  return;
55827
- const next = normalizeBitcoinData(data, bitcoin, info);
55828
- if (info.fromCache && bitcoin.priceUpdatedAt != null && next.priceUpdatedAt <= bitcoin.priceUpdatedAt) {
55829
- next.price = bitcoin.price;
55830
- next.priceUpdatedAt = bitcoin.priceUpdatedAt;
55831
- next.priceFromCache = bitcoin.priceFromCache;
55832
- }
55833
- if (priceStorage && hasBitcoinPrice(data?.price) && !info.fromCache && (next.price !== bitcoin.price || next.priceUpdatedAt !== bitcoin.priceUpdatedAt || bitcoin.priceFromCache)) {
56168
+ const next = normalizeBitcoinData(data, bitcoin);
56169
+ if (priceStorage && next.priceUpdatedAt != null && !next.priceFromCache && (next.price !== bitcoin.price || next.priceUpdatedAt !== bitcoin.priceUpdatedAt || bitcoin.priceFromCache)) {
55834
56170
  const cached = { price: next.price, updatedAt: next.priceUpdatedAt };
55835
56171
  priceWrite = priceWrite.catch(NOOP3).then(() => priceStorage.write(cached));
55836
56172
  priceWrite.catch((error) => diag?.("bitcoin.price.cache.write.error", { code: error?.code || "" }));
@@ -56991,8 +57327,9 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
56991
57327
  async function fetchProfileByUid(uid) {
56992
57328
  if (!uid)
56993
57329
  return null;
56994
- if (profileCache.has(uid))
56995
- return profileCache.get(uid);
57330
+ const cached = profileCache.get(uid);
57331
+ if (isFullProfile(cached))
57332
+ return cached;
56996
57333
  try {
56997
57334
  const record = await cloud.peer.read(uid);
56998
57335
  if (!record)
@@ -57040,11 +57377,11 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
57040
57377
  return null;
57041
57378
  if (field === "walletPK" && walletToUid.has(value)) {
57042
57379
  const uid = walletToUid.get(value);
57043
- return profileCache.get(uid) ?? null;
57380
+ return fetchProfileByUid(uid);
57044
57381
  }
57045
57382
  if (field === "chatPK" && chatToUid.has(value)) {
57046
57383
  const uid = chatToUid.get(value);
57047
- return profileCache.get(uid) ?? null;
57384
+ return fetchProfileByUid(uid);
57048
57385
  }
57049
57386
  try {
57050
57387
  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);
@@ -64591,7 +64928,14 @@ function openAccount(options = {}) {
64591
64928
  maintenance: messageMaintenance,
64592
64929
  resolveActiveCall: (chatId) => calls.resolveActiveCall(chatId)
64593
64930
  }, chatSources());
64594
- const calls = createCallSession({ cloud, chat, ...options.calls, diag });
64931
+ const calls = createCallSession({
64932
+ cloud,
64933
+ chat,
64934
+ ...options.calls,
64935
+ diag,
64936
+ savePeerAudio: (chatPK, audio) => user.getSnapshot().updateSettings({ peerAudio: { [chatPK]: audio } }),
64937
+ saveAudio: (callAudio) => user.getSnapshot().updateSettings({ callAudio })
64938
+ });
64595
64939
  const wallet = openWallet({
64596
64940
  ...options.wallet || {},
64597
64941
  network: state.network,
@@ -64665,7 +65009,7 @@ function openAccount(options = {}) {
64665
65009
  syncPresence();
64666
65010
  syncPeers();
64667
65011
  calls.setOnline(state.online);
64668
- calls.setSources(state.lockState === "unlocked" ? state.session : null);
65012
+ calls.setSources(state.lockState === "unlocked" ? state.session : null, state.user?.settings);
64669
65013
  }
64670
65014
  function syncPresence() {
64671
65015
  const session = state.session;
@@ -75870,7 +76214,7 @@ var package_default;
75870
76214
  var init_package2 = __esm(() => {
75871
76215
  package_default = {
75872
76216
  name: "veyl",
75873
- version: "0.73.0",
76217
+ version: "0.74.0",
75874
76218
  packageManager: "bun@1.4.0",
75875
76219
  private: true,
75876
76220
  license: "UNLICENSED",
@@ -75937,6 +76281,7 @@ var init_package2 = __esm(() => {
75937
76281
  "@noble/hashes": "catalog:",
75938
76282
  "@scure/bip39": "catalog:",
75939
76283
  "@veyl/core": "workspace:*",
76284
+ "@veyl/bitcoin": "workspace:*",
75940
76285
  "@veyl/firebase": "workspace:*",
75941
76286
  "@veyl/live": "workspace:*",
75942
76287
  "@veyl/product": "workspace:*",
@@ -223421,7 +223766,7 @@ function createFirebaseCloud({
223421
223766
  const nextSettings = normalizeSettings(settings, base2);
223422
223767
  const body = await sealSettings(key, uid, nextSettings);
223423
223768
  await callVaultFunction("setSettings", VAULT_SIGNATURE_PURPOSES.SET_SETTINGS, { settings: bytesBase64(body) });
223424
- return nextSettings;
223769
+ return { settings: nextSettings, body };
223425
223770
  }
223426
223771
  async function addUserPush(payload = {}) {
223427
223772
  await callFunction("setPush", payload);
@@ -223453,15 +223798,6 @@ function createFirebaseCloud({
223453
223798
  });
223454
223799
  }, onError);
223455
223800
  }
223456
- function watchBitcoin(onUpdate, onError) {
223457
- return onSnapshot2(doc3(db, "bitcoin", "current"), { includeMetadataChanges: true }, (snap) => {
223458
- onUpdate?.(snap.exists() ? snap.data() : null, {
223459
- exists: snap.exists(),
223460
- fromCache: snap.metadata.fromCache,
223461
- pending: snap.metadata.hasPendingWrites
223462
- });
223463
- }, onError);
223464
- }
223465
223801
  async function readPeer(uid) {
223466
223802
  requireUid(uid);
223467
223803
  const snap = await getDocFromServer2(doc3(db, "profiles", uid));
@@ -225215,9 +225551,6 @@ function createFirebaseCloud({
225215
225551
  watch: watchAdmin
225216
225552
  }
225217
225553
  },
225218
- bitcoin: {
225219
- watch: watchBitcoin
225220
- },
225221
225554
  peer: {
225222
225555
  read: readPeer,
225223
225556
  avatar: {
@@ -225362,10 +225695,11 @@ var init_cloud = __esm(() => {
225362
225695
  });
225363
225696
 
225364
225697
  // ../../core/cloud.js
225365
- function createCloud({ data, live, presence, calls, connection } = {}) {
225698
+ function createCloud({ data, bitcoin, live, presence, calls, connection } = {}) {
225366
225699
  if (!data?.environment)
225367
225700
  throw new Error("cloud data adapter required");
225368
225701
  const channels = new Set;
225702
+ const watches = new Set;
225369
225703
  let disposed = false;
225370
225704
  const controlled = connection?.enabled === true;
225371
225705
  const transportPort = (transport, { watchable = false, requestable = false } = {}) => transport && Object.freeze({
@@ -225400,6 +225734,9 @@ function createCloud({ data, live, presence, calls, connection } = {}) {
225400
225734
  return channel?.send(...args) ?? false;
225401
225735
  },
225402
225736
  ...requestable ? {
225737
+ ready() {
225738
+ return channel.ready();
225739
+ },
225403
225740
  request(...args) {
225404
225741
  return channel.request(...args);
225405
225742
  },
@@ -225439,6 +225776,19 @@ function createCloud({ data, live, presence, calls, connection } = {}) {
225439
225776
  });
225440
225777
  return Object.freeze({
225441
225778
  ...data,
225779
+ bitcoin: bitcoin && Object.freeze({
225780
+ watch(next, error) {
225781
+ if (disposed)
225782
+ throw new Error("cloud closed");
225783
+ const unsubscribe = bitcoin.watch(next, error);
225784
+ const stop = () => {
225785
+ if (watches.delete(stop))
225786
+ unsubscribe();
225787
+ };
225788
+ watches.add(stop);
225789
+ return stop;
225790
+ }
225791
+ }),
225442
225792
  availability: Object.freeze({
225443
225793
  getSnapshot: () => controlled && connection.getSnapshot() !== "off" ? false : null,
225444
225794
  subscribe: (listener) => controlled ? connection.subscribe(listener) : () => {}
@@ -225452,11 +225802,112 @@ function createCloud({ data, live, presence, calls, connection } = {}) {
225452
225802
  disposed = true;
225453
225803
  for (const channel of [...channels])
225454
225804
  channel.close();
225805
+ for (const stop of [...watches])
225806
+ stop();
225455
225807
  calls?.close();
225456
225808
  }
225457
225809
  });
225458
225810
  }
225459
225811
 
225812
+ // ../../adapters/bitcoin/transport.js
225813
+ function createBitcoinTransport({ endpoint = bitcoinEndpoint, fetcher = globalThis.fetch, appState = null, connection = null } = {}) {
225814
+ const listeners = new Set;
225815
+ let snapshot2 = null;
225816
+ let fetchedAt = 0;
225817
+ let timer = null;
225818
+ let pending = null;
225819
+ let activitySubscription = null;
225820
+ let stopConnection = null;
225821
+ let initialized2 = connection?.enabled !== true;
225822
+ const active = () => listeners.size > 0 && initialized2 && (!appState || appState.currentState === "active") && (!connection?.enabled || connection.getSnapshot() === "off");
225823
+ function cancel() {
225824
+ clearTimeout(timer);
225825
+ timer = null;
225826
+ const previous = pending;
225827
+ pending = null;
225828
+ previous?.abort();
225829
+ }
225830
+ async function refresh() {
225831
+ if (!active() || pending)
225832
+ return;
225833
+ const controller = new AbortController;
225834
+ pending = controller;
225835
+ const timeout = setTimeout(() => controller.abort(), BITCOIN_FETCH_TIMEOUT_MS);
225836
+ try {
225837
+ const response = await fetcher(endpoint, { signal: controller.signal, credentials: "omit" });
225838
+ if (!response.ok)
225839
+ throw new Error(`bitcoin data unavailable (${response.status})`);
225840
+ const value = await response.json();
225841
+ if (!value || !Number.isFinite(value.updatedAt) || value.updatedAt <= 0)
225842
+ throw new Error("invalid bitcoin snapshot");
225843
+ if (pending !== controller)
225844
+ return;
225845
+ fetchedAt = Date.now();
225846
+ if (!snapshot2 || value.updatedAt > snapshot2.updatedAt)
225847
+ snapshot2 = value;
225848
+ for (const entry of listeners)
225849
+ entry.next(snapshot2);
225850
+ } catch (error) {
225851
+ if (pending === controller)
225852
+ for (const entry of listeners)
225853
+ entry.error?.(error);
225854
+ } finally {
225855
+ clearTimeout(timeout);
225856
+ if (pending === controller) {
225857
+ pending = null;
225858
+ if (active())
225859
+ timer = setTimeout(refresh, BITCOIN_POLL_MS);
225860
+ }
225861
+ }
225862
+ }
225863
+ function sync() {
225864
+ if (!active()) {
225865
+ cancel();
225866
+ return;
225867
+ }
225868
+ if (pending)
225869
+ return;
225870
+ clearTimeout(timer);
225871
+ const delay = Math.max(0, BITCOIN_POLL_MS - (Date.now() - fetchedAt));
225872
+ if (delay)
225873
+ timer = setTimeout(refresh, delay);
225874
+ else
225875
+ refresh();
225876
+ }
225877
+ return Object.freeze({
225878
+ watch(next, error) {
225879
+ const entry = { next, error };
225880
+ listeners.add(entry);
225881
+ if (snapshot2)
225882
+ next(snapshot2);
225883
+ if (listeners.size === 1) {
225884
+ activitySubscription = appState?.addEventListener("change", sync);
225885
+ stopConnection = connection?.enabled ? connection.subscribe(sync) : null;
225886
+ if (!initialized2)
225887
+ connection.ready.then(() => {
225888
+ initialized2 = true;
225889
+ sync();
225890
+ });
225891
+ sync();
225892
+ }
225893
+ return () => {
225894
+ listeners.delete(entry);
225895
+ if (listeners.size)
225896
+ return;
225897
+ cancel();
225898
+ activitySubscription?.remove();
225899
+ stopConnection?.();
225900
+ activitySubscription = null;
225901
+ stopConnection = null;
225902
+ };
225903
+ }
225904
+ });
225905
+ }
225906
+ var init_transport = __esm(() => {
225907
+ init_config();
225908
+ init_origins();
225909
+ });
225910
+
225460
225911
  // ../../adapters/live/retry.js
225461
225912
  function createSocketRetry({ plane, realm: realm2, staleAfter, diag }) {
225462
225913
  let closed = false;
@@ -225757,7 +226208,7 @@ function createLiveTransport({ endpoint: endpointValue, realm: realm2, WebSocket
225757
226208
  });
225758
226209
  }
225759
226210
  var ROOM_RE;
225760
- var init_transport = __esm(() => {
226211
+ var init_transport2 = __esm(() => {
225761
226212
  init_config();
225762
226213
  init_wire2();
225763
226214
  init_retry();
@@ -226074,6 +226525,8 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226074
226525
  lifetime.signal.addEventListener("abort", abort, { once: true });
226075
226526
  activity?.addEventListener("abort", abort, { once: true });
226076
226527
  const timer = setTimeout(abort, REQUEST_MS);
226528
+ const startedAt = Date.now();
226529
+ let status = 0;
226077
226530
  try {
226078
226531
  const response = await fetcher(`${http}${path}`, {
226079
226532
  method: "POST",
@@ -226082,6 +226535,7 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226082
226535
  body: JSON.stringify(body),
226083
226536
  signal: controller.signal
226084
226537
  });
226538
+ status = response.status;
226085
226539
  const data = await readResponse(response);
226086
226540
  if (closed)
226087
226541
  throw callWireError("closed");
@@ -226097,6 +226551,12 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226097
226551
  throw error;
226098
226552
  throw callWireError(controller.signal.aborted ? "timeout" : "connection");
226099
226553
  } finally {
226554
+ markDone(diag, "call.transport.http", startedAt, {
226555
+ kind: body.request?.kind || "ticket",
226556
+ type: body.request?.body?.type || "ticket",
226557
+ stage: body.request?.body?.action || "",
226558
+ status
226559
+ });
226100
226560
  clearTimeout(timer);
226101
226561
  lifetime.signal.removeEventListener("abort", abort);
226102
226562
  activity?.removeEventListener("abort", abort);
@@ -226142,6 +226602,7 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226142
226602
  let socket = null;
226143
226603
  let generation = 0;
226144
226604
  let ready2 = false;
226605
+ let snapshot2 = null;
226145
226606
  let deadline = null;
226146
226607
  let heartbeat = null;
226147
226608
  let lastEvidenceAt = 0;
@@ -226176,6 +226637,7 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226176
226637
  const previous = socket;
226177
226638
  socket = null;
226178
226639
  ready2 = false;
226640
+ snapshot2 = null;
226179
226641
  generation += 1;
226180
226642
  clearTimeout(deadline);
226181
226643
  clearInterval(heartbeat);
@@ -226205,11 +226667,13 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226205
226667
  }
226206
226668
  const version7 = ++generation;
226207
226669
  retry.opening();
226670
+ const startedAt = Date.now();
226208
226671
  const current = () => !stopped2 && !closed && !suspended && version7 === generation;
226209
226672
  try {
226210
226673
  const frame = await wrap3({ type: "observe", ...getCheckpoint?.() || { cursor: 0 } });
226211
226674
  if (!current())
226212
226675
  return;
226676
+ markDone(diag, "call.transport.open", startedAt, { kind: capability.kind, stage: "authentication" });
226213
226677
  const auth2 = encodeCallBytes(new TextEncoder().encode(JSON.stringify(frame))).replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "");
226214
226678
  if (auth2.length > 8000)
226215
226679
  throw callWireError("too-large");
@@ -226222,6 +226686,11 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226222
226686
  return;
226223
226687
  }
226224
226688
  const liveSocket = socket;
226689
+ const socketStartedAt = Date.now();
226690
+ liveSocket.onopen = () => {
226691
+ if (current())
226692
+ markDone(diag, "call.transport.open", socketStartedAt, { kind: capability.kind, stage: "upgrade" });
226693
+ };
226225
226694
  deadline = setTimeout(() => {
226226
226695
  if (current())
226227
226696
  disconnect("timeout");
@@ -226239,7 +226708,9 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226239
226708
  throw callWireError("invalid-frame");
226240
226709
  const frame = JSON.parse(event2.data);
226241
226710
  if (frame.type === "snapshot") {
226711
+ snapshot2 = { record: frame.record, now: frame.now };
226242
226712
  if (!ready2) {
226713
+ markDone(diag, "call.transport.open", socketStartedAt, { kind: capability.kind, stage: "snapshot" });
226243
226714
  ready2 = true;
226244
226715
  clearTimeout(deadline);
226245
226716
  retry.ready();
@@ -226310,37 +226781,79 @@ function createCallTransport({ endpoint: value, realm: realm2, getToken, fetcher
226310
226781
  });
226311
226782
  }
226312
226783
  const channel = Object.freeze({
226784
+ async ready() {
226785
+ if (!stream2)
226786
+ throw callWireError("invalid-request");
226787
+ const startedAt = Date.now();
226788
+ let code = "";
226789
+ try {
226790
+ await waitReady();
226791
+ if (!ready2 || !snapshot2 || stopped2 || closed || suspended)
226792
+ throw callWireError("connection");
226793
+ return snapshot2;
226794
+ } catch (error) {
226795
+ code = error?.code || "calls/unavailable";
226796
+ throw error;
226797
+ } finally {
226798
+ markDone(diag, "call.transport.ready", startedAt, { kind: capability.kind, code });
226799
+ }
226800
+ },
226313
226801
  async request(body) {
226314
226802
  if (!stream2 && body?.type !== "read")
226315
226803
  throw callWireError("invalid-request");
226316
- await waitReady();
226317
- const requestActivity = activity;
226318
- const frame = await wrap3(body);
226319
- if (!stream2) {
226320
- if (stopped2 || closed)
226321
- throw callWireError("closed");
226322
- if (suspended || requestActivity !== activity)
226804
+ const startedAt = Date.now();
226805
+ let readyAt = null;
226806
+ let authenticatedAt = null;
226807
+ let stage = "ready";
226808
+ let code = "";
226809
+ try {
226810
+ await waitReady();
226811
+ readyAt = Date.now();
226812
+ stage = "authentication";
226813
+ const requestActivity = activity;
226814
+ const frame = await wrap3(body);
226815
+ authenticatedAt = Date.now();
226816
+ stage = "response";
226817
+ if (!stream2) {
226818
+ if (stopped2 || closed)
226819
+ throw callWireError("closed");
226820
+ if (suspended || requestActivity !== activity)
226821
+ throw callWireError("connection");
226822
+ return await post(path, frame, {}, requestActivity.signal);
226823
+ }
226824
+ if (!ready2 || stopped2 || closed || suspended || socket?.readyState !== 1)
226323
226825
  throw callWireError("connection");
226324
- return post(path, frame, {}, requestActivity.signal);
226826
+ const text2 = JSON.stringify(frame);
226827
+ if (text2.length > CALL_REQUEST_MAX_BYTES)
226828
+ throw callWireError("too-large");
226829
+ return await new Promise((resolve, reject) => {
226830
+ const nonce = frame.request.nonce;
226831
+ const timer = setTimeout(() => {
226832
+ pending.delete(nonce);
226833
+ reject(callWireError("timeout"));
226834
+ }, REQUEST_MS);
226835
+ pending.set(nonce, { resolve, reject, timer });
226836
+ try {
226837
+ socket.send(text2);
226838
+ } catch {
226839
+ disconnect("send");
226840
+ }
226841
+ });
226842
+ } catch (error) {
226843
+ code = error?.code || "calls/unavailable";
226844
+ throw error;
226845
+ } finally {
226846
+ const now = Date.now();
226847
+ markDone(diag, "call.transport.request", startedAt, {
226848
+ kind: capability.kind,
226849
+ type: body?.type,
226850
+ stage,
226851
+ code,
226852
+ readyMs: (readyAt ?? now) - startedAt,
226853
+ authenticationMs: readyAt === null ? 0 : (authenticatedAt ?? now) - readyAt,
226854
+ responseMs: authenticatedAt === null ? 0 : now - authenticatedAt
226855
+ });
226325
226856
  }
226326
- if (!ready2 || stopped2 || closed || suspended || socket?.readyState !== 1)
226327
- throw callWireError("connection");
226328
- const text2 = JSON.stringify(frame);
226329
- if (text2.length > CALL_REQUEST_MAX_BYTES)
226330
- throw callWireError("too-large");
226331
- return new Promise((resolve, reject) => {
226332
- const nonce = frame.request.nonce;
226333
- const timer = setTimeout(() => {
226334
- pending.delete(nonce);
226335
- reject(callWireError("timeout"));
226336
- }, REQUEST_MS);
226337
- pending.set(nonce, { resolve, reject, timer });
226338
- try {
226339
- socket.send(text2);
226340
- } catch {
226341
- disconnect("send");
226342
- }
226343
- });
226344
226857
  },
226345
226858
  async provider(body) {
226346
226859
  if (!stream2)
@@ -226453,6 +226966,7 @@ function createCloudRuntime(options2 = {}) {
226453
226966
  const db = environment.databaseId === "(default)" ? getFirestore(app2) : getFirestore(app2, environment.databaseId);
226454
226967
  const functions2 = getFunctions(app2, region);
226455
226968
  const cloud = createCloud({
226969
+ bitcoin,
226456
226970
  data: createFirebaseCloud({ db, auth: auth2, environment: environment.name, functions: functions2, onOperation: options2.onOperation }),
226457
226971
  live: options2.live === undefined ? createLiveTransport({ endpoint: liveEndpoints[environment.name], realm: environment.name, diag: options2.diag, WebSocketClass: observeWebSocket(globalThis.WebSocket, options2.onOperation, "chat") }) : options2.live,
226458
226972
  presence: options2.presence === undefined ? createPresenceTransport({ endpoint: liveEndpoints[environment.name], realm: environment.name, diag: options2.diag, WebSocketClass: observeWebSocket(globalThis.WebSocket, options2.onOperation, "presence") }) : options2.presence,
@@ -226487,19 +227001,21 @@ async function signOutRuntime(auth2) {
226487
227001
  await signOut(auth2);
226488
227002
  }
226489
227003
  }
226490
- var DEFAULT_FUNCTION_REGION = "us-central1";
227004
+ var DEFAULT_FUNCTION_REGION = "us-central1", bitcoin;
226491
227005
  var init_cloud2 = __esm(() => {
226492
227006
  init_dist4();
226493
227007
  init_dist5();
226494
227008
  init_dist6();
226495
227009
  init_dist7();
226496
227010
  init_cloud();
226497
- init_origins();
226498
227011
  init_transport();
227012
+ init_origins();
227013
+ init_transport2();
226499
227014
  init_presence();
226500
227015
  init_calls();
226501
227016
  init_observe2();
226502
227017
  init_config3();
227018
+ bitcoin = createBitcoinTransport();
226503
227019
  });
226504
227020
 
226505
227021
  // src/runtime/kdf-worker.js