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