@glyphteck/veyl 0.70.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/account.js CHANGED
@@ -1127,6 +1127,16 @@ function numberToBytesBE(n, len) {
1127
1127
  function numberToBytesLE(n, len) {
1128
1128
  return numberToBytesBE(n, len).reverse();
1129
1129
  }
1130
+ function equalBytes(a, b) {
1131
+ a = abytes2(a);
1132
+ b = abytes2(b);
1133
+ if (a.length !== b.length)
1134
+ return false;
1135
+ let diff = 0;
1136
+ for (let i = 0;i < a.length; i++)
1137
+ diff |= a[i] ^ b[i];
1138
+ return diff === 0;
1139
+ }
1130
1140
  function copyBytes(bytes) {
1131
1141
  return Uint8Array.from(abytes2(bytes));
1132
1142
  }
@@ -4813,7 +4823,7 @@ function checkOpts2(defaults, opts) {
4813
4823
  const merged = Object.assign(defaults, opts);
4814
4824
  return merged;
4815
4825
  }
4816
- function equalBytes(a, b) {
4826
+ function equalBytes2(a, b) {
4817
4827
  a = abytes3(a);
4818
4828
  b = abytes3(b);
4819
4829
  if (a.length !== b.length)
@@ -5251,7 +5261,7 @@ var gcm = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 12, tagLength
5251
5261
  const passedTag = ciphertext.subarray(-tagLength);
5252
5262
  const tag = _computeTag(authKey, tagMask, data);
5253
5263
  toClean.push(tag);
5254
- if (!equalBytes(tag, passedTag)) {
5264
+ if (!equalBytes2(tag, passedTag)) {
5255
5265
  clean2(...toClean);
5256
5266
  throw new Error("aes-gcm: invalid tag");
5257
5267
  }
@@ -5704,6 +5714,7 @@ var CHAT_TTL_CLIENT_DELETE_GRACE_MS = MINUTE_MS;
5704
5714
  var CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500;
5705
5715
  var CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250;
5706
5716
  var CHAT_LIST_PAGE_SIZE = 20;
5717
+ var CHAT_ADMISSION_CACHE_PEERS = 1024;
5707
5718
  var CHAT_LIST_LIVE_COUNT = CHAT_LIST_PAGE_SIZE;
5708
5719
  var CHAT_INBOX_PING_PAGE_SIZE = 25;
5709
5720
  var CHAT_INBOX_DISCOVERY_PARALLEL_CHATS = CHAT_INBOX_PING_PAGE_SIZE;
@@ -5877,9 +5888,12 @@ function normalizeChatNotificationMode(value, manifest) {
5877
5888
  const mode = cleanText(value);
5878
5889
  if (!NOTIFICATION_MODES.has(mode))
5879
5890
  throw new Error("chat notification mode required");
5880
- if (manifest?.lineage !== "group")
5891
+ if (manifest?.lineage === "self")
5881
5892
  return CHAT_NOTIFICATION_MODES.ALL;
5882
- if (mode === CHAT_NOTIFICATION_MODES.ALL && manifest.members?.length > CHAT_ORDINARY_DELIVERY_MAX_MEMBERS) {
5893
+ if (manifest?.lineage === "direct" && mode === CHAT_NOTIFICATION_MODES.MENTIONS) {
5894
+ throw new Error("mentions notifications require a group chat");
5895
+ }
5896
+ if (mode === CHAT_NOTIFICATION_MODES.ALL && manifest?.lineage === "group" && manifest.members?.length > CHAT_ORDINARY_DELIVERY_MAX_MEMBERS) {
5883
5897
  return CHAT_NOTIFICATION_MODES.MENTIONS;
5884
5898
  }
5885
5899
  return mode;
@@ -6940,7 +6954,7 @@ var _poly1305_aead = (xorStream) => (key, nonce, AAD) => {
6940
6954
  const data = ciphertext.subarray(0, -tagLength);
6941
6955
  const passedTag = ciphertext.subarray(-tagLength);
6942
6956
  const tag = computeTag2(xorStream, key, nonce, data, AAD);
6943
- if (!equalBytes(passedTag, tag)) {
6957
+ if (!equalBytes2(passedTag, tag)) {
6944
6958
  clean2(tag);
6945
6959
  throw new Error("invalid tag");
6946
6960
  }
@@ -8262,35 +8276,68 @@ function chatAdmissionCapabilityCommitment(value) {
8262
8276
  cleanBytes(capability);
8263
8277
  }
8264
8278
  }
8265
- function directAdmissionCapabilityForPeer(chatPrivateKey, chatPK, profile) {
8266
- return deriveDirectAdmissionCapability(chatPrivateKey, chatPK, profile?.chatPK, profile?.chatPK);
8267
- }
8268
- function hasDirectAdmissionGrant(chatPrivateKey, chatPK, profile) {
8269
- const policy = normalizeChatAdmission(profile?.chatAdmission);
8270
- if (!policy.directGrants.length)
8271
- return false;
8272
- const capability = directAdmissionCapabilityForPeer(chatPrivateKey, chatPK, profile);
8273
- try {
8274
- return policy.directGrants.includes(chatAdmissionCapabilityCommitment(capability));
8275
- } finally {
8276
- cleanBytes(capability);
8277
- }
8279
+ function deriveDirectAdmissionCommitment(chatPrivateKey, chatPK, peerChatPK, recipientChatPK) {
8280
+ return chatAdmissionCapabilityCommitment(deriveDirectAdmissionCapability(chatPrivateKey, chatPK, peerChatPK, recipientChatPK));
8278
8281
  }
8279
- function directAdmissionForPeer(chatPrivateKey, chatPK, profile, ownAdmission = null) {
8280
- if (ownAdmission != null && normalizeChatAdmission(ownAdmission).direct === CHAT_DIRECT_ADMISSION_MODES.CLOSED && incomingChatAdmissionDecision(chatPrivateKey, chatPK, profile, ownAdmission, "direct") !== CHAT_ADMISSION_DECISIONS.ACCEPT)
8282
+ function directAdmissionDecision(chatPK, profile, ownAdmission, commitmentFor) {
8283
+ const ownPolicy = normalizeChatAdmission(ownAdmission);
8284
+ if (ownAdmission != null && ownPolicy.direct === CHAT_DIRECT_ADMISSION_MODES.CLOSED && !ownPolicy.directGrants.includes(commitmentFor(profile?.chatPK, chatPK)))
8281
8285
  return CHAT_ADMISSION_DECISIONS.REJECT;
8282
- if (hasDirectAdmissionGrant(chatPrivateKey, chatPK, profile)) {
8286
+ const policy = normalizeChatAdmission(profile?.chatAdmission);
8287
+ if (policy.directGrants.length && policy.directGrants.includes(commitmentFor(profile?.chatPK, profile?.chatPK))) {
8283
8288
  return CHAT_ADMISSION_DECISIONS.ACCEPT;
8284
8289
  }
8285
- const mode = normalizeChatAdmission(profile?.chatAdmission).direct;
8290
+ const mode = policy.direct;
8286
8291
  if (mode === CHAT_DIRECT_ADMISSION_MODES.OPEN)
8287
8292
  return CHAT_ADMISSION_DECISIONS.ACCEPT;
8288
8293
  if (mode === CHAT_DIRECT_ADMISSION_MODES.REQUESTS)
8289
8294
  return CHAT_ADMISSION_DECISIONS.REQUEST;
8290
8295
  return CHAT_ADMISSION_DECISIONS.REJECT;
8291
8296
  }
8292
- function canStartDirectChat(chatPrivateKey, chatPK, profile, ownAdmission = null) {
8293
- return directAdmissionForPeer(chatPrivateKey, chatPK, profile, ownAdmission) !== CHAT_ADMISSION_DECISIONS.REJECT;
8297
+ function directAdmissionForPeer(chatPrivateKey, chatPK, profile, ownAdmission = null) {
8298
+ return directAdmissionDecision(chatPK, profile, ownAdmission, (peerChatPK, recipientChatPK) => deriveDirectAdmissionCommitment(chatPrivateKey, chatPK, peerChatPK, recipientChatPK));
8299
+ }
8300
+ function createChatAdmission({ getIdentity, deriveCommitment = deriveDirectAdmissionCommitment }) {
8301
+ let chatPK = null;
8302
+ let privateKey = null;
8303
+ let privateKeyBytes = null;
8304
+ const peers = new Map;
8305
+ const reset = () => {
8306
+ peers.clear();
8307
+ cleanBytes(privateKeyBytes);
8308
+ chatPK = null;
8309
+ privateKey = null;
8310
+ privateKeyBytes = null;
8311
+ };
8312
+ const directAdmissionForPeer2 = (profile) => {
8313
+ const identity = getIdentity();
8314
+ if (chatPK !== identity.chatPK || privateKey !== identity.chatPrivateKey || privateKey instanceof Uint8Array && !equalBytes(privateKey, privateKeyBytes)) {
8315
+ reset();
8316
+ chatPK = identity.chatPK;
8317
+ privateKey = identity.chatPrivateKey;
8318
+ privateKeyBytes = privateKey instanceof Uint8Array ? new Uint8Array(privateKey) : null;
8319
+ }
8320
+ return directAdmissionDecision(chatPK, profile, identity.chatAdmission, (peerChatPK, recipientChatPK) => {
8321
+ let commitments = peers.get(peerChatPK);
8322
+ if (!commitments?.has(recipientChatPK)) {
8323
+ const commitment = deriveCommitment(privateKey, chatPK, peerChatPK, recipientChatPK);
8324
+ if (!commitments) {
8325
+ commitments = new Map;
8326
+ if (peers.size >= CHAT_ADMISSION_CACHE_PEERS)
8327
+ peers.delete(peers.keys().next().value);
8328
+ }
8329
+ commitments.set(recipientChatPK, commitment);
8330
+ }
8331
+ peers.delete(peerChatPK);
8332
+ peers.set(peerChatPK, commitments);
8333
+ return commitments.get(recipientChatPK);
8334
+ });
8335
+ };
8336
+ return {
8337
+ directAdmissionForPeer: directAdmissionForPeer2,
8338
+ canStartDirectChat: (profile) => directAdmissionForPeer2(profile) !== CHAT_ADMISSION_DECISIONS.REJECT,
8339
+ reset
8340
+ };
8294
8341
  }
8295
8342
  function canInviteToGroup(profile, ownAdmission = null) {
8296
8343
  return (ownAdmission == null || normalizeChatAdmission(ownAdmission).groups === CHAT_GROUP_ADMISSION_MODES.OPEN) && normalizeChatAdmission(profile?.chatAdmission).groups === CHAT_GROUP_ADMISSION_MODES.OPEN;
@@ -10394,6 +10441,26 @@ function transitionCore(certificate) {
10394
10441
  return normalizeTransitionCore(certificate);
10395
10442
  }
10396
10443
 
10444
+ // ../../core/chat/capabilities.js
10445
+ function memberCount(value) {
10446
+ const count = Number(value);
10447
+ if (!Number.isSafeInteger(count) || count < 1 || count > CHAT_MAX_MEMBERS) {
10448
+ throw new Error("invalid chat member count");
10449
+ }
10450
+ return count;
10451
+ }
10452
+ function chatCapabilities(value) {
10453
+ const members = memberCount(value);
10454
+ return Object.freeze({
10455
+ members,
10456
+ sharedReceipts: members <= CHAT_RECEIPT_MAX_MEMBERS,
10457
+ ordinaryDelivery: members <= CHAT_ORDINARY_DELIVERY_MAX_MEMBERS
10458
+ });
10459
+ }
10460
+ function chatEpochCapabilities(epochState) {
10461
+ return chatCapabilities(epochState?.manifest?.members?.length);
10462
+ }
10463
+
10397
10464
  // ../../core/chat/ttl.js
10398
10465
  "use client";
10399
10466
  var CHAT_RETENTION_SEEN = "seen";
@@ -10406,6 +10473,10 @@ var CHAT_RETENTION_LABELS = Object.freeze({
10406
10473
  [CHAT_RETENTION_SEEN]: "delete after seen",
10407
10474
  [CHAT_RETENTION_24H]: "keep for 24h"
10408
10475
  });
10476
+ var CHAT_RETENTION_DESCRIPTIONS = Object.freeze({
10477
+ [CHAT_RETENTION_SEEN]: "messages now delete after being seen",
10478
+ [CHAT_RETENTION_24H]: "messages now delete 24h after being seen"
10479
+ });
10409
10480
  var MESSAGE_STORAGE_TTL_MS = CHAT_UNSAVED_TTL_MS;
10410
10481
  var AFTER_SEEN_MS = CHAT_AFTER_SEEN_MS;
10411
10482
  function hasChatRetention(value) {
@@ -11068,6 +11139,8 @@ async function prepareOwnChatMutation(identity, entryId, current, update) {
11068
11139
  open: (record) => openOwnChatMutationEntry(identity, entryId, record),
11069
11140
  prepare: async (record) => {
11070
11141
  if (!record) {
11142
+ if (current != null)
11143
+ throw makeChatUnavailableError();
11071
11144
  return prepareMutation(identity, entryId, null, update);
11072
11145
  }
11073
11146
  if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1) {
@@ -11827,26 +11900,6 @@ async function openChatMemberState(epoch, record) {
11827
11900
  return evidence.state;
11828
11901
  }
11829
11902
 
11830
- // ../../core/chat/capabilities.js
11831
- function memberCount(value) {
11832
- const count = Number(value);
11833
- if (!Number.isSafeInteger(count) || count < 1 || count > CHAT_MAX_MEMBERS) {
11834
- throw new Error("invalid chat member count");
11835
- }
11836
- return count;
11837
- }
11838
- function chatCapabilities(value) {
11839
- const members = memberCount(value);
11840
- return Object.freeze({
11841
- members,
11842
- sharedReceipts: members <= CHAT_RECEIPT_MAX_MEMBERS,
11843
- ordinaryDelivery: members <= CHAT_ORDINARY_DELIVERY_MAX_MEMBERS
11844
- });
11845
- }
11846
- function chatEpochCapabilities(epochState) {
11847
- return chatCapabilities(epochState?.manifest?.members?.length);
11848
- }
11849
-
11850
11903
  // ../../core/chat/memberstate.js
11851
11904
  "use client";
11852
11905
  function openStateEpoch(epochState, actor = null) {
@@ -12987,7 +13040,8 @@ function getMsgReactions(msg) {
12987
13040
  continue;
12988
13041
  }
12989
13042
  seen.add(user);
12990
- reactions.push({ emoji, user });
13043
+ const ts = timestampMs(item.ts, null);
13044
+ reactions.push({ emoji, user, ...ts == null ? {} : { ts } });
12991
13045
  if (reactions.length >= MAX_REACTIONS) {
12992
13046
  break;
12993
13047
  }
@@ -13864,7 +13918,7 @@ function senderIdentity(senderChatPK, senderPrivateKey, options) {
13864
13918
  notificationPrivateKey: options.notificationPrivateKey
13865
13919
  };
13866
13920
  }
13867
- function ownerPreview(senderChatPK, message, messageId, head, tsMs, ttlMs) {
13921
+ function ownerPreview(epoch, senderChatPK, message, messageId, head, tsMs, ttlMs) {
13868
13922
  return {
13869
13923
  ...message || {},
13870
13924
  s: senderChatPK,
@@ -13873,6 +13927,8 @@ function ownerPreview(senderChatPK, message, messageId, head, tsMs, ttlMs) {
13873
13927
  id: messageId,
13874
13928
  ts: tsMs,
13875
13929
  ttl: Number.isFinite(ttlMs) ? ttlMs : null,
13930
+ epochId: epoch.epochId,
13931
+ epochMemberChatPKs: epoch.manifest.members.map((member) => member.chatPK),
13876
13932
  pending: false,
13877
13933
  failed: false
13878
13934
  };
@@ -14089,7 +14145,7 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
14089
14145
  msgId: messageId,
14090
14146
  cid: head.cid,
14091
14147
  record: { lane: epoch.messageLane, head, body, ttlMs },
14092
- message: ownerPreview(identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
14148
+ message: ownerPreview(epoch, identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
14093
14149
  mentionRecipientChatPKs: mentionTargets.recipientChatPKs,
14094
14150
  tsMs
14095
14151
  };
@@ -18368,12 +18424,6 @@ function dropCachedChats(cache, chatIds, currentPayload = null) {
18368
18424
  ...ids.map((chatId) => dropCachedMessageState(cache, chatId))
18369
18425
  ]));
18370
18426
  }
18371
- function dropCachedChat(cache, chatId) {
18372
- if (!chatId) {
18373
- return;
18374
- }
18375
- dropCachedChats(cache, [chatId]);
18376
- }
18377
18427
  // ../../core/cache/localdata/balance.js
18378
18428
  "use client";
18379
18429
  function normalizedBalance(value) {
@@ -22547,6 +22597,17 @@ function createRouteMemory(limit = CHAT_MESSAGE_VIEW_CACHE_SIZE) {
22547
22597
  }
22548
22598
  return {
22549
22599
  clear,
22600
+ dropChat(chatId) {
22601
+ const id = String(chatId || "").trim();
22602
+ if (!id)
22603
+ return;
22604
+ const keys = chatRouteMemoryKeys(id, [...states.keys(), ...retained.keys()]);
22605
+ for (const key of keys) {
22606
+ states.delete(key);
22607
+ retained.delete(key);
22608
+ }
22609
+ activeChats.delete(id);
22610
+ },
22550
22611
  resetAccount(nextAccountKey) {
22551
22612
  const next = nextAccountKey || "";
22552
22613
  if (accountKey === next) {
@@ -24099,6 +24160,9 @@ async function verifyOpenedPingEpoch(context) {
24099
24160
  const decision = await decideIncomingChat(context, epochState);
24100
24161
  if (decision === CHAT_ADMISSION_DECISIONS.REJECT)
24101
24162
  return "rejected";
24163
+ const unavailable = await checkOpenedWelcomeAvailability(context);
24164
+ if (unavailable)
24165
+ return unavailable;
24102
24166
  if (decision === CHAT_ADMISSION_DECISIONS.REQUEST)
24103
24167
  return "request";
24104
24168
  } else if (context.entry && (epochState.manifest.lineage === "direct" || epochState.manifest.lineage === "group")) {
@@ -24108,6 +24172,27 @@ async function verifyOpenedPingEpoch(context) {
24108
24172
  }
24109
24173
  return null;
24110
24174
  }
24175
+ async function checkOpenedWelcomeAvailability(context) {
24176
+ const { cloud, identity, options, uid, epochState } = context;
24177
+ assertInboxLease(options);
24178
+ try {
24179
+ await cloud.chat.state.transition(epochState.manifest.chatId, epochState.manifest.epochId, epochState.stateCapability);
24180
+ assertInboxLease(options);
24181
+ return null;
24182
+ } catch (error) {
24183
+ const code = cleanText(error?.code).toLowerCase();
24184
+ if (code !== "permission-denied" && !code.endsWith("/permission-denied"))
24185
+ throw error;
24186
+ try {
24187
+ await retireConfirmedDeletedChat(cloud, uid, identity, context.committedEntry || context.entry || makeOwnChatEntry(epochState), options);
24188
+ return "deleted";
24189
+ } catch (confirmationError) {
24190
+ if (isRetryableInboxError(confirmationError))
24191
+ throw confirmationError;
24192
+ throw error;
24193
+ }
24194
+ }
24195
+ }
24111
24196
  function startOpenedWelcomeHydration(context) {
24112
24197
  const membershipWelcome = (!context.entry || context.restoringMembership) && context.payload.kind === "welcome";
24113
24198
  if (!membershipWelcome)
@@ -24344,17 +24429,30 @@ async function settleOpenedPing(prepared, onAuthoritative) {
24344
24429
  const createsMembership = !context.entry && context.payload.kind === "welcome";
24345
24430
  const ownerPersistence = createsMembership ? captureAsync(persistOpenedPingOwner(context)) : null;
24346
24431
  const messageStartedAt = Date.now();
24347
- const result = await applyOpenedPingMessage(context);
24432
+ const messageOutcome = await captureAsync(applyOpenedPingMessage(context));
24348
24433
  markDiag(context.options.diag, "chat.inbox.settle.message.done", {
24349
24434
  elapsedMs: Date.now() - messageStartedAt
24350
24435
  });
24351
- if (result)
24352
- return result;
24353
- prepareOpenedPingActivity(context);
24354
24436
  if (ownerPersistence)
24355
24437
  await unwrapAsync(ownerPersistence);
24356
- else
24357
- await persistOpenedPingOwner(context);
24438
+ else if (messageOutcome.error)
24439
+ throw messageOutcome.error;
24440
+ if (messageOutcome.value)
24441
+ return messageOutcome.value;
24442
+ if (!messageOutcome.error) {
24443
+ prepareOpenedPingActivity(context);
24444
+ if (!ownerPersistence)
24445
+ await persistOpenedPingOwner(context);
24446
+ }
24447
+ if (context.membershipWelcome) {
24448
+ const unavailable = await checkOpenedWelcomeAvailability(context);
24449
+ if (unavailable) {
24450
+ await cleanupOpenedPing(context);
24451
+ return unavailable;
24452
+ }
24453
+ }
24454
+ if (messageOutcome.error)
24455
+ throw messageOutcome.error;
24358
24456
  publishOpenedPing(context);
24359
24457
  onAuthoritative?.();
24360
24458
  const phaseStartedAt = Date.now();
@@ -24481,6 +24579,7 @@ async function preparePreparedPing(cloud, uid, identity, prepared, options) {
24481
24579
  ping,
24482
24580
  uid
24483
24581
  });
24582
+ assertInboxLease(options);
24484
24583
  return { opened, releaseMutation };
24485
24584
  } catch (error) {
24486
24585
  releaseMutation();
@@ -24935,10 +25034,11 @@ async function prepareInboxWork(context, prepared, processingOptions = {}) {
24935
25034
  const { blockedUids, cloud, identity, options, state, uid } = context;
24936
25035
  const { document, opened } = prepared;
24937
25036
  let announcement = null;
25037
+ let ping = null;
24938
25038
  try {
24939
25039
  const authenticated = await authenticateInboxPing(cloud, identity, document, opened, state.currentChats, context.profileCache);
24940
25040
  assertInboxLease(options);
24941
- const ping = await preparePreparedPing(cloud, uid, identity, authenticated, {
25041
+ ping = await preparePreparedPing(cloud, uid, identity, authenticated, {
24942
25042
  ...options,
24943
25043
  blockedUids,
24944
25044
  currentChats: state.currentChats,
@@ -24955,9 +25055,11 @@ async function prepareInboxWork(context, prepared, processingOptions = {}) {
24955
25055
  if (ping.opened?.result === "request") {
24956
25056
  await hydrateOpenedChatRequest(ping.opened.context);
24957
25057
  }
25058
+ assertInboxLease(options);
24958
25059
  announcement = announcePreparedWelcome(context, authenticated, ping, document, prepared.startedAt);
24959
25060
  return { announcement, document, ping, prepared, processingOptions, status: null };
24960
25061
  } catch (error) {
25062
+ ping?.releaseMutation();
24961
25063
  state.revokeAnnouncement(announcement);
24962
25064
  if (processingOptions.deferUnknownWelcome === true && error?.message === "chat mls welcome required") {
24963
25065
  markDiag(options.diag, "chat.inbox.document.deferred", { reason: "welcome_required" });
@@ -25085,6 +25187,8 @@ function createInboxSettlementController(options = {}) {
25085
25187
  for (const job of lane.jobs) {
25086
25188
  if (job.settled)
25087
25189
  continue;
25190
+ if (job.state === "ready")
25191
+ job.discard?.(job.prepared);
25088
25192
  job.settled = true;
25089
25193
  job.state = "settled";
25090
25194
  job.deferred.reject(error);
@@ -25129,9 +25233,12 @@ function createInboxSettlementController(options = {}) {
25129
25233
  }
25130
25234
  job.state = "preparing";
25131
25235
  Promise.resolve().then(job.prepare).then((prepared) => {
25132
- if (job.settled)
25236
+ if (job.settled) {
25237
+ job.discard?.(prepared);
25133
25238
  return;
25239
+ }
25134
25240
  if (!hasLease()) {
25241
+ job.discard?.(prepared);
25135
25242
  finishJob(job, inboxLeaseError());
25136
25243
  return;
25137
25244
  }
@@ -25152,6 +25259,7 @@ function createInboxSettlementController(options = {}) {
25152
25259
  if (!job || job.settled)
25153
25260
  continue;
25154
25261
  if (!hasLease()) {
25262
+ job.discard?.(job.prepared);
25155
25263
  finishJob(job, inboxLeaseError());
25156
25264
  continue;
25157
25265
  }
@@ -25187,6 +25295,7 @@ function createInboxSettlementController(options = {}) {
25187
25295
  const job = {
25188
25296
  chatId,
25189
25297
  deferred: inboxDeferred(),
25298
+ discard: task.discard,
25190
25299
  lane,
25191
25300
  prepare: task.prepare,
25192
25301
  prepared: null,
@@ -25244,6 +25353,10 @@ async function processInboxDocuments(context, documents, processingOptions = {})
25244
25353
  const deferred = [];
25245
25354
  await Promise.all(groups.map((group) => context.settlementController.schedule(cleanText(group[0]?.opened?.payload?.chatId), {
25246
25355
  prepare: () => context.discoveryPool.run(() => prepareInboxWork(context, group[0], processingOptions)),
25356
+ discard: (work) => {
25357
+ work?.ping?.releaseMutation();
25358
+ context.state.revokeAnnouncement(work?.announcement);
25359
+ },
25247
25360
  settle: async (first, markAuthoritative) => {
25248
25361
  if (await settleInboxWork(context, first, markAuthoritative) === "deferred")
25249
25362
  deferred.push(first.document);
@@ -25531,6 +25644,7 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25531
25644
  nextAfterChat: snapshot.nextAfterChat,
25532
25645
  hasMore: snapshot.hasMore,
25533
25646
  ownerListToken: snapshot.ownerListToken,
25647
+ sourceRevision: snapshot.sourceRevision,
25534
25648
  source: "server"
25535
25649
  });
25536
25650
  deleteConfirmedUserChats(cloud, uid, page.deletedEntryIds);
@@ -25755,7 +25869,8 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25755
25869
  latestOwnerSnapshot = {
25756
25870
  docs: entries,
25757
25871
  nextAfterChat: meta.nextAfterChat ?? null,
25758
- hasMore: !!meta.hasMore
25872
+ hasMore: !!meta.hasMore,
25873
+ sourceRevision: options.getSourceRevision?.()
25759
25874
  };
25760
25875
  queueOwnerSnapshot();
25761
25876
  }, onError, { limitCount: pageSize });
@@ -26246,15 +26361,15 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
26246
26361
  const ownerFor = (entry) => composeActivity(structuralOwnerFor(entry), entry);
26247
26362
  const cacheFor = (entry) => composeActivity(entry?.cache, entry);
26248
26363
  const sourceCanPublish = (chat, sourceRevision) => {
26364
+ const boundary = authorityBoundaries.get(chat?.id);
26365
+ if (boundary?.terminal)
26366
+ return false;
26249
26367
  if (!chat?.id || !Number.isSafeInteger(sourceRevision))
26250
26368
  return true;
26251
26369
  if (sourceRevision < sourceRevisionFloor)
26252
26370
  return false;
26253
- const boundary = authorityBoundaries.get(chat.id);
26254
26371
  if (!boundary)
26255
26372
  return true;
26256
- if (boundary.terminal)
26257
- return false;
26258
26373
  return sourceRevision >= boundary.revision || compareOwners(chat, boundary.owner) > 0;
26259
26374
  };
26260
26375
  const sourceCanRemove = (chatId, sourceRevision) => {
@@ -26394,21 +26509,33 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
26394
26509
  updatePeers(shown);
26395
26510
  return shown;
26396
26511
  };
26512
+ const clearRemovedOwners = (chatIds) => {
26513
+ if (!chatIds.length)
26514
+ return;
26515
+ const sources = getSources();
26516
+ for (const chatId of chatIds)
26517
+ sources.pendingDeleteIdsRef.current.delete(chatId);
26518
+ sources.onOwnerRemoved?.(chatIds);
26519
+ cache.removeMany(chatIds);
26520
+ const removedIds = new Set(chatIds);
26521
+ sources.setSelectedChat((current) => removedIds.has(current) ? null : current);
26522
+ };
26397
26523
  const convergeCompleteOwnerSet = (incomingIds = null, sourceRevision = null) => {
26398
- const evictedCacheIds = [];
26524
+ const removedIds = [];
26399
26525
  for (const [chatId, entry] of entries) {
26400
- if (!entry.observedOwner && !entry.committedOwner && !entry.announcement) {
26401
- if (!sourceCanRemove(chatId, sourceRevision))
26402
- continue;
26403
- recordTerminal(chatId, cacheFor(entry));
26404
- if (entry.cache)
26405
- evictedCacheIds.push(chatId);
26406
- entries.delete(chatId);
26407
- } else if (!incomingIds || !incomingIds.has(chatId)) {
26526
+ if (incomingIds ? incomingIds.has(chatId) : entry.observedOwner)
26527
+ continue;
26528
+ if (!sourceCanRemove(chatId, sourceRevision))
26529
+ continue;
26530
+ if (entry.announcement && !authorityBoundaries.get(chatId)?.owner) {
26408
26531
  entry.cache = null;
26532
+ continue;
26409
26533
  }
26534
+ recordTerminal(chatId, ownerFor(entry) || cacheFor(entry));
26535
+ entries.delete(chatId);
26536
+ removedIds.push(chatId);
26410
26537
  }
26411
- cache.removeMany(evictedCacheIds);
26538
+ clearRemovedOwners(removedIds);
26412
26539
  };
26413
26540
  const observeOwnerChats = (rows, options = {}) => {
26414
26541
  const incoming = (rows || []).filter((chat) => chat?.id && sourceCanPublish(chat, options.sourceRevision));
@@ -26484,6 +26611,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
26484
26611
  const announceWelcome = (announcement) => {
26485
26612
  if (!announcement?.chatId || !announcement?.token)
26486
26613
  return false;
26614
+ if (authorityBoundaries.get(announcement.chatId)?.terminal)
26615
+ return false;
26487
26616
  const sources = getSources();
26488
26617
  if (sources.pendingDeleteIdsRef.current.has(announcement.chatId))
26489
26618
  return false;
@@ -26520,7 +26649,6 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
26520
26649
  return false;
26521
26650
  if (!sourceCanRemove(chatId, options.sourceRevision))
26522
26651
  return false;
26523
- recordTerminal(chatId, cacheFor(entry));
26524
26652
  entry.cache = null;
26525
26653
  entry.presentation = null;
26526
26654
  cache.remove(chatId);
@@ -26530,15 +26658,12 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
26530
26658
  return true;
26531
26659
  };
26532
26660
  const removeOwner = (chatId, options = {}) => {
26533
- const sources = getSources();
26534
26661
  if (!chatId)
26535
26662
  return false;
26536
26663
  recordTerminal(chatId, ownerFor(entries.get(chatId)) || cacheFor(entries.get(chatId)));
26537
- sources.pendingDeleteIdsRef.current.delete(chatId);
26538
26664
  const removed = entries.delete(chatId);
26539
- cache.remove(chatId);
26665
+ clearRemovedOwners([chatId]);
26540
26666
  publishOwners({ ...options, warm: false });
26541
- sources.setSelectedChat((current) => current === chatId ? null : current);
26542
26667
  return removed;
26543
26668
  };
26544
26669
  const removeInboxOwner = (chatId, options = {}) => {
@@ -26886,7 +27011,7 @@ function createChatListListener({
26886
27011
  sync();
26887
27012
  }, CHAT_LIST_LISTENER_RETRY_MS);
26888
27013
  };
26889
- const subscription = subscribe(sources.cloud, sources.uid, sources.chatPK, sources.chatPrivateKey, (nextChats, _nextPeers, meta = {}) => handleUpdate(nextChats, meta, listenStartedAt, index.getAuthorityRevision()), (error) => {
27014
+ const subscription = subscribe(sources.cloud, sources.uid, sources.chatPK, sources.chatPrivateKey, (nextChats, _nextPeers, meta = {}) => handleUpdate(nextChats, meta, listenStartedAt, meta.sourceRevision), (error) => {
26890
27015
  const current = getSources();
26891
27016
  state.setIsChatListLive(false);
26892
27017
  markError(current.diag, "chat.provider.listen", listenStartedAt, error);
@@ -26897,6 +27022,7 @@ function createChatListListener({
26897
27022
  retry(error);
26898
27023
  }, {
26899
27024
  limitCount: CHAT_LIST_LIVE_COUNT,
27025
+ getSourceRevision: index.getAuthorityRevision,
26900
27026
  onInboxAnnouncement: index.announceWelcome,
26901
27027
  onInboxRevoke: index.revokeAnnouncement,
26902
27028
  onInboxCommit: index.commitOwner,
@@ -28343,6 +28469,7 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
28343
28469
  if (!identity.chatSigningSecret)
28344
28470
  throw new Error("unlocked chat signing identity required");
28345
28471
  const mls = await lifecycle.mls?.open?.();
28472
+ lifecycle.assertCurrent?.();
28346
28473
  if (!mls)
28347
28474
  throw new Error("mls engine required");
28348
28475
  const selfLeafId = lifecycle.selfLeafId || randomBytes3(32);
@@ -28363,7 +28490,7 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
28363
28490
  if (additions.length)
28364
28491
  mlsGroup = await mls.stageChange(mlsGroup.snapshot, { adds: additions });
28365
28492
  const material = deriveChatMlsEpochMaterial(mlsGroup.epochKey, { chatId, epochVersion: 1 });
28366
- const lineage = members.length === 1 ? CHAT_LINEAGES.SELF : members.length === 2 ? CHAT_LINEAGES.DIRECT : CHAT_LINEAGES.GROUP;
28493
+ const lineage = lifecycle.lineage || (members.length === 1 ? CHAT_LINEAGES.SELF : members.length === 2 ? CHAT_LINEAGES.DIRECT : CHAT_LINEAGES.GROUP);
28367
28494
  const manifest = makeInitialEpochManifest({
28368
28495
  chatId,
28369
28496
  epochId: material.epochId,
@@ -28426,6 +28553,7 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
28426
28553
  });
28427
28554
  initial.backend.membershipOutbox = queuedWelcomes.map(({ outboxId: outboxId2, sealed }) => ({ outboxId: outboxId2, sealed }));
28428
28555
  stage = "owner";
28556
+ lifecycle.assertCurrent?.();
28429
28557
  const registration = registerInitialDelivery(cloud, identity, initialState);
28430
28558
  const stateId = chatMlsStateId(identity.chatPrivateKey, chatId, initial.transitionCommitment);
28431
28559
  const ownerMlsState = await sealChatMlsState(identity.chatPrivateKey, stateId, { v: 1, chatId, transitionCommitment: initial.transitionCommitment }, mlsGroup.snapshot);
@@ -28437,6 +28565,8 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
28437
28565
  let stateError = null;
28438
28566
  try {
28439
28567
  stage = "state";
28568
+ lifecycle.assertCurrent?.();
28569
+ lifecycle.onCreateDispatched?.(initial.manifest.chatId);
28440
28570
  await cloud.chat.state.create({
28441
28571
  ...initial.backend,
28442
28572
  ownerEntry: owner.write,
@@ -28771,21 +28901,23 @@ function createChatSessionLaunches({
28771
28901
  if (!opened?.chatId || !opened?.entry)
28772
28902
  return "";
28773
28903
  const chat = projectOwnChatEntry(opened.entry, opened.entryId || "", operation.identity.chatPK, opened.tsMs || Date.now());
28774
- operation.listOwner.commitOwner(chat);
28904
+ if (!operation.listOwner.commitOwner(chat))
28905
+ throw makeChatUnavailableError();
28775
28906
  return opened.chatId;
28776
28907
  };
28777
28908
  const presentOpenedChat = (opened, operation) => {
28778
28909
  assertCurrent(operation);
28779
28910
  if (!opened?.chatId || !opened?.entry)
28780
28911
  return "";
28781
- operation.pendingOwner.adoptOpenedChatId(opened.chatId);
28782
28912
  const chatId = commitOpenedChat(opened, operation);
28913
+ operation.pendingOwner.adoptOpenedChatId(chatId);
28783
28914
  pendingPeerSelect = null;
28784
28915
  setSelectedChat(chatId);
28785
28916
  return chatId;
28786
28917
  };
28787
28918
  const createChat = async (profiles, settings, lifecycle, adoptOpenedChat, operation, { profilesCurrent = false } = {}) => {
28788
28919
  assertCurrent(operation);
28920
+ lifecycle.assertCurrent?.();
28789
28921
  const { blocked, chatPK, chatPrivateKey } = operation.identity;
28790
28922
  let profileList = Array.isArray(profiles) ? profiles : [profiles];
28791
28923
  if (!profilesCurrent && profileList.length) {
@@ -28793,9 +28925,10 @@ function createChatSessionLaunches({
28793
28925
  }
28794
28926
  const blockedSet = new Set(blocked);
28795
28927
  const requestedMembers = profileList.map(profileMember);
28796
- if (profileList.length === 1) {
28928
+ const expectedLineage = lifecycle.lineage || (requestedMembers.length === 0 ? "self" : requestedMembers.length === 1 ? "direct" : "group");
28929
+ if (expectedLineage === "direct") {
28797
28930
  assertDirectAdmission(profileList[0], operation);
28798
- } else if (profileList.length > 1) {
28931
+ } else if (expectedLineage === "group") {
28799
28932
  if (normalizeChatAdmission(operation.identity.chatAdmission).groups === CHAT_GROUP_ADMISSION_MODES.CLOSED) {
28800
28933
  throw new Error("group chats are closed");
28801
28934
  }
@@ -28809,8 +28942,7 @@ function createChatSessionLaunches({
28809
28942
  if (blockedSet.has(member.uid))
28810
28943
  throw new Error("blocked peer cannot be added");
28811
28944
  }
28812
- const canonicalChatId = lifecycle.chatId || (requestedMembers.length === 0 ? deriveSelfChatId(chatPrivateKey, chatPK) : "");
28813
- const expectedLineage = requestedMembers.length === 0 ? "self" : requestedMembers.length === 1 ? "direct" : "group";
28945
+ const canonicalChatId = lifecycle.chatId || (expectedLineage === "self" ? deriveSelfChatId(chatPrivateKey, chatPK) : "");
28814
28946
  const expectedMemberKeys = [chatPK, ...requestedMembers.map((member) => member.chatPK)].sort();
28815
28947
  const matchingExistingChat = () => {
28816
28948
  assertCurrent(operation);
@@ -28849,6 +28981,7 @@ function createChatSessionLaunches({
28849
28981
  markDiag(diag, "chat.group.create.prekeys.start", { members: requestedMembers.length });
28850
28982
  const additions = await prepareMlsAdditions(profileList);
28851
28983
  assertCurrent(operation);
28984
+ lifecycle.assertCurrent?.();
28852
28985
  markDiag(diag, "chat.group.create.prekeys.done", { members: requestedMembers.length, elapsedMs: Date.now() - prekeyStartedAt });
28853
28986
  if (!additions.length && requestedMembers.length)
28854
28987
  throw new Error("peer mls key package required");
@@ -28858,15 +28991,23 @@ function createChatSessionLaunches({
28858
28991
  const created = await createGroupChat(cloud, operation.transitionIdentity, [...byKey.values()], settings, {
28859
28992
  mls,
28860
28993
  chatId: canonicalChatId || undefined,
28994
+ lineage: expectedLineage,
28861
28995
  wakeMessageId: lifecycle.wakeMessageId,
28996
+ assertCurrent: () => {
28997
+ assertCurrent(operation);
28998
+ lifecycle.assertCurrent?.();
28999
+ },
29000
+ onCreateDispatched: lifecycle.onCreateDispatched,
28862
29001
  onStateReady: (opened) => {
28863
29002
  assertCurrent(operation);
29003
+ lifecycle.assertCurrent?.();
28864
29004
  stateReady = true;
28865
29005
  const chatId = adoptOpenedChat(opened);
28866
29006
  lifecycle.onStateReady?.(chatId, opened, { created: true });
28867
29007
  },
28868
29008
  onOwnerReady: async (opened) => {
28869
29009
  assertCurrent(operation);
29010
+ lifecycle.assertCurrent?.();
28870
29011
  const chatId = adoptOpenedChat(opened);
28871
29012
  const result = await lifecycle.onOwnerReady?.(chatId, opened, { created: true });
28872
29013
  assertCurrent(operation);
@@ -28877,7 +29018,7 @@ function createChatSessionLaunches({
28877
29018
  return adoptOpenedChat(created);
28878
29019
  } catch (error) {
28879
29020
  assertCurrent(operation);
28880
- if (stateReady || !canonicalChatId || requestedMembers.length > 0)
29021
+ if (stateReady || !canonicalChatId)
28881
29022
  throw error;
28882
29023
  await ensureChat(canonicalChatId);
28883
29024
  assertCurrent(operation);
@@ -28891,6 +29032,10 @@ function createChatSessionLaunches({
28891
29032
  const operation = operationContext();
28892
29033
  return createChat(profiles, settings, lifecycle, (opened) => presentOpenedChat(opened, operation), operation);
28893
29034
  };
29035
+ const materializeGroupChat = (profiles, settings = {}, lifecycle = {}) => {
29036
+ const operation = operationContext();
29037
+ return createChat(profiles, settings, lifecycle, (opened) => commitOpenedChat(opened, operation), operation);
29038
+ };
28894
29039
  const runExistingDirectLifecycle = async (chatId, lifecycle, operation) => {
28895
29040
  assertCurrent(operation);
28896
29041
  const existing = getOwnerChat(chatId);
@@ -28962,6 +29107,7 @@ function createChatSessionLaunches({
28962
29107
  getOpenOperation: (chatId) => directOpens.get(chatId)?.promise || null,
28963
29108
  getPeerChatId,
28964
29109
  materializeDirectChat,
29110
+ materializeGroupChat,
28965
29111
  openDirectChat,
28966
29112
  openNotesChat,
28967
29113
  releaseOpenOperation: (chatId) => {
@@ -29026,16 +29172,21 @@ function createChatSessionPending({
29026
29172
  getPeerProfile,
29027
29173
  resolvePeerProfile,
29028
29174
  setLocalByChat,
29029
- getLocalByChat = () => new Map
29175
+ getLocalByChat = () => new Map,
29176
+ retireMaterializedChat = () => {}
29030
29177
  }) {
29178
+ const admission = createChatAdmission({ getIdentity });
29031
29179
  let pendingLaunch = null;
29032
29180
  const pendingOperations = new Map;
29033
29181
  let generation = 0;
29182
+ const operationForChat = (chatId) => pendingOperations.get(chatId) || [...pendingOperations.values()].find((state) => state.canonicalChatId === chatId) || null;
29183
+ const launchForChat = (chatId) => pendingLaunch && [pendingLaunch.id, pendingLaunch.canonicalChatId].includes(chatId) ? pendingLaunch : operationForChat(chatId)?.launch || storedLaunch(chatId);
29034
29184
  const storedLaunch = (chatId) => getLocalByChat().get(chatId)?.find((message) => message.chatDraft)?.chatDraft || null;
29035
29185
  const getPendingLaunch = () => {
29036
29186
  if (!pendingLaunch)
29037
29187
  return null;
29038
- const messages = getLocalByChat().get(pendingLaunch.id) || [];
29188
+ const localByChat = getLocalByChat();
29189
+ const messages = localByChat.get(pendingLaunch.id) || localByChat.get(pendingLaunch.canonicalChatId) || [];
29039
29190
  const message = messages[messages.length - 1] || null;
29040
29191
  return { ...pendingLaunch, messages, message, chat: {
29041
29192
  ...pendingLaunch.chat,
@@ -29043,23 +29194,33 @@ function createChatSessionPending({
29043
29194
  ts: message ? timestampMs(message.ts, null) : null
29044
29195
  } };
29045
29196
  };
29197
+ const resolvePendingChatId = (chatId) => {
29198
+ const canonicalId = operationForChat(chatId)?.canonicalChatId || launchForChat(chatId)?.canonicalChatId;
29199
+ return canonicalId && getOwnerChat(canonicalId) ? canonicalId : chatId;
29200
+ };
29046
29201
  const finish = (launchId) => {
29047
29202
  if (pendingLaunch?.id !== launchId)
29048
29203
  return;
29204
+ if (pendingLaunch.canonicalChatId !== launchId) {
29205
+ publish();
29206
+ return;
29207
+ }
29049
29208
  pendingLaunch = null;
29050
29209
  pendingOperations.delete(launchId);
29051
29210
  publish();
29052
29211
  };
29053
29212
  const track = (launchId, operation) => {
29054
29213
  const tracked = Promise.resolve(operation);
29055
- pendingOperations.set(launchId, tracked);
29214
+ const state = pendingOperations.get(launchId) || { active: true, promise: null };
29215
+ state.promise = tracked;
29216
+ pendingOperations.set(launchId, state);
29056
29217
  tracked.then(() => {
29057
- if (pendingOperations.get(launchId) !== tracked)
29218
+ if (pendingOperations.get(launchId) !== state || state.promise !== tracked)
29058
29219
  return;
29059
29220
  pendingOperations.delete(launchId);
29060
29221
  finish(launchId);
29061
29222
  }, () => {
29062
- if (pendingOperations.get(launchId) !== tracked)
29223
+ if (pendingOperations.get(launchId) !== state || state.promise !== tracked)
29063
29224
  return;
29064
29225
  pendingOperations.delete(launchId);
29065
29226
  if (pendingLaunch?.id !== launchId)
@@ -29071,6 +29232,12 @@ function createChatSessionPending({
29071
29232
  });
29072
29233
  return tracked;
29073
29234
  };
29235
+ const cancelOperation = (launchId) => {
29236
+ const operation = pendingOperations.get(launchId);
29237
+ if (operation)
29238
+ operation.active = false;
29239
+ pendingOperations.delete(launchId);
29240
+ };
29074
29241
  const show = (launch) => {
29075
29242
  if (pendingLaunch?.id && pendingLaunch.id !== launch.id && !pendingOperations.has(pendingLaunch.id)) {
29076
29243
  getRouteOwner().releaseOpenOperation?.(pendingLaunch.id);
@@ -29166,7 +29333,7 @@ function createChatSessionPending({
29166
29333
  return cached;
29167
29334
  return (fallbackProfiles || []).find((profile) => profileMatchesPeer(profile, peer)) || null;
29168
29335
  };
29169
- const profileAcceptsDirect = (identity, profile) => !!identity?.chatPK && !!identity?.chatPrivateKey && directAdmissionForPeer(identity.chatPrivateKey, identity.chatPK, profile, identity.chatAdmission) !== CHAT_ADMISSION_DECISIONS.REJECT;
29336
+ const profileAcceptsDirect = (identity, profile) => !!identity?.chatPK && !!identity?.chatPrivateKey && admission.directAdmissionForPeer(profile) !== CHAT_ADMISSION_DECISIONS.REJECT;
29170
29337
  const isChatAdmitted = (chat, fallbackProfiles = []) => {
29171
29338
  const identity = getIdentity();
29172
29339
  if (!chat)
@@ -29299,18 +29466,29 @@ function createChatSessionPending({
29299
29466
  throw error;
29300
29467
  }
29301
29468
  };
29302
- const sendDurableAttachment = async (chatId, attachment, options = {}) => {
29303
- const target = await admittedSendTarget(chatId, options);
29304
- return getSendActions().sendAttachment(target.routeAnchor, attachment, target.options);
29305
- };
29306
- const sendDurableSharedAttachment = async (chatId, attachment, options = {}) => {
29469
+ const withReadyAttachmentTarget = async (chatId, options, send) => {
29470
+ const attemptGeneration = generation;
29471
+ await waitForSend(chatId, { expiresAt: options.expiresAt, signal: options.signal });
29472
+ if (attemptGeneration !== generation)
29473
+ throw makeChatUnavailableError();
29307
29474
  const target = await admittedSendTarget(chatId, options);
29308
- return getSendActions().sendSharedAttachment(target.routeAnchor, attachment, target.options);
29475
+ if (attemptGeneration !== generation)
29476
+ throw makeChatUnavailableError();
29477
+ return send(target);
29309
29478
  };
29479
+ const sendDurableAttachment = (chatId, attachment, options = {}) => withReadyAttachmentTarget(chatId, options, (target) => getSendActions().sendAttachment(target.routeAnchor, attachment, target.options));
29480
+ const sendDurableSharedAttachment = (chatId, attachment, options = {}) => withReadyAttachmentTarget(chatId, options, (target) => getSendActions().sendSharedAttachment(target.routeAnchor, attachment, target.options));
29310
29481
  const stagePendingMessage = (launchId, local) => {
29311
- if (!pendingLaunch || pendingLaunch.id !== launchId)
29482
+ const launch = launchForChat(launchId);
29483
+ if (!launch || launch.id !== launchId)
29312
29484
  throw makeChatUnavailableError();
29313
- const { id, profiles, settings, chat } = pendingLaunch;
29485
+ const { id, profiles, settings, chat } = launch;
29486
+ const canonicalId = resolvePendingChatId(launchId);
29487
+ if (getOwnerChat(canonicalId)) {
29488
+ const { chatDraft: _draft, ...content } = local;
29489
+ setLocalByChat((current) => addLocalMessage(current, canonicalId, content));
29490
+ return;
29491
+ }
29314
29492
  const chatDraft = { id, canonicalChatId: id, profiles, settings, chat };
29315
29493
  setLocalByChat((current) => addLocalMessage(current, launchId, { ...local, chatDraft }));
29316
29494
  };
@@ -29322,6 +29500,8 @@ function createChatSessionPending({
29322
29500
  const canonical = targetId !== launchId ? current.get(targetId) || [] : [];
29323
29501
  const writerIds = new Set(canonical.map((message) => message.cid));
29324
29502
  const staged = (current.get(launchId) || []).filter((message) => !writerIds.has(message.cid));
29503
+ if (!canonical.length && !staged.length)
29504
+ return current;
29325
29505
  next.set(targetId, [...canonical, ...staged].map((message) => {
29326
29506
  const { chatDraft, ...content } = message;
29327
29507
  return {
@@ -29337,14 +29517,22 @@ function createChatSessionPending({
29337
29517
  if (getOwnerChat(canonicalId) && pendingLaunch?.id === launchId)
29338
29518
  setSelectedChat(canonicalId);
29339
29519
  };
29340
- const retirePeer = (peerChatPK) => {
29341
- const launch = pendingLaunch;
29342
- if (!launch?.chat?.members?.some((member) => member?.chatPK === peerChatPK))
29520
+ const dropChat = (chatId) => {
29521
+ const launch = launchForChat(chatId);
29522
+ if (!launch || launch.id !== chatId && launch.canonicalChatId !== chatId)
29343
29523
  return false;
29344
29524
  beginSelection();
29345
29525
  getRouteOwner().releaseOpenOperation?.(launch.id);
29346
- pendingLaunch = null;
29347
- pendingOperations.delete(launch.id);
29526
+ if (pendingLaunch?.id === launch.id)
29527
+ pendingLaunch = null;
29528
+ cancelOperation(launch.id);
29529
+ setLocalByChat((current) => {
29530
+ if (!current.has(launch.id))
29531
+ return current;
29532
+ const next = new Map(current);
29533
+ next.delete(launch.id);
29534
+ return next;
29535
+ });
29348
29536
  if (getSelectedChatId() === launch.id || getSelectedChatId() === launch.canonicalChatId) {
29349
29537
  setSelectedChat(null);
29350
29538
  } else {
@@ -29352,25 +29540,74 @@ function createChatSessionPending({
29352
29540
  }
29353
29541
  return true;
29354
29542
  };
29543
+ const retirePeer = (peerChatPK) => pendingLaunch?.chat?.members?.some((member) => member?.chatPK === peerChatPK) ? dropChat(pendingLaunch.id) : false;
29544
+ const discardChat = async (chatId) => {
29545
+ const state = operationForChat(chatId);
29546
+ if (!state?.dispatched)
29547
+ return { discarded: dropChat(chatId), chat: null };
29548
+ state.discarding = true;
29549
+ if (pendingLaunch?.id === state.launch.id)
29550
+ pendingLaunch = null;
29551
+ if ([state.launch.id, state.canonicalChatId].includes(getSelectedChatId()))
29552
+ setSelectedChat(null);
29553
+ else
29554
+ publish();
29555
+ state.retirementTarget ||= getOwnerChat(state.canonicalChatId) || null;
29556
+ if (state.retirementTarget)
29557
+ retireMaterializedChat(state.canonicalChatId);
29558
+ const attemptGeneration = generation;
29559
+ await state.promise.catch(() => null);
29560
+ if (attemptGeneration !== generation)
29561
+ throw makeChatUnavailableError();
29562
+ if (!state.retirementTarget) {
29563
+ if (!getOwnerChat(state.canonicalChatId))
29564
+ await ensureChat(state.canonicalChatId);
29565
+ state.retirementTarget = getOwnerChat(state.canonicalChatId) || null;
29566
+ }
29567
+ if (attemptGeneration !== generation || !state.retirementTarget)
29568
+ throw makeChatUnavailableError();
29569
+ retireMaterializedChat(state.canonicalChatId);
29570
+ return { discarded: true, chat: state.retirementTarget };
29571
+ };
29355
29572
  const materialize = (launchId, wakeMessageId, sendInitial) => {
29356
29573
  const launch = pendingLaunch;
29357
29574
  if (!launch || launch.id !== launchId || pendingOperations.has(launchId))
29358
29575
  throw makeChatUnavailableError();
29359
29576
  const attemptGeneration = generation;
29577
+ const state = { active: true, promise: null, launch, canonicalChatId: launchId };
29578
+ pendingOperations.set(launchId, state);
29579
+ const assertCurrent = () => {
29580
+ if (attemptGeneration !== generation || !state.active)
29581
+ throw makeChatUnavailableError();
29582
+ };
29360
29583
  const expiresAt = Date.now() + CHAT_SEND_CONNECTION_WAIT_MS;
29361
29584
  let canonicalId = launchId;
29362
29585
  const operation = (async () => {
29363
29586
  await waitForSend(launchId, { expiresAt });
29364
- if (attemptGeneration !== generation)
29365
- throw makeChatUnavailableError();
29587
+ assertCurrent();
29366
29588
  let initialResult = null;
29367
29589
  const lifecycle = {
29368
29590
  wakeMessageId,
29591
+ assertCurrent,
29592
+ onCreateDispatched: (chatId) => {
29593
+ assertCurrent();
29594
+ state.dispatched = true;
29595
+ state.canonicalChatId = chatId;
29596
+ state.launch = { ...state.launch, canonicalChatId: chatId };
29597
+ },
29369
29598
  onStateReady: (chatId) => {
29599
+ assertCurrent();
29370
29600
  canonicalId = chatId;
29601
+ state.canonicalChatId = chatId;
29602
+ state.launch = { ...state.launch, canonicalChatId: chatId };
29371
29603
  adoptOpenedChatId(chatId, launchId);
29604
+ if (state.discarding) {
29605
+ state.retirementTarget = getOwnerChat(chatId);
29606
+ retireMaterializedChat(chatId);
29607
+ }
29372
29608
  },
29373
29609
  onOwnerReady: async (chatId, _opened, context) => {
29610
+ assertCurrent();
29374
29611
  canonicalId = chatId;
29375
29612
  initialResult = await sendInitial(chatId, { ...context, expiresAt });
29376
29613
  return initialResult;
@@ -29385,18 +29622,20 @@ function createChatSessionPending({
29385
29622
  canonicalId = resolvedChatId;
29386
29623
  initialResult = await sendInitial(resolvedChatId, { created: false, expiresAt });
29387
29624
  } else {
29388
- canonicalId = await getRouteOwner().createGroupChat(launch.profiles, launch.settings, {
29625
+ canonicalId = await getRouteOwner().materializeGroupChat(launch.profiles, launch.settings, {
29389
29626
  ...lifecycle,
29390
- chatId: launchId
29627
+ chatId: launch.canonicalChatId,
29628
+ lineage: launch.chat.lineage
29391
29629
  });
29392
29630
  }
29393
29631
  }
29394
29632
  if (attemptGeneration !== generation)
29395
29633
  throw makeChatUnavailableError();
29396
- settleMaterialization(launchId, canonicalId);
29634
+ if (state.active)
29635
+ settleMaterialization(launchId, canonicalId);
29397
29636
  return { ...initialResult || {}, chatId: canonicalId };
29398
29637
  })().catch((error) => {
29399
- if (attemptGeneration === generation)
29638
+ if (attemptGeneration === generation && state.active)
29400
29639
  settleMaterialization(launchId, canonicalId, true);
29401
29640
  throw error;
29402
29641
  });
@@ -29407,10 +29646,74 @@ function createChatSessionPending({
29407
29646
  ...context?.expiresAt ? { expiresAt: context.expiresAt } : {},
29408
29647
  ...context?.created === true ? { deliverRecipients: false, ownerImmediate: true } : {}
29409
29648
  });
29649
+ const runChatMutation = (chatId, mutate, { initialProfiles } = {}) => {
29650
+ const launch = launchForChat(chatId);
29651
+ const canonicalId = resolvePendingChatId(chatId);
29652
+ const active = launch && pendingOperations.get(launch.id);
29653
+ if (active?.discarding)
29654
+ throw makeChatUnavailableError();
29655
+ if (!active && getOwnerChat(canonicalId))
29656
+ return mutate(canonicalId, { created: false });
29657
+ if (!launch || getIdentity().chatBanned)
29658
+ throw makeChatUnavailableError();
29659
+ const attemptGeneration = generation;
29660
+ const perform = async (id, context) => {
29661
+ if (attemptGeneration !== generation)
29662
+ throw makeChatUnavailableError();
29663
+ const value = await mutate(id, context);
29664
+ if (attemptGeneration !== generation)
29665
+ throw makeChatUnavailableError();
29666
+ const ownerEntry = getOwnerChat(id)?.ownEntry;
29667
+ if (!ownerEntry)
29668
+ throw makeChatUnavailableError();
29669
+ return { value, ownerEntry };
29670
+ };
29671
+ if (active) {
29672
+ return track(launch.id, active.promise.then(async (result) => {
29673
+ if (!active.active)
29674
+ throw makeChatUnavailableError();
29675
+ const id = result?.chatId || launch.canonicalChatId;
29676
+ return { ...await perform(id, { created: false }), chatId: id };
29677
+ })).then((result) => result.value);
29678
+ }
29679
+ pendingLaunch = launch;
29680
+ if (initialProfiles) {
29681
+ if (launch.chat.lineage === "self")
29682
+ throw new Error("self chat membership is fixed");
29683
+ const profiles = initialProfiles(launch.profiles);
29684
+ if (profiles === launch.profiles)
29685
+ return Promise.resolve({ unchanged: true });
29686
+ const members = profiles.map(profileMember);
29687
+ chatCapabilities(members.length + 1);
29688
+ const self = memberFromChatIdentity(transitionIdentity());
29689
+ if (members.some((member) => member.chatPK === self.chatPK))
29690
+ throw new Error("current user is already a chat member");
29691
+ const lineage = launch.chat.lineage === "group" || members.length !== 1 ? "group" : launch.chat.lineage;
29692
+ pendingLaunch = {
29693
+ ...launch,
29694
+ canonicalChatId: lineage === "group" && launch.chat.lineage !== "group" ? randomChatId() : launch.canonicalChatId,
29695
+ profiles: profiles.map((profile, index) => pendingProfile(profile, members[index])),
29696
+ chat: {
29697
+ ...launch.chat,
29698
+ lineage,
29699
+ peerChatPK: lineage === "direct" ? members[0]?.chatPK || null : null,
29700
+ members: [self, ...members],
29701
+ memberCount: members.length + 1,
29702
+ memberChatPKs: [self.chatPK, ...members.map((member) => member.chatPK)],
29703
+ memberUids: [self.uid, ...members.map((member) => member.uid)]
29704
+ }
29705
+ };
29706
+ publish();
29707
+ }
29708
+ return materialize(launch.id, "", (id, context) => perform(id, {
29709
+ ...context,
29710
+ ...initialSendOptions({}, context)
29711
+ })).then((result) => result.value);
29712
+ };
29410
29713
  const sendPendingMessage = (launchId, message, options = {}) => {
29411
29714
  const { chatPK } = getIdentity();
29412
- const launch = pendingLaunch;
29413
- const previousOperation = pendingOperations.get(launchId);
29715
+ const launch = launchForChat(launchId);
29716
+ const previousOperation = pendingOperations.get(launchId)?.promise;
29414
29717
  if (!launch || launch.id !== launchId)
29415
29718
  throw makeChatUnavailableError();
29416
29719
  const routeAnchor = launch.chat?.members?.find((member) => member.chatPK !== chatPK)?.chatPK || chatPK;
@@ -29429,8 +29732,8 @@ function createChatSessionPending({
29429
29732
  };
29430
29733
  const sendPendingAttachment = (launchId, attachment, options = {}) => {
29431
29734
  const { chatPK } = getIdentity();
29432
- const launch = pendingLaunch;
29433
- const previousOperation = pendingOperations.get(launchId);
29735
+ const launch = launchForChat(launchId);
29736
+ const previousOperation = pendingOperations.get(launchId)?.promise;
29434
29737
  if (!launch || launch.id !== launchId)
29435
29738
  throw makeChatUnavailableError();
29436
29739
  const routeAnchor = launch.chat?.members?.find((member) => member.chatPK !== chatPK)?.chatPK || chatPK;
@@ -29445,8 +29748,8 @@ function createChatSessionPending({
29445
29748
  };
29446
29749
  const sendPendingSharedAttachment = (launchId, attachment, options = {}) => {
29447
29750
  const { chatPK } = getIdentity();
29448
- const launch = pendingLaunch;
29449
- const previousOperation = pendingOperations.get(launchId);
29751
+ const launch = launchForChat(launchId);
29752
+ const previousOperation = pendingOperations.get(launchId)?.promise;
29450
29753
  if (!launch || launch.id !== launchId)
29451
29754
  throw makeChatUnavailableError();
29452
29755
  const routeAnchor = launch.chat?.members?.find((member) => member.chatPK !== chatPK)?.chatPK || chatPK;
@@ -29459,6 +29762,12 @@ function createChatSessionPending({
29459
29762
  return track(launchId, previousOperation.then((result) => sendDurableSharedAttachment(result?.chatId || launchId, queuedAttachment, options)));
29460
29763
  };
29461
29764
  const sendChatMessage = (chatId, message, options = {}) => {
29765
+ const operation = operationForChat(chatId);
29766
+ if (operation?.discarding)
29767
+ throw makeChatUnavailableError();
29768
+ if (operation)
29769
+ return sendPendingMessage(operation.launch.id, message, options);
29770
+ chatId = resolvePendingChatId(chatId);
29462
29771
  if (!getOwnerChat(chatId)) {
29463
29772
  if (!pendingLaunch)
29464
29773
  pendingLaunch = storedLaunch(chatId);
@@ -29469,9 +29778,10 @@ function createChatSessionPending({
29469
29778
  return sendDurableMessage(chatId, message, options);
29470
29779
  };
29471
29780
  const retryMessage = (chatId, cid) => {
29781
+ chatId = resolvePendingChatId(chatId);
29472
29782
  const attemptGeneration = generation;
29473
29783
  const launch = pendingLaunch && (pendingLaunch.id === chatId || pendingLaunch.canonicalChatId === chatId) ? pendingLaunch : storedLaunch(chatId);
29474
- const failed = getLocalByChat().get(launch?.id || chatId)?.find((message) => message.cid === cid && message.failed);
29784
+ const failed = getLocalByChat().get(chatId)?.find((message) => message.cid === cid && message.failed);
29475
29785
  if (!failed)
29476
29786
  return false;
29477
29787
  if (launch && !getOwnerChat(chatId)) {
@@ -29508,8 +29818,24 @@ function createChatSessionPending({
29508
29818
  getSendActions().discardLocalMessage(chatId, cid);
29509
29819
  return true;
29510
29820
  };
29511
- const sendChatAttachment = (chatId, attachment, options = {}) => pendingLaunch?.id === chatId && !getOwnerChat(chatId) ? sendPendingAttachment(chatId, attachment, options) : sendDurableAttachment(chatId, attachment, options);
29512
- const sendChatSharedAttachment = (chatId, attachment, options = {}) => pendingLaunch?.id === chatId && !getOwnerChat(chatId) ? sendPendingSharedAttachment(chatId, attachment, options) : sendDurableSharedAttachment(chatId, attachment, options);
29821
+ const sendChatAttachment = (chatId, attachment, options = {}) => {
29822
+ const operation = operationForChat(chatId);
29823
+ if (operation?.discarding)
29824
+ throw makeChatUnavailableError();
29825
+ if (operation)
29826
+ return sendPendingAttachment(operation.launch.id, attachment, options);
29827
+ chatId = resolvePendingChatId(chatId);
29828
+ return pendingLaunch?.id === chatId && !getOwnerChat(chatId) ? sendPendingAttachment(chatId, attachment, options) : sendDurableAttachment(chatId, attachment, options);
29829
+ };
29830
+ const sendChatSharedAttachment = (chatId, attachment, options = {}) => {
29831
+ const operation = operationForChat(chatId);
29832
+ if (operation?.discarding)
29833
+ throw makeChatUnavailableError();
29834
+ if (operation)
29835
+ return sendPendingSharedAttachment(operation.launch.id, attachment, options);
29836
+ chatId = resolvePendingChatId(chatId);
29837
+ return pendingLaunch?.id === chatId && !getOwnerChat(chatId) ? sendPendingSharedAttachment(chatId, attachment, options) : sendDurableSharedAttachment(chatId, attachment, options);
29838
+ };
29513
29839
  const sendDirectAttachment = async (profile, attachment, options = {}) => sendChatAttachment(await getRouteOwner().openDirectChat(profile), attachment, options);
29514
29840
  const sendMaterializedDirect = async (profile, payload, options, send) => {
29515
29841
  const queuedPayload = { ...payload, cid: makeSendCid(payload) };
@@ -29556,6 +29882,10 @@ function createChatSessionPending({
29556
29882
  return settleNormalizedTargets(normalizedTargets, (target) => sendPickerTarget(target, (chatId) => sendChatSharedAttachment(chatId, attachment), (profile) => sendPickerDirectSharedAttachment(profile, attachment)));
29557
29883
  };
29558
29884
  return {
29885
+ directAdmissionForPeer: admission.directAdmissionForPeer,
29886
+ dropChat,
29887
+ discardChat,
29888
+ canStartDirectChat: admission.canStartDirectChat,
29559
29889
  adoptOpenedChatId,
29560
29890
  adoptResolved,
29561
29891
  beginPendingChat,
@@ -29563,8 +29893,11 @@ function createChatSessionPending({
29563
29893
  canSendToChat,
29564
29894
  isChatAdmitted,
29565
29895
  isPendingLaunchAdmitted,
29566
- isPendingChat: (chatId) => pendingLaunch?.id === chatId || !!storedLaunch(chatId),
29896
+ isPendingChat: (chatId) => !!launchForChat(chatId),
29567
29897
  getPendingLaunch,
29898
+ getActionChat: (chatId) => getOwnerChat(resolvePendingChatId(chatId)) || launchForChat(chatId)?.chat || null,
29899
+ hasPendingChatOperation: (chatId) => !!operationForChat(chatId),
29900
+ runChatMutation,
29568
29901
  openNewChat,
29569
29902
  retirePeer,
29570
29903
  reconcileAdmission: () => {
@@ -29574,7 +29907,7 @@ function createChatSessionPending({
29574
29907
  beginSelection();
29575
29908
  getRouteOwner().clearOpenOperations?.();
29576
29909
  pendingLaunch = null;
29577
- pendingOperations.delete(launch.id);
29910
+ cancelOperation(launch.id);
29578
29911
  if (getSelectedChatId() === launch.id || getSelectedChatId() === launch.canonicalChatId) {
29579
29912
  setSelectedChat(null);
29580
29913
  } else {
@@ -29584,10 +29917,13 @@ function createChatSessionPending({
29584
29917
  },
29585
29918
  reset: () => {
29586
29919
  generation += 1;
29920
+ admission.reset();
29587
29921
  beginSelection();
29588
29922
  if (pendingLaunch?.id)
29589
29923
  getRouteOwner().releaseOpenOperation?.(pendingLaunch.id);
29590
29924
  pendingLaunch = null;
29925
+ for (const operation of pendingOperations.values())
29926
+ operation.active = false;
29591
29927
  pendingOperations.clear();
29592
29928
  },
29593
29929
  selectChat: (chatId, { flushPrevious }) => {
@@ -29600,9 +29936,14 @@ function createChatSessionPending({
29600
29936
  const selectResolved = () => {
29601
29937
  if (!isSelectionCurrent(selection) || getIdentity().chatBanned)
29602
29938
  return false;
29603
- if (pendingLaunch && chatId !== pendingLaunch.id && !pendingOperations.has(pendingLaunch.id)) {
29939
+ if (pendingLaunch && chatId !== pendingLaunch.id) {
29940
+ if (chatId === pendingLaunch.canonicalChatId) {
29941
+ settleMaterialization(pendingLaunch.id, chatId);
29942
+ }
29604
29943
  getRouteOwner().releaseOpenOperation?.(pendingLaunch.id);
29605
29944
  pendingLaunch = null;
29945
+ if (getSelectedChatId() === chatId)
29946
+ publish();
29606
29947
  }
29607
29948
  const previousChatId = getSelectedChatId();
29608
29949
  if (previousChatId && previousChatId !== chatId)
@@ -30119,7 +30460,6 @@ function createChatDelete({
30119
30460
  for (const chatId of chatIds || []) {
30120
30461
  if (!chatId)
30121
30462
  continue;
30122
- dropCachedChat(localCache, chatId);
30123
30463
  closeMessageBatchRef.current?.(chatId);
30124
30464
  if (localForRender.has(chatId)) {
30125
30465
  if (!localChanged)
@@ -30151,6 +30491,7 @@ function createChatDelete({
30151
30491
  keepSelectedDeletedChatIdsRef.current.delete(chatId);
30152
30492
  }
30153
30493
  }
30494
+ dropCachedChats(localCache, ids);
30154
30495
  clearDeletedChatState(ids);
30155
30496
  renderVisibleChats();
30156
30497
  setSelectedChat((current) => current && ids.includes(current) && !options?.keepSelected ? null : current);
@@ -30208,9 +30549,7 @@ function createChatDelete({
30208
30549
  const dropChat = (chatId) => {
30209
30550
  if (!chatId)
30210
30551
  return;
30211
- clearDeletedChatState([chatId]);
30212
30552
  listActionsRef.current?.removeOwner?.(chatId, { warm: false });
30213
- setSelectedChat((current) => current === chatId ? null : current);
30214
30553
  };
30215
30554
  const dropLeftChat = (chatId) => {
30216
30555
  if (!chatId)
@@ -31576,8 +31915,6 @@ async function setOwnChatNotificationMode(cloud, identity, entryId, entry, value
31576
31915
  if (!current?.current || current.retirement)
31577
31916
  throw makeChatUnavailableError();
31578
31917
  const manifest = current.current.manifest;
31579
- if (manifest.lineage !== "group")
31580
- throw new Error("group chat required");
31581
31918
  const requestedMode = cleanText(value);
31582
31919
  const notificationMode = normalizeChatNotificationMode(requestedMode, manifest);
31583
31920
  if (notificationMode !== requestedMode) {
@@ -31637,20 +31974,20 @@ function createChatSessionNotifications({
31637
31974
  throw makeChatUnavailableError();
31638
31975
  }
31639
31976
  };
31640
- const requireOwnerChat = (chatId, operation, { group = false } = {}) => {
31977
+ const requireOwnerChat = (chatId, operation) => {
31641
31978
  assertCurrent(operation);
31642
31979
  const chat = getOwnerChat(chatId);
31643
- if (operation.identity.chatBanned || canWriteChat(chatId) !== true || group && chat?.lineage !== "group" || !chat.ownEntry || chat.ownEntry.retirement || !chat.entryId) {
31980
+ if (operation.identity.chatBanned || canWriteChat(chatId) !== true || !chat?.ownEntry || chat.ownEntry.retirement || !chat.entryId) {
31644
31981
  throw makeChatUnavailableError();
31645
31982
  }
31646
31983
  return chat;
31647
31984
  };
31648
31985
  const setChatNotificationMode = async (chatId, mode) => {
31649
31986
  const operation = operationContext();
31650
- requireOwnerChat(chatId, operation, { group: true });
31987
+ requireOwnerChat(chatId, operation);
31651
31988
  const releaseMutation = await operation.mutationGate.acquireExclusive(chatId);
31652
31989
  try {
31653
- const chat = requireOwnerChat(chatId, operation, { group: true });
31990
+ const chat = requireOwnerChat(chatId, operation);
31654
31991
  const committed = await writeMode(cloud, operation.identity, chat.entryId, chat.ownEntry, mode);
31655
31992
  assertCurrent(operation);
31656
31993
  const projected = projectOwnChatEntry(committed, chat.entryId, operation.identity.chatPK, chat.ts);
@@ -32036,7 +32373,7 @@ function createChatSession({
32036
32373
  const owner = getOwnerChat(chatId);
32037
32374
  if (owner)
32038
32375
  return !isChatMembershipRemoved(owner);
32039
- return pendingOwner?.getPendingLaunch()?.id === chatId;
32376
+ return pendingOwner?.isPendingChat(chatId) === true;
32040
32377
  };
32041
32378
  const requireOnline = () => {
32042
32379
  if (!online)
@@ -32051,10 +32388,6 @@ function createChatSession({
32051
32388
  if (chatId && !isMutableChat(chatId))
32052
32389
  throw makeChatUnavailableError();
32053
32390
  };
32054
- const gateChatMutation = (fn) => (chatId, ...rest) => {
32055
- requireMutableChat(chatId);
32056
- return fn(chatId, ...rest);
32057
- };
32058
32391
  const gateAdmittedChatMutation = (fn) => (chatId, ...rest) => {
32059
32392
  requireMutableChat(chatId);
32060
32393
  return Promise.resolve(pendingOwner.assertChatAdmission(chatId)).then(() => fn(chatId, ...rest));
@@ -32256,7 +32589,8 @@ function createChatSession({
32256
32589
  getPeerProfile,
32257
32590
  resolvePeerProfile,
32258
32591
  setLocalByChat,
32259
- getLocalByChat: () => localByChatRef.current
32592
+ getLocalByChat: () => localByChatRef.current,
32593
+ retireMaterializedChat: (chatId) => actionOwner.deleteActions.beginRetirement(chatId)
32260
32594
  });
32261
32595
  launchOwner = createChatSessionLaunches({
32262
32596
  cloud,
@@ -32296,29 +32630,79 @@ function createChatSession({
32296
32630
  const openDirectChat = (...args) => launchOwner.openDirectChat(...args);
32297
32631
  const openNotesChat = (...args) => launchOwner.openNotesChat(...args);
32298
32632
  const openNewChat = (...args) => pendingOwner.openNewChat(...args);
32299
- const directAdmissionForPeer2 = (profile) => directAdmissionForPeer(chatPrivateKey, chatPK, profile, chatAdmission);
32300
- const canStartDirectChat2 = (profile) => canStartDirectChat(chatPrivateKey, chatPK, profile, chatAdmission);
32633
+ const { directAdmissionForPeer: directAdmissionForPeer2, canStartDirectChat } = pendingOwner;
32301
32634
  const canInviteToGroup2 = (profile) => canInviteToGroup(profile, chatAdmission);
32302
32635
  const createGroupChat2 = (...args) => launchOwner.createGroupChat(...args);
32303
32636
  const selectLocalChat = (...args) => launchOwner.selectLocalChat(...args);
32304
- const addChatMembers = async (chatId, profiles) => {
32637
+ const mutateMaterializedChat = (chatId, mutate, options) => {
32305
32638
  requireMutableChat(chatId);
32306
- await pendingOwner.assertChatAdmission(chatId);
32307
- return membershipOwner.addChatMembers(chatId, profiles);
32639
+ return pendingOwner.runChatMutation(chatId, async (id, context) => {
32640
+ await pendingOwner.assertChatAdmission(id);
32641
+ return mutate(id, context);
32642
+ }, options);
32643
+ };
32644
+ const addChatMembers = (chatId, profiles) => {
32645
+ const additions = Array.isArray(profiles) ? profiles : [profiles];
32646
+ additions.forEach(membershipOwner.profileMember);
32647
+ return mutateMaterializedChat(chatId, (id, context) => context.created ? { entry: getOwnerChat(id).ownEntry } : membershipOwner.addChatMembers(id, additions), {
32648
+ initialProfiles: (current) => {
32649
+ const next = new Map(current.map((profile) => [profile.chatPK, profile]));
32650
+ for (const profile of additions)
32651
+ next.set(profile.chatPK, profile);
32652
+ return next.size === current.length ? current : [...next.values()];
32653
+ }
32654
+ });
32308
32655
  };
32309
- const kickChatMember = async (chatId, memberChatPK) => {
32310
- requireMutableChat(chatId);
32311
- await pendingOwner.assertChatAdmission(chatId);
32312
- return membershipOwner.kickChatMember(chatId, memberChatPK);
32656
+ const kickChatMember = (chatId, memberChatPK) => {
32657
+ const target = cleanText(memberChatPK);
32658
+ if (!target)
32659
+ throw new Error("chat member required");
32660
+ if (target === chatPK && pendingOwner.isPendingChat(chatId)) {
32661
+ return deleteChat(chatId);
32662
+ }
32663
+ return mutateMaterializedChat(chatId, (id, context) => context.created ? { entry: getOwnerChat(id).ownEntry } : membershipOwner.kickChatMember(id, target), {
32664
+ initialProfiles: (current) => current.some((profile) => profile.chatPK === target) ? current.filter((profile) => profile.chatPK !== target) : current
32665
+ });
32313
32666
  };
32314
32667
  const leaveChat = (chatId) => {
32668
+ if (pendingOwner.isPendingChat(chatId)) {
32669
+ return deleteChat(chatId);
32670
+ }
32315
32671
  requireMutableChat(chatId);
32316
32672
  return membershipOwner.leaveChat(chatId);
32317
32673
  };
32318
32674
  const leaveOwnedChat = (...args) => membershipOwner.leaveOwnedChat(...args);
32319
- const updateChatSettings2 = gateAdmittedChatMutation((...args) => settingsOwner.updateChatSettings(...args));
32320
- const updateChatAvatar = gateAdmittedChatMutation((...args) => settingsOwner.updateChatAvatar(...args));
32321
- const setChatNotificationMode = gateAdmittedChatMutation((...args) => notificationsOwner.setChatNotificationMode(...args));
32675
+ const sharedSettingsChat = (chatId) => {
32676
+ const chat = pendingOwner.getActionChat(chatId);
32677
+ if (chat?.lineage === "self")
32678
+ throw new Error("self chat identity follows your profile");
32679
+ return chat;
32680
+ };
32681
+ const updateChatSettings2 = async (chatId, settings) => {
32682
+ const chat = sharedSettingsChat(chatId);
32683
+ const requested = normalizeChatSettingsInput({ ...chat?.settings, ...settings });
32684
+ const changes = ["title", "avatarRef", "retention"].filter((key) => Object.prototype.hasOwnProperty.call(settings, key));
32685
+ const patch = Object.fromEntries(changes.map((key) => [key, requested[key]]));
32686
+ if (chat && !pendingOwner.hasPendingChatOperation(chatId) && changes.every((key) => chat.settings?.[key] === requested[key])) {
32687
+ return Promise.resolve({ unchanged: true });
32688
+ }
32689
+ return mutateMaterializedChat(chatId, (id) => settingsOwner.updateChatSettings(id, patch));
32690
+ };
32691
+ const updateChatAvatar = async (chatId, data) => {
32692
+ assertChatAvatarBytes(data);
32693
+ const chat = sharedSettingsChat(chatId);
32694
+ if (chat?.lineage !== "group")
32695
+ throw new Error("group chat required");
32696
+ return mutateMaterializedChat(chatId, (id) => settingsOwner.updateChatAvatar(id, data));
32697
+ };
32698
+ const setChatNotificationMode = async (chatId, mode) => {
32699
+ const chat = pendingOwner.getActionChat(chatId);
32700
+ if (chat && normalizeChatNotificationMode(mode, chat) !== mode)
32701
+ throw new Error("all notifications are unavailable in large groups");
32702
+ if (chat && !pendingOwner.hasPendingChatOperation(chatId) && (chat.notificationMode || defaultChatNotificationMode(chat)) === mode)
32703
+ return mode;
32704
+ return mutateMaterializedChat(chatId, (id) => notificationsOwner.setChatNotificationMode(id, mode));
32705
+ };
32322
32706
  const updateMessage = async (chatId, original, changes) => {
32323
32707
  if (chatBanned)
32324
32708
  throw makeChatUnavailableError();
@@ -32472,7 +32856,27 @@ function createChatSession({
32472
32856
  };
32473
32857
  const dropChat = (...args) => actionOwner.deleteActions.dropChat(...args);
32474
32858
  const dropUnavailableChat = (...args) => actionOwner.deleteActions.dropUnavailableChat(...args);
32475
- const deleteChat = gateOnlineMutation((...args) => actionOwner.deleteActions.deleteChat(...args));
32859
+ const deleteChat = async (value, options) => {
32860
+ const inputs = Array.isArray(value) ? value : [value];
32861
+ const durable = [];
32862
+ let discarded = 0;
32863
+ for (const input of inputs) {
32864
+ const id = cleanText(typeof input === "string" ? input : input?.chatId || input?.id);
32865
+ if (pendingOwner.hasPendingChatOperation(id) || !getOwnerChat(id) && pendingOwner.isPendingChat(id)) {
32866
+ const result = await pendingOwner.discardChat(id);
32867
+ if (result.chat)
32868
+ durable.push(result.chat);
32869
+ else if (result.discarded)
32870
+ discarded += 1;
32871
+ } else
32872
+ durable.push(input);
32873
+ }
32874
+ if (!durable.length)
32875
+ return Array.isArray(value) ? discarded : discarded > 0;
32876
+ requireOnline();
32877
+ const deleted = await actionOwner.deleteActions.deleteChat(Array.isArray(value) ? durable : durable[0], options);
32878
+ return Array.isArray(value) ? discarded + Number(deleted) : deleted;
32879
+ };
32476
32880
  const purgePeerChats = async (affected, operation) => {
32477
32881
  const identity = operation.identity;
32478
32882
  for (const chat of affected) {
@@ -32577,14 +32981,14 @@ function createChatSession({
32577
32981
  const sendPendingChatMessage = (...args) => pendingOwner.sendPendingChatMessage(...args);
32578
32982
  const sendChatMessage = sendMessage;
32579
32983
  const sendDirectAttachment = gateOnlineMutation((...args) => pendingOwner.sendDirectAttachment(...args));
32580
- const sendChatAttachment = gateChatMutation((...args) => pendingOwner.sendChatAttachment(...args));
32984
+ const sendChatAttachment = gateOnlineMutation((...args) => pendingOwner.sendChatAttachment(...args));
32581
32985
  const retryMessage = async (...args) => {
32582
32986
  const result = await pendingOwner.retryMessage(...args);
32583
32987
  if (online)
32584
32988
  drainMembershipOutbox();
32585
32989
  return result;
32586
32990
  };
32587
- const sendAttachment = gateChatMutation((...args) => pendingOwner.sendChatAttachment(...args));
32991
+ const sendAttachment = gateOnlineMutation((...args) => pendingOwner.sendChatAttachment(...args));
32588
32992
  const sendChatImage = (chatId, image, options = {}) => sendChatAttachment(chatId, { ...image, type: image?.type || "img" }, options);
32589
32993
  const sendAttachmentMany = gateOnlineMutation((...args) => pendingOwner.sendAttachmentMany(...args));
32590
32994
  const sendImageMany = (targets, image, options = {}) => sendAttachmentMany(targets, { ...image, type: image?.type || "img" }, options);
@@ -32642,7 +33046,11 @@ function createChatSession({
32642
33046
  });
32643
33047
  return state;
32644
33048
  };
32645
- const setChatTtl = gateAdmittedChatMutation((...args) => settingsOwner.setChatTtl(...args));
33049
+ const setChatTtl = async (chatId, retention) => {
33050
+ const next = cleanChatRetention(retention);
33051
+ await updateChatSettings2(chatId, { retention: next });
33052
+ return next;
33053
+ };
32646
33054
  const makeMessagePermanent = gateAdmittedChatMutation((...args) => actionOwner.saveActions.makeMessagePermanent(...args));
32647
33055
  const makeMessageTemporary = gateAdmittedChatMutation((...args) => actionOwner.saveActions.makeMessageTemporary(...args));
32648
33056
  const readMessageFile = (...args) => actionOwner.saveActions.readMessageFile(...args);
@@ -32745,6 +33153,17 @@ function createChatSession({
32745
33153
  chatsRef,
32746
33154
  reconcileMessageBatches: (...args) => messageBatchOwner.reconcileChats(...args),
32747
33155
  warmChats: (...args) => messageBatchOwner.warm(...args),
33156
+ onOwnerRemoved: (chatIds) => {
33157
+ for (const chatId of chatIds)
33158
+ pendingOwner.dropChat(chatId);
33159
+ actionOwner.deleteActions.clearDeletedChatState(chatIds);
33160
+ for (const chatId of chatIds) {
33161
+ actionOwner.liveActions.closeChatActivity(chatId);
33162
+ historyOwner.clear(chatId);
33163
+ messageBatchOwner.routeMemory.dropChat(chatId);
33164
+ routeMemoryScopes.delete(chatId);
33165
+ }
33166
+ },
32748
33167
  onTransition: stageCommittedMembershipMessage,
32749
33168
  onRemoved: (removedChatId, _epochVersion, retiredEntry) => {
32750
33169
  const applied = chatListOwner.applyMembershipRemoval(removedChatId, retiredEntry);
@@ -32801,7 +33220,7 @@ function createChatSession({
32801
33220
  openNotesChat,
32802
33221
  openNewChat,
32803
33222
  directAdmissionForPeer: directAdmissionForPeer2,
32804
- canStartDirectChat: canStartDirectChat2,
33223
+ canStartDirectChat,
32805
33224
  canInviteToGroup: canInviteToGroup2,
32806
33225
  createGroupChat: createGroupChat2,
32807
33226
  addChatMembers,
@@ -33247,7 +33666,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
33247
33666
  let expiryTimer = null;
33248
33667
  let command = null;
33249
33668
  let authenticating = null;
33250
- let snapshot = Object.freeze({ visibility: null, ready: false, pending: false, error: null, ...UNKNOWN_PRESENCE });
33669
+ let snapshot = Object.freeze({ visibility: null, ready: false, pending: false, pendingVisibility: null, error: null, ...UNKNOWN_PRESENCE });
33251
33670
  function topic() {
33252
33671
  return sources.uid ? derivePresenceTopic(realm2, sources.uid) : null;
33253
33672
  }
@@ -33255,7 +33674,13 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
33255
33674
  return Boolean(channel && challenge && policy && sources.enabled && !closed);
33256
33675
  }
33257
33676
  function publish(patch = {}) {
33258
- const next = { ...snapshot, ...patch, ready: isReady() };
33677
+ const next = {
33678
+ ...snapshot,
33679
+ ...patch,
33680
+ ready: isReady(),
33681
+ pending: Boolean(command),
33682
+ pendingVisibility: command?.policy.visibility ?? null
33683
+ };
33259
33684
  if (Object.keys(next).every((key) => Object.is(next[key], snapshot[key])))
33260
33685
  return;
33261
33686
  snapshot = Object.freeze(next);
@@ -33311,7 +33736,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
33311
33736
  const pending = command;
33312
33737
  command = null;
33313
33738
  clearTimeout(pending.timer);
33314
- publish({ pending: false, error });
33739
+ publish({ error });
33315
33740
  if (error)
33316
33741
  pending.reject(error);
33317
33742
  else
@@ -33551,12 +33976,14 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
33551
33976
  listener();
33552
33977
  }
33553
33978
  function update(next) {
33554
- const identityChanged = sources.uid !== next.uid || sources.identity?.publicKey !== next.identity?.publicKey;
33979
+ const accountChanged = sources.uid !== next.uid;
33980
+ const identityChanged = accountChanged || sources.identity?.publicKey !== next.identity?.publicKey;
33555
33981
  if (identityChanged) {
33556
33982
  stop();
33557
33983
  policy = null;
33558
33984
  records.clear();
33559
- peers.clear();
33985
+ if (accountChanged)
33986
+ peers.clear();
33560
33987
  publish({ visibility: null, error: null, ...UNKNOWN_PRESENCE });
33561
33988
  }
33562
33989
  sources = next;
@@ -33586,7 +34013,7 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
33586
34013
  reject,
33587
34014
  timer: setTimeout(() => finishCommand(new Error("could not confirm activity setting; reconnect to check")), COMMAND_TIMEOUT_MS)
33588
34015
  };
33589
- publish({ pending: true, error: null });
34016
+ publish({ error: null });
33590
34017
  if (!channel.send(presenceShard(topic()), { type: "policy", expectedRevision: policy.revision, policy: nextPolicy })) {
33591
34018
  finishCommand(new Error("could not update activity while offline"));
33592
34019
  }
@@ -36938,6 +37365,7 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
36938
37365
  let profilesLoading = false;
36939
37366
  let hydratedCacheKey = "";
36940
37367
  let avatarHydrateKey = "";
37368
+ let avatarPublishSession = null;
36941
37369
  let profilesReady = false;
36942
37370
  let blockedPeersReady = false;
36943
37371
  let discoveredPeerUids = new Set;
@@ -37348,12 +37776,21 @@ function createPeerList({ peerApi, presence, diag, onMissingPeers }) {
37348
37776
  const session = lifecycle;
37349
37777
  const startedAt = Date.now();
37350
37778
  const requested = walletPKs.length + chatPeerPKs.length;
37351
- hydrateCachedAvatars(walletPKs, chatPeerPKs).then((changed) => {
37779
+ hydrateCachedAvatars(walletPKs, chatPeerPKs, () => {
37780
+ if (!isCurrent(session) || avatarPublishSession === session)
37781
+ return;
37782
+ avatarPublishSession = session;
37783
+ queueMicrotask(() => {
37784
+ if (avatarPublishSession !== session)
37785
+ return;
37786
+ avatarPublishSession = null;
37787
+ if (isCurrent(session))
37788
+ rebuild({ base: true, persist: false });
37789
+ });
37790
+ }).then((changed) => {
37352
37791
  if (!isCurrent(session))
37353
37792
  return;
37354
37793
  markDiag(diag, "peers.avatars.hydrate", { requested, changed, elapsedMs: Date.now() - startedAt });
37355
- if (changed)
37356
- rebuild({ base: true });
37357
37794
  }).catch((error) => {
37358
37795
  if (!isCurrent(session))
37359
37796
  return;
@@ -37933,7 +38370,7 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
37933
38370
  function uniqueLookupKeys(keys) {
37934
38371
  return uniqueValues(keys);
37935
38372
  }
37936
- async function hydrateCachedAvatars(walletPKs, chatPKs) {
38373
+ async function hydrateCachedAvatars(walletPKs, chatPKs, onHydrated) {
37937
38374
  if (typeof avatarCache?.read !== "function")
37938
38375
  return false;
37939
38376
  const profiles = new Map;
@@ -37952,21 +38389,18 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
37952
38389
  queue(key, "chat");
37953
38390
  if (!profiles.size)
37954
38391
  return false;
37955
- const cached = await Promise.all(Array.from(profiles.values()).map(async ({ uid, version }) => ({
37956
- avatar: await readCachedAvatar(uid, version),
37957
- uid,
37958
- version
37959
- })));
37960
38392
  let changed = false;
37961
- for (const entry of cached) {
37962
- if (!entry.avatar)
37963
- continue;
37964
- const current = profileCache.get(entry.uid);
37965
- if (!current || current.avatarVersion !== entry.version || current.avatar === entry.avatar)
37966
- continue;
37967
- storeProfile({ ...current, avatar: entry.avatar });
38393
+ await Promise.all(Array.from(profiles.values()).map(async ({ uid, version }) => {
38394
+ const avatar = await readCachedAvatar(uid, version);
38395
+ if (!avatar)
38396
+ return;
38397
+ const current = profileCache.get(uid);
38398
+ if (!current || current.avatarVersion !== version || current.avatar === avatar)
38399
+ return;
38400
+ storeProfile({ ...current, avatar });
37968
38401
  changed = true;
37969
- }
38402
+ onHydrated?.();
38403
+ }));
37970
38404
  return changed;
37971
38405
  }
37972
38406
  function needsAvatarResolve(profile) {
@@ -40556,6 +40990,7 @@ function createPayment({
40556
40990
  throw new Error("bitcoin payment must be reviewed before sending");
40557
40991
  }
40558
40992
  const payment = paymentInput?.state === PAYMENT_STATE_PREPARED ? normalizePreparedPayment(paymentInput, wallet, network) : await preparePayment(paymentInput);
40993
+ assertReady();
40559
40994
  const startedAt = Date.now();
40560
40995
  const diagKind = payment.kind;
40561
40996
  markDiag(diag, "wallet.payment.start", { kind: diagKind });