@glyphteck/veyl 0.60.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11129,6 +11129,20 @@ function normalizeCurrentEpoch(value) {
11129
11129
  settings: normalizeChatSettingsProjection(value.settings)
11130
11130
  };
11131
11131
  }
11132
+ function normalizeOwnerRetirement(value, current) {
11133
+ if (value == null)
11134
+ return null;
11135
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11136
+ throw new Error("invalid chat owner retirement");
11137
+ }
11138
+ const kind = cleanText(value.kind);
11139
+ const epochVersion = value.epochVersion;
11140
+ if (!Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion) || epochVersion < current.manifest.epochVersion) {
11141
+ throw new Error("invalid chat owner retirement");
11142
+ }
11143
+ const proposalId = kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? cleanTransitionMessageId(value.proposalId) : null;
11144
+ return { kind, epochVersion, proposalId };
11145
+ }
11132
11146
  function normalizeOwnerEntry(value) {
11133
11147
  if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
11134
11148
  throw new Error("invalid chat entry");
@@ -11149,7 +11163,8 @@ function normalizeOwnerEntry(value) {
11149
11163
  saved: value.saved || null,
11150
11164
  startMs: Number.isFinite(value.startMs) ? value.startMs : null,
11151
11165
  deliveryRegistered: value.deliveryRegistered === true,
11152
- notificationTag: cleanText(value.notificationTag) || null
11166
+ notificationTag: cleanText(value.notificationTag) || null,
11167
+ retirement: normalizeOwnerRetirement(value.retirement, current)
11153
11168
  };
11154
11169
  }
11155
11170
  async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
@@ -11190,7 +11205,8 @@ function makeOwnChatEntry(epoch, fields = {}) {
11190
11205
  saved: fields.saved || null,
11191
11206
  startMs: Number.isFinite(fields.startMs) ? fields.startMs : null,
11192
11207
  deliveryRegistered: fields.deliveryRegistered === true,
11193
- notificationTag: cleanText(fields.notificationTag) || null
11208
+ notificationTag: cleanText(fields.notificationTag) || null,
11209
+ retirement: fields.retirement || null
11194
11210
  });
11195
11211
  }
11196
11212
  function normalizeOwnerEpochRecord(value) {
@@ -11198,6 +11214,7 @@ function normalizeOwnerEpochRecord(value) {
11198
11214
  throw new Error("invalid owner epoch entry");
11199
11215
  }
11200
11216
  const manifest = normalizeEpochManifest(value.manifest);
11217
+ const leaveProposalId = value.leaveProposalId == null ? null : cleanTransitionMessageId(value.leaveProposalId);
11201
11218
  return {
11202
11219
  v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
11203
11220
  protocol: CHAT_PROTOCOL_VERSION,
@@ -11207,8 +11224,9 @@ function normalizeOwnerEpochRecord(value) {
11207
11224
  manifest,
11208
11225
  epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
11209
11226
  cutoffMs: Number.isFinite(value.cutoffMs) ? value.cutoffMs : null,
11210
- successorTransitionCommitment: cleanTransitionCommitment(value.successorTransitionCommitment),
11211
- successorTransitionMessageId: cleanTransitionMessageId(value.successorTransitionMessageId)
11227
+ successorTransitionCommitment: leaveProposalId ? null : cleanTransitionCommitment(value.successorTransitionCommitment),
11228
+ successorTransitionMessageId: leaveProposalId ? null : cleanTransitionMessageId(value.successorTransitionMessageId),
11229
+ leaveProposalId
11212
11230
  };
11213
11231
  }
11214
11232
  function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
@@ -11219,7 +11237,8 @@ function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
11219
11237
  epochSecret: secretHex(epochSecret, "epoch secret"),
11220
11238
  cutoffMs: Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : null,
11221
11239
  successorTransitionCommitment: fields.successorTransitionCommitment,
11222
- successorTransitionMessageId: fields.successorTransitionMessageId
11240
+ successorTransitionMessageId: fields.successorTransitionMessageId,
11241
+ leaveProposalId: fields.leaveProposalId
11223
11242
  });
11224
11243
  }
11225
11244
  async function sealOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, record) {
@@ -11243,7 +11262,7 @@ async function openOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, body)
11243
11262
  function ownEpochEntryId(chatPrivateKey, chatId, epochId) {
11244
11263
  return ownerEpochEntryId(chatPrivateKey, chatId, epochId);
11245
11264
  }
11246
- var CHAT_ENTRY_VERSION = 4, CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
11265
+ var CHAT_ENTRY_VERSION = 4, CHAT_OWNER_EPOCH_ENTRY_VERSION = 2, CHAT_OWNER_RETIREMENT_KINDS;
11247
11266
  var init_entry = __esm(() => {
11248
11267
  init_canonical();
11249
11268
  init_core();
@@ -11255,6 +11274,10 @@ var init_entry = __esm(() => {
11255
11274
  init_state2();
11256
11275
  init_settings2();
11257
11276
  "use client";
11277
+ CHAT_OWNER_RETIREMENT_KINDS = Object.freeze({
11278
+ LEAVE: "leave",
11279
+ REMOVED: "removed"
11280
+ });
11258
11281
  });
11259
11282
 
11260
11283
  // ../../core/chat/epochs/bootstrap.js
@@ -11395,6 +11418,32 @@ async function mutateOwnChatEntry(cloud, identity, entryId, current, update) {
11395
11418
  const committed = await cloud.user.chats.mutate(identity.uid, entryId, mutation);
11396
11419
  return committed?.result ?? mutation.result ?? null;
11397
11420
  }
11421
+ async function retireOwnChatEntry(cloud, identity, entryId, current, fields = {}) {
11422
+ const expectedEpochVersion = fields.expectedEpochVersion;
11423
+ const retirement = fields.retirement;
11424
+ if (!Number.isSafeInteger(expectedEpochVersion) || expectedEpochVersion < 1 || !retirement?.kind || !Number.isSafeInteger(retirement.epochVersion)) {
11425
+ throw new Error("chat owner retirement required");
11426
+ }
11427
+ return mutateOwnChatEntry(cloud, identity, entryId, current, (latest) => {
11428
+ if (latest?.retirement?.kind === retirement.kind && latest.retirement.epochVersion >= retirement.epochVersion) {
11429
+ return { result: latest };
11430
+ }
11431
+ if (latest?.retirement || latest?.current?.manifest?.epochVersion !== expectedEpochVersion) {
11432
+ throw new Error("chat owner epoch changed");
11433
+ }
11434
+ return {
11435
+ entry: {
11436
+ ...latest,
11437
+ routes: {},
11438
+ deliveryRegistered: false,
11439
+ notificationTag: null,
11440
+ retirement
11441
+ },
11442
+ epochs: fields.epoch ? [fields.epoch] : [],
11443
+ mlsDeletes: fields.mlsStateId ? [fields.mlsStateId] : []
11444
+ };
11445
+ });
11446
+ }
11398
11447
  async function openOwnChatMutationEntry(identity, entryId, record) {
11399
11448
  if (!record)
11400
11449
  return null;
@@ -12117,48 +12166,6 @@ function cleanFrontier(value) {
12117
12166
  throw new Error("chat receipt checkpoint frontier time required");
12118
12167
  return Object.freeze({ id, at: value.at, seenAt: value.seenAt });
12119
12168
  }
12120
- function maskBytes(memberCount2) {
12121
- return Math.ceil(memberCount2 / 8);
12122
- }
12123
- function cleanMask(value, memberCount2, label) {
12124
- const mask = cleanText(value).toLowerCase();
12125
- const size = maskBytes(memberCount2);
12126
- if (!new RegExp(`^[0-9a-f]{${size * 2}}$`, "u").test(mask)) {
12127
- throw new Error(`${label} invalid`);
12128
- }
12129
- const unused = size * 8 - memberCount2;
12130
- if (unused > 0 && Number.parseInt(mask.slice(-2), 16) & (1 << unused) - 1 << 8 - unused) {
12131
- throw new Error(`${label} overflow`);
12132
- }
12133
- return mask;
12134
- }
12135
- function makeMask(indices, memberCount2) {
12136
- const bytes = new Uint8Array(maskBytes(memberCount2));
12137
- for (const index of indices)
12138
- bytes[Math.floor(index / 8)] |= 1 << index % 8;
12139
- return [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
12140
- }
12141
- function maskHas(mask, index) {
12142
- const offset = Math.floor(index / 8) * 2;
12143
- return (Number.parseInt(mask.slice(offset, offset + 2), 16) & 1 << index % 8) !== 0;
12144
- }
12145
- function maskCount(mask, memberCount2) {
12146
- let count = 0;
12147
- for (let index = 0;index < memberCount2; index += 1) {
12148
- if (maskHas(mask, index))
12149
- count += 1;
12150
- }
12151
- return count;
12152
- }
12153
- function frontierWithSeenAt(frontier, states) {
12154
- if (!frontier)
12155
- return null;
12156
- return {
12157
- id: frontier.id,
12158
- at: frontier.at,
12159
- seenAt: Math.max(frontier.seenAt, ...states.map((state) => Number(state?.readFrontier?.seenAt) || 0))
12160
- };
12161
- }
12162
12169
  function normalizeChatReceiptProjection(value) {
12163
12170
  if (!value || typeof value !== "object" || Array.isArray(value)) {
12164
12171
  throw new Error("chat receipt checkpoint projection required");
@@ -12167,41 +12174,13 @@ function normalizeChatReceiptProjection(value) {
12167
12174
  if (!Array.isArray(value.revisions) || value.revisions.length !== memberCount2) {
12168
12175
  throw new Error("chat receipt checkpoint revisions invalid");
12169
12176
  }
12170
- const lowFrontier = cleanFrontier(value.lowFrontier);
12171
- const nextLowFrontier = cleanFrontier(value.nextLowFrontier);
12172
- const highFrontier = cleanFrontier(value.highFrontier);
12173
- const nextHighFrontier = cleanFrontier(value.nextHighFrontier);
12174
- const lowMask = cleanMask(value.lowMask, memberCount2, "chat receipt checkpoint low mask");
12175
- const highMask = cleanMask(value.highMask, memberCount2, "chat receipt checkpoint high mask");
12176
- const nextHighMask = cleanMask(value.nextHighMask, memberCount2, "chat receipt checkpoint next high mask");
12177
- if (!highFrontier && maskCount(highMask, memberCount2) !== 0) {
12178
- throw new Error("chat receipt checkpoint high frontier missing");
12179
- }
12180
- if (highFrontier && maskCount(highMask, memberCount2) === 0) {
12181
- throw new Error("chat receipt checkpoint high readers missing");
12182
- }
12183
- if (!nextHighFrontier && maskCount(nextHighMask, memberCount2) !== 0) {
12184
- throw new Error("chat receipt checkpoint next high frontier missing");
12185
- }
12186
- if (nextHighFrontier && maskCount(nextHighMask, memberCount2) === 0) {
12187
- throw new Error("chat receipt checkpoint next high readers missing");
12188
- }
12189
- if (lowFrontier && compareReadFrontiers(lowFrontier, highFrontier) > 0) {
12190
- throw new Error("chat receipt checkpoint frontier order invalid");
12191
- }
12192
- if (nextLowFrontier && compareReadFrontiers(nextLowFrontier, highFrontier) > 0) {
12193
- throw new Error("chat receipt checkpoint next frontier order invalid");
12177
+ if (!Array.isArray(value.frontiers) || value.frontiers.length !== memberCount2) {
12178
+ throw new Error("chat receipt checkpoint frontiers invalid");
12194
12179
  }
12195
12180
  return Object.freeze({
12196
12181
  memberCount: memberCount2,
12197
12182
  revisions: Object.freeze(value.revisions.map(cleanRevision3)),
12198
- lowFrontier,
12199
- lowMask,
12200
- nextLowFrontier,
12201
- highFrontier,
12202
- highMask,
12203
- nextHighFrontier,
12204
- nextHighMask
12183
+ frontiers: Object.freeze(value.frontiers.map(cleanFrontier))
12205
12184
  });
12206
12185
  }
12207
12186
  function buildChatReceiptProjection(manifest, states) {
@@ -12209,36 +12188,66 @@ function buildChatReceiptProjection(manifest, states) {
12209
12188
  const memberCount2 = cleanCount(members.length);
12210
12189
  const values = members.map((member) => states?.get?.(member.chatPK) || null);
12211
12190
  const revisions = values.map((state) => Math.max(0, Number(state?.revision) || 0));
12212
- const missing = [];
12213
- const present = [];
12214
- values.forEach((state, index) => {
12215
- if (state?.readFrontier)
12216
- present.push({ index, state, frontier: state.readFrontier });
12217
- else
12218
- missing.push(index);
12219
- });
12220
- present.sort((left, right) => compareReadFrontiers(left.frontier, right.frontier));
12221
- const minimum = present[0]?.frontier || null;
12222
- const maximum = present.at(-1)?.frontier || null;
12223
- const lowIndices = missing.length ? missing : present.filter((item) => compareReadFrontiers(item.frontier, minimum) === 0).map((item) => item.index);
12224
- const nextLowItems = missing.length ? present : present.filter((item) => compareReadFrontiers(item.frontier, minimum) > 0);
12225
- const nextLow = nextLowItems[0]?.frontier || null;
12226
- const highItems = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) === 0) : [];
12227
- const belowHigh = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) < 0) : [];
12228
- const nextHigh = belowHigh.at(-1)?.frontier || null;
12229
- const nextHighItems = nextHigh ? belowHigh.filter((item) => compareReadFrontiers(item.frontier, nextHigh) === 0) : [];
12230
12191
  return normalizeChatReceiptProjection({
12231
12192
  memberCount: memberCount2,
12232
12193
  revisions,
12233
- lowFrontier: missing.length ? null : frontierWithSeenAt(minimum, values.filter(Boolean)),
12234
- lowMask: makeMask(lowIndices, memberCount2),
12235
- nextLowFrontier: frontierWithSeenAt(nextLow, nextLowItems.map((item) => item.state)),
12236
- highFrontier: frontierWithSeenAt(maximum, highItems.map((item) => item.state)),
12237
- highMask: makeMask(highItems.map((item) => item.index), memberCount2),
12238
- nextHighFrontier: frontierWithSeenAt(nextHigh, nextHighItems.map((item) => item.state)),
12239
- nextHighMask: makeMask(nextHighItems.map((item) => item.index), memberCount2)
12194
+ frontiers: values.map((state) => state?.readFrontier || null)
12240
12195
  });
12241
12196
  }
12197
+ function mergeChatReceiptProjections(current, incoming) {
12198
+ if (!current)
12199
+ return normalizeChatReceiptProjection(incoming);
12200
+ if (!incoming)
12201
+ return normalizeChatReceiptProjection(current);
12202
+ const left = normalizeChatReceiptProjection(current);
12203
+ const right = normalizeChatReceiptProjection(incoming);
12204
+ if (left.memberCount !== right.memberCount) {
12205
+ throw new Error("chat receipt checkpoint member count mismatch");
12206
+ }
12207
+ return normalizeChatReceiptProjection({
12208
+ memberCount: left.memberCount,
12209
+ revisions: left.revisions.map((revision, index) => Math.max(revision, right.revisions[index])),
12210
+ frontiers: left.frontiers.map((frontier, index) => {
12211
+ const other = right.frontiers[index];
12212
+ const comparison = compareReadFrontiers(frontier, other);
12213
+ if (comparison > 0)
12214
+ return frontier;
12215
+ if (comparison < 0)
12216
+ return other;
12217
+ if (!frontier)
12218
+ return null;
12219
+ return {
12220
+ ...frontier,
12221
+ seenAt: Math.max(frontier.seenAt, other.seenAt)
12222
+ };
12223
+ })
12224
+ });
12225
+ }
12226
+ function mergeChatReceiptCheckpointsByEpoch(current, incoming) {
12227
+ if (!(incoming instanceof Map) || !incoming.size) {
12228
+ return current instanceof Map ? current : new Map;
12229
+ }
12230
+ if (!(current instanceof Map) || !current.size)
12231
+ return new Map(incoming);
12232
+ const merged = new Map(current instanceof Map ? current : []);
12233
+ let changed = false;
12234
+ for (const [epochId, value] of incoming instanceof Map ? incoming : []) {
12235
+ const existing = merged.get(epochId);
12236
+ if (existing === value)
12237
+ continue;
12238
+ const exact = isChatReceiptCheckpoint(existing) && isChatReceiptCheckpoint(value) && existing.epochId === value.epochId && existing.memberChatPKs.length === value.memberChatPKs.length && existing.memberChatPKs.every((chatPK, index) => chatPK === value.memberChatPKs[index]);
12239
+ const next = exact ? Object.freeze({
12240
+ ...value,
12241
+ publishedAt: Math.max(Number(existing.publishedAt) || 0, Number(value.publishedAt) || 0) || null,
12242
+ projection: mergeChatReceiptProjections(existing.projection, value.projection)
12243
+ }) : value;
12244
+ if (exact && sameChatReceiptCheckpoint(existing, next) && existing.publishedAt === next.publishedAt)
12245
+ continue;
12246
+ merged.set(epochId, next);
12247
+ changed = true;
12248
+ }
12249
+ return changed ? merged : current;
12250
+ }
12242
12251
  function projectChatReceiptCheckpoint(manifest, value) {
12243
12252
  const projection = normalizeChatReceiptProjection(value?.projection || value);
12244
12253
  if (projection.memberCount !== manifest?.members?.length) {
@@ -12271,13 +12280,14 @@ function chatReceiptCheckpointCoversState(checkpoint, manifest, state) {
12271
12280
  function chatReceiptAllSeenFrontier(checkpoint, authorChatPK) {
12272
12281
  if (!isChatReceiptCheckpoint(checkpoint))
12273
12282
  return null;
12274
- const { lowFrontier, lowMask, nextLowFrontier, memberCount: memberCount2 } = checkpoint.projection;
12275
- const authorIndex = checkpoint.memberChatPKs?.indexOf?.(authorChatPK) ?? -1;
12276
- const lowCount = maskCount(lowMask, memberCount2);
12277
- if (authorIndex >= 0 && lowCount === 1 && maskHas(lowMask, authorIndex)) {
12278
- return nextLowFrontier;
12279
- }
12280
- return lowFrontier;
12283
+ const readers = checkpoint.projection.frontiers.filter((_frontier, index) => checkpoint.memberChatPKs[index] !== authorChatPK);
12284
+ if (!readers.length || readers.some((frontier) => !frontier))
12285
+ return null;
12286
+ const minimum = readers.reduce((current, frontier) => !current || compareReadFrontiers(frontier, current) < 0 ? frontier : current, null);
12287
+ return Object.freeze({
12288
+ ...minimum,
12289
+ seenAt: Math.max(...readers.map((frontier) => frontier.seenAt))
12290
+ });
12281
12291
  }
12282
12292
  function chatReceiptProjectionFingerprint(value) {
12283
12293
  return JSON.stringify(normalizeChatReceiptProjection(value));
@@ -12636,7 +12646,7 @@ function getLatestLocalReadTarget(messages, chatPK) {
12636
12646
  function getLatestReadStateTarget(messages) {
12637
12647
  for (let i = (messages?.length || 0) - 1;i >= 0; i -= 1) {
12638
12648
  const message = messages[i];
12639
- if (isServerConfirmedMsg(message) && canShowMsg(message) && !isSystemMsg(message))
12649
+ if (isServerConfirmedMsg(message) && !isSystemMsg(message) && (canShowMsg(message) || isReactionMsg(message)))
12640
12650
  return message;
12641
12651
  }
12642
12652
  return null;
@@ -14786,10 +14796,6 @@ async function prepareOwnerChatMlsSnapshot(identity, entryId, epochState, snapsh
14786
14796
  function ownerChatMlsSnapshotId(identity, entryId, epochState) {
14787
14797
  return stateAddress(identity, entryId, epochState).stateId;
14788
14798
  }
14789
- async function discardOwnerChatMlsSnapshot(cloud, identity, entryId, epochState) {
14790
- const address = stateAddress(identity, entryId, epochState);
14791
- await cloud.user.chats.mls.delete(identity.uid, entryId, address.stateId).catch(() => false);
14792
- }
14793
14799
  var init_owner2 = __esm(() => {
14794
14800
  init_state3();
14795
14801
  "use client";
@@ -16595,7 +16601,45 @@ function reviveMarker(marker) {
16595
16601
  function serializeKeys(keys) {
16596
16602
  return [...keySet(keys)].filter(Boolean).sort();
16597
16603
  }
16598
- function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
16604
+ function cleanReceiptCheckpoint(value) {
16605
+ if (!isChatReceiptCheckpoint(value))
16606
+ return null;
16607
+ const epochId = cleanText(value.epochId);
16608
+ const memberChatPKs = (value.memberChatPKs || []).map(cleanText).filter(Boolean);
16609
+ if (!epochId || memberChatPKs.length !== value.memberChatPKs?.length)
16610
+ return null;
16611
+ try {
16612
+ return projectChatReceiptCheckpoint({
16613
+ epochId,
16614
+ members: memberChatPKs.map((chatPK) => ({ chatPK }))
16615
+ }, value);
16616
+ } catch {
16617
+ return null;
16618
+ }
16619
+ }
16620
+ function serializeReceiptCheckpoints(statesByEpoch, epochIds) {
16621
+ const allowed = new Set(epochIds || []);
16622
+ const checkpoints = [];
16623
+ for (const [epochId, value] of statesByEpoch instanceof Map ? statesByEpoch : []) {
16624
+ if (allowed.size && !allowed.has(epochId))
16625
+ continue;
16626
+ const checkpoint = cleanReceiptCheckpoint(value);
16627
+ if (checkpoint)
16628
+ checkpoints.push(checkpoint);
16629
+ }
16630
+ return checkpoints.sort((left, right) => left.epochId.localeCompare(right.epochId));
16631
+ }
16632
+ function reviveReceiptCheckpoints(values) {
16633
+ return new Map((Array.isArray(values) ? values : []).map((value) => cleanReceiptCheckpoint(value)).filter(Boolean).map((checkpoint) => [checkpoint.epochId, checkpoint]));
16634
+ }
16635
+ function receiptEpochIds(messages, fallbackEpochId = "") {
16636
+ const ids = new Set((messages || []).map((message) => cleanText(message?.epochId)).filter(Boolean));
16637
+ const fallback = cleanText(fallbackEpochId);
16638
+ if (fallback)
16639
+ ids.add(fallback);
16640
+ return ids;
16641
+ }
16642
+ function serializeBatch(batch, { expiredKeys, deletedKeys, epochIds } = {}) {
16599
16643
  if (!batch && !expiredKeys?.size && !deletedKeys?.size) {
16600
16644
  return null;
16601
16645
  }
@@ -16605,14 +16649,16 @@ function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
16605
16649
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
16606
16650
  ...Number.isFinite(batch?.firstMs) ? { firstMs: batch.firstMs } : {},
16607
16651
  ...Number.isFinite(batch?.lastMs) ? { lastMs: batch.lastMs } : {},
16652
+ receiptCheckpoints: serializeReceiptCheckpoints(batch?.memberStatesByEpoch, epochIds),
16608
16653
  expiredKeys: serializeKeys(expiredKeys || batch?.expiredKeys),
16609
16654
  deletedKeys: serializeKeys(deletedKeys || batch?.deletedKeys)
16610
16655
  };
16611
16656
  }
16612
- function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
16657
+ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback, receiptMessages = latestMessages) {
16613
16658
  const source = latestMessages || [];
16659
+ const epochIds = receiptEpochIds(receiptMessages, fallback?.epochId);
16614
16660
  if (!source.length) {
16615
- return serializeBatch(fallback, { expiredKeys, deletedKeys });
16661
+ return serializeBatch(fallback, { expiredKeys, deletedKeys, epochIds });
16616
16662
  }
16617
16663
  const firstMs = getMessageOrderMs(source[0]);
16618
16664
  const lastMs = getMessageOrderMs(source[source.length - 1]);
@@ -16620,8 +16666,9 @@ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
16620
16666
  firstMs,
16621
16667
  lastMs,
16622
16668
  epochId: fallback?.epochId,
16623
- epochVersion: fallback?.epochVersion
16624
- } : fallback, { expiredKeys, deletedKeys });
16669
+ epochVersion: fallback?.epochVersion,
16670
+ memberStatesByEpoch: fallback?.memberStatesByEpoch
16671
+ } : fallback, { expiredKeys, deletedKeys, epochIds });
16625
16672
  }
16626
16673
  function reviveBatch(batch, expiredKeys, deletedKeys) {
16627
16674
  const hasRange = Number.isFinite(batch?.firstMs) && Number.isFinite(batch?.lastMs);
@@ -16633,6 +16680,7 @@ function reviveBatch(batch, expiredKeys, deletedKeys) {
16633
16680
  ...cleanText(batch?.epochId) ? { epochId: cleanText(batch.epochId) } : {},
16634
16681
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
16635
16682
  ...hasRange ? { firstMs: batch.firstMs, lastMs: batch.lastMs } : {},
16683
+ memberStatesByEpoch: reviveReceiptCheckpoints(batch?.receiptCheckpoints),
16636
16684
  expiredKeys,
16637
16685
  deletedKeys
16638
16686
  };
@@ -16649,10 +16697,20 @@ function mergeBatches(left, right) {
16649
16697
  ...Number.isSafeInteger(right?.epochVersion ?? left?.epochVersion) ? { epochVersion: right?.epochVersion ?? left?.epochVersion } : {},
16650
16698
  ...firstValues.length ? { firstMs: Math.min(...firstValues) } : {},
16651
16699
  ...lastValues.length ? { lastMs: Math.max(...lastValues) } : {},
16700
+ memberStatesByEpoch: mergeChatReceiptCheckpointsByEpoch(left?.memberStatesByEpoch, right?.memberStatesByEpoch),
16652
16701
  expiredKeys: keySet([...left?.expiredKeys || [], ...right?.expiredKeys || []]),
16653
16702
  deletedKeys: keySet([...left?.deletedKeys || [], ...right?.deletedKeys || []])
16654
16703
  };
16655
16704
  }
16705
+ function cachedReceiptCheckpointSignature(statesByEpoch) {
16706
+ const values = [];
16707
+ for (const [epochId, value] of statesByEpoch instanceof Map ? statesByEpoch : []) {
16708
+ const checkpoint = cleanReceiptCheckpoint(value);
16709
+ if (checkpoint)
16710
+ values.push(`${epochId}:${chatReceiptProjectionFingerprint(checkpoint.projection)}`);
16711
+ }
16712
+ return values.sort().join("|");
16713
+ }
16656
16714
  function olderMarker(left, right) {
16657
16715
  if (!left)
16658
16716
  return right || null;
@@ -17055,7 +17113,7 @@ function writeCachedMessageState(cache, { stateVersion = null, chatId, selfChatP
17055
17113
  historyStartReached: nextHistoryStartReached === true,
17056
17114
  olderThan: serializeMarker(storedOlderThan),
17057
17115
  olderLoaded: nextOlderLoaded === true || compacted.historyMessages.length > 0,
17058
- latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch),
17116
+ latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch, compacted.all),
17059
17117
  deletedKeys: serializeKeys(deletedKeys),
17060
17118
  expiredKeys: serializeKeys(expiredKeys),
17061
17119
  historyMessages: compacted.historyMessages,
@@ -17084,6 +17142,7 @@ var init_messages2 = __esm(() => {
17084
17142
  init_state();
17085
17143
  init_messagekeys();
17086
17144
  init_compact();
17145
+ init_receipt();
17087
17146
  init_filename();
17088
17147
  init_time();
17089
17148
  init_core();
@@ -19722,6 +19781,9 @@ var init_chatlive = __esm(() => {
19722
19781
  });
19723
19782
 
19724
19783
  // ../../core/chat/live.js
19784
+ function makeChatCompositionId() {
19785
+ return toHex(randomBytes3(16));
19786
+ }
19725
19787
  function chatMessageCompositionId(message) {
19726
19788
  const value = String(message?.compositionId || "").trim().toLowerCase();
19727
19789
  return CHAT_COMPOSITION_ID_RE.test(value) ? value : "";
@@ -19905,10 +19967,16 @@ function createChatLive({
19905
19967
  onActivity?.(entry.chatId, EMPTY_ACTIVITY);
19906
19968
  };
19907
19969
  const enterChatActivity = async (chatId) => {
19908
- if (!enabled || !chatId || rooms.has(chatId))
19970
+ if (!enabled || !chatId)
19909
19971
  return false;
19972
+ const current = rooms.get(chatId);
19973
+ if (current && !current.closed) {
19974
+ current.retainers += 1;
19975
+ return true;
19976
+ }
19910
19977
  const entry = {
19911
19978
  chatId,
19979
+ retainers: 1,
19912
19980
  epochId: "",
19913
19981
  material: null,
19914
19982
  connection: null,
@@ -19984,6 +20052,9 @@ function createChatLive({
19984
20052
  const entry = rooms.get(chatId);
19985
20053
  if (!entry)
19986
20054
  return false;
20055
+ entry.retainers -= 1;
20056
+ if (entry.retainers > 0)
20057
+ return true;
19987
20058
  rooms.delete(chatId);
19988
20059
  destroy(entry);
19989
20060
  return true;
@@ -20031,13 +20102,26 @@ function createChatLive({
20031
20102
  publish(entry);
20032
20103
  return scheduleReadSnapshot(entry);
20033
20104
  };
20105
+ const flushChatReadFrontier = async (chatId) => {
20106
+ const entry = rooms.get(chatId);
20107
+ if (!entry || entry.closed || !entry.connected || !entry.readFrontier)
20108
+ return false;
20109
+ if (!sendSnapshot(entry))
20110
+ return false;
20111
+ const pending = entry.sendChain;
20112
+ await pending;
20113
+ return !entry.closed;
20114
+ };
20034
20115
  const leaveAllChatActivity = () => {
20035
- for (const chatId of [...rooms.keys()])
20036
- leaveChatActivity(chatId);
20116
+ for (const [chatId, entry] of [...rooms]) {
20117
+ rooms.delete(chatId);
20118
+ destroy(entry);
20119
+ }
20037
20120
  };
20038
20121
  return Object.freeze({
20039
20122
  close: leaveAllChatActivity,
20040
20123
  enterChatActivity,
20124
+ flushChatReadFrontier,
20041
20125
  leaveChatActivity,
20042
20126
  leaveAllChatActivity,
20043
20127
  markChatReadFrontier,
@@ -23659,7 +23743,7 @@ async function openChatReceiptCheckpoint(epoch, record) {
23659
23743
  cleanBytes(keys.key, plaintext);
23660
23744
  }
23661
23745
  }
23662
- var CHAT_RECEIPT_CHECKPOINT_VERSION = 1, CHAT_RECEIPT_CHECKPOINT_MAX_BYTES = 4096, CHECKPOINT_BODY_VERSION = 1, CHECKPOINT_BODY_SUITE = 1, CHECKPOINT_HEADER_BYTES, CHECKPOINT_SIGN_SCOPE;
23746
+ var CHAT_RECEIPT_CHECKPOINT_VERSION = 2, CHAT_RECEIPT_CHECKPOINT_MAX_BYTES, CHECKPOINT_BODY_VERSION = 1, CHECKPOINT_BODY_SUITE = 1, CHECKPOINT_HEADER_BYTES, CHECKPOINT_SIGN_SCOPE;
23663
23747
  var init_chatreceipt = __esm(() => {
23664
23748
  init_box();
23665
23749
  init_canonical();
@@ -23671,8 +23755,9 @@ var init_chatreceipt = __esm(() => {
23671
23755
  init_protocol();
23672
23756
  init_receipt();
23673
23757
  "use client";
23758
+ CHAT_RECEIPT_CHECKPOINT_MAX_BYTES = 16 * 1024;
23674
23759
  CHECKPOINT_HEADER_BYTES = 2 + BOX_NONCE_BYTES;
23675
- CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v3-sign");
23760
+ CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v4-sign");
23676
23761
  });
23677
23762
 
23678
23763
  // ../../core/chat/receiptsession.js
@@ -23759,6 +23844,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23759
23844
  let closed = false;
23760
23845
  let closing = false;
23761
23846
  let checkpoint = null;
23847
+ let displayedProjection = null;
23762
23848
  let sourceProjection = null;
23763
23849
  let sourceStop = null;
23764
23850
  let sourceLeaseTimer = null;
@@ -23824,8 +23910,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23824
23910
  checkpoint = next;
23825
23911
  if (closing || closed)
23826
23912
  return;
23827
- if (next)
23828
- onUpdate?.(projected(next));
23913
+ if (next) {
23914
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, next.projection);
23915
+ onUpdate?.(projected({ ...next, projection: displayedProjection }));
23916
+ }
23829
23917
  if (coversDesired(next)) {
23830
23918
  desiredState = null;
23831
23919
  clearStalledClaimTimer();
@@ -23838,10 +23926,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23838
23926
  else
23839
23927
  armSourceExpiry();
23840
23928
  };
23841
- const writeCheckpoint = async (mode, expectedVersion = null) => {
23842
- if (closed || closing)
23929
+ const writeCheckpoint = async (mode, expectedVersion = null, projectionOverride = null) => {
23930
+ if (closed || closing && mode !== "release")
23843
23931
  return false;
23844
- const desiredProjection = mode === "publish" ? sourceProjection : null;
23932
+ const desiredProjection = mode === "publish" || mode === "release" ? projectionOverride || sourceProjection : null;
23845
23933
  const result = await cloud.chat.memberState.write({
23846
23934
  epochId: epoch.epochId,
23847
23935
  lane: address.lane,
@@ -23852,19 +23940,22 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23852
23940
  const current = record ? await openChatReceiptCheckpoint(epoch, record).catch(() => null) : null;
23853
23941
  const heldByOther = !holdsLease(current, now) && Number(current?.leaseUntil) > now;
23854
23942
  const stalledTakeover = mode === "claim" && expectedVersion && checkpointVersion(current) === expectedVersion && !coversDesired(current);
23943
+ if (mode === "release" && !holdsLease(current, now)) {
23944
+ return { result: { won: false, checkpoint: current } };
23945
+ }
23855
23946
  if (heldByOther && !stalledTakeover)
23856
23947
  return { result: { won: false, checkpoint: current } };
23857
- const projection = desiredProjection || current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
23948
+ const projection = desiredProjection ? mergeChatReceiptProjections(current?.projection, desiredProjection) : current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
23858
23949
  if (mode === "publish" && holdsLease(current, now) && chatReceiptProjectionFingerprint(current.projection) === chatReceiptProjectionFingerprint(projection)) {
23859
23950
  return { result: { won: true, checkpoint: current } };
23860
23951
  }
23861
23952
  const term = current ? current.term + (!holdsLease(current, now) || Number(current.leaseUntil) <= now ? 1 : 0) : 1;
23862
23953
  const sealed = await sealChatReceiptCheckpoint(epoch, {
23863
- v: 1,
23954
+ v: CHAT_RECEIPT_CHECKPOINT_VERSION,
23864
23955
  coordinator: epoch.actor.chatPK,
23865
23956
  holder,
23866
23957
  term,
23867
- leaseUntil: now + RECEIPT_COORDINATOR_LEASE_MS,
23958
+ leaseUntil: mode === "release" ? now : now + RECEIPT_COORDINATOR_LEASE_MS,
23868
23959
  publishedAt: now,
23869
23960
  projection
23870
23961
  });
@@ -23914,10 +24005,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23914
24005
  sourceStop = watchChatMemberStateEvidence(cloud, epochState, ({ states }) => {
23915
24006
  if (closed || closing || !holdsLease(checkpoint))
23916
24007
  return;
23917
- const nextProjection = buildChatReceiptProjection(epoch.manifest, states);
23918
- sourceProjection = nextProjection;
24008
+ sourceProjection = mergeChatReceiptProjections(checkpoint?.projection, buildChatReceiptProjection(epoch.manifest, states));
24009
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, sourceProjection);
23919
24010
  onUpdate?.(projectChatReceiptCheckpoint(epoch.manifest, {
23920
- projection: sourceProjection,
24011
+ projection: displayedProjection,
23921
24012
  publishedAt: Date.now()
23922
24013
  }));
23923
24014
  schedulePublish();
@@ -24007,6 +24098,8 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
24007
24098
  if (closed || closing)
24008
24099
  return;
24009
24100
  closing = true;
24101
+ const finalProjection = sourceProjection;
24102
+ const release = holdsLease(checkpoint) ? track(writeCheckpoint("release", null, finalProjection)) : null;
24010
24103
  watchRun += 1;
24011
24104
  clearClaimTimer();
24012
24105
  clearStalledClaimTimer();
@@ -24020,7 +24113,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
24020
24113
  closing = false;
24021
24114
  closeReceiptEpoch(opened);
24022
24115
  };
24023
- Promise.allSettled([...operations]).finally(finish);
24116
+ Promise.allSettled([...operations, release].filter(Boolean)).finally(finish);
24024
24117
  }
24025
24118
  };
24026
24119
  }
@@ -25092,6 +25185,62 @@ var init_session2 = __esm(() => {
25092
25185
  init_receiptsession();
25093
25186
  });
25094
25187
 
25188
+ // ../../core/chat/retirement.js
25189
+ function planChatRetirement(chat, selfChatPK) {
25190
+ const self2 = cleanText(selfChatPK);
25191
+ const members = Array.isArray(chat?.members) ? chat.members : [];
25192
+ const remaining = members.filter((member) => cleanText(member?.chatPK) !== self2);
25193
+ const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
25194
+ return {
25195
+ action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
25196
+ remaining
25197
+ };
25198
+ }
25199
+ async function retireChatMembershipOwner(cloud, identity, entry, fields = {}) {
25200
+ const parent = entry?.current?.manifest;
25201
+ const kind = fields.kind;
25202
+ const epochVersion = fields.epochVersion;
25203
+ if (!parent?.chatId || !identity?.uid || !identity?.chatPrivateKey || !Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion)) {
25204
+ throw new Error("chat membership retirement required");
25205
+ }
25206
+ const entryId = ownChatEntryId(identity.chatPrivateKey, parent.chatId);
25207
+ const historyId = ownEpochEntryId(identity.chatPrivateKey, parent.chatId, parent.epochId);
25208
+ const cutoffMs = Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : Date.now();
25209
+ const history = makeOwnerEpochRecord(parent, entry.current.epochSecret, {
25210
+ cutoffMs,
25211
+ successorTransitionCommitment: fields.successorTransitionCommitment,
25212
+ successorTransitionMessageId: fields.successorTransitionMessageId,
25213
+ leaveProposalId: fields.leaveProposalId
25214
+ });
25215
+ return retireOwnChatEntry(cloud, identity, entryId, entry, {
25216
+ expectedEpochVersion: parent.epochVersion,
25217
+ retirement: {
25218
+ kind,
25219
+ epochVersion,
25220
+ ...kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? { proposalId: fields.leaveProposalId } : {}
25221
+ },
25222
+ epoch: {
25223
+ id: historyId,
25224
+ record: {
25225
+ body: await sealOwnerEpochRecord(identity.chatPrivateKey, entryId, historyId, history),
25226
+ tsMs: cutoffMs
25227
+ }
25228
+ },
25229
+ mlsStateId: ownerChatMlsSnapshotId(identity, entryId, entry.current)
25230
+ });
25231
+ }
25232
+ var CHAT_RETIREMENT_ACTIONS;
25233
+ var init_retirement = __esm(() => {
25234
+ init_entry();
25235
+ init_manifest();
25236
+ init_owner2();
25237
+ init_owner();
25238
+ CHAT_RETIREMENT_ACTIONS = Object.freeze({
25239
+ DELETE: "delete",
25240
+ LEAVE: "leave"
25241
+ });
25242
+ });
25243
+
25095
25244
  // ../../core/chat/inbox.js
25096
25245
  function normalizeChatPreview(record, message) {
25097
25246
  if (!message || typeof message !== "object")
@@ -25393,7 +25542,7 @@ function sameRoutes(left, right) {
25393
25542
  return leftKeys.length === rightKeys.length && leftKeys.every((key) => sameRoute(left[key], right[key]));
25394
25543
  }
25395
25544
  function sameStableEntry(left, right) {
25396
- return !!left && !!right && left.current?.manifest?.epochId === right.current?.manifest?.epochId && left.current?.settings?.digest === right.current?.settings?.digest && sameRoutes(left.routes, right.routes) && left.deliveryRegistered === right.deliveryRegistered && left.notificationTag === right.notificationTag;
25545
+ return !!left && !!right && left.current?.manifest?.epochId === right.current?.manifest?.epochId && left.current?.settings?.digest === right.current?.settings?.digest && sameRoutes(left.routes, right.routes) && left.deliveryRegistered === right.deliveryRegistered && left.notificationTag === right.notificationTag && left.retirement?.kind === right.retirement?.kind && left.retirement?.epochVersion === right.retirement?.epochVersion;
25397
25546
  }
25398
25547
  function projectOwnChatEntry(entry, entryId, userChatPK, ts, activity = {}) {
25399
25548
  const manifest = entry.current.manifest;
@@ -25443,13 +25592,17 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25443
25592
  }
25444
25593
  let knownMember = entry?.current?.manifest?.members?.find((member) => member.chatPK === payload.senderChatPK) || null;
25445
25594
  const senderProfile = openedProfile || (knownMember ? { ...knownMember, uid: knownMember.uid } : null) || await resolveSenderProfile(cloud, payload);
25446
- if (entry && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
25595
+ let restoringMembership = !!entry?.retirement;
25596
+ if (restoringMembership && (payload.kind !== "welcome" || payload.epochVersion <= entry.retirement.epochVersion)) {
25597
+ return "stale";
25598
+ }
25599
+ if (entry && !restoringMembership && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
25447
25600
  return "duplicate";
25448
25601
  }
25449
- if (entry && payload.epochVersion < entry.current.manifest.epochVersion) {
25602
+ if (entry && !restoringMembership && payload.epochVersion < entry.current.manifest.epochVersion) {
25450
25603
  return "stale";
25451
25604
  }
25452
- if (entry && payload.epochVersion > entry.current.manifest.epochVersion) {
25605
+ if (entry && !restoringMembership && payload.epochVersion > entry.current.manifest.epochVersion) {
25453
25606
  let advanced;
25454
25607
  try {
25455
25608
  advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
@@ -25466,11 +25619,16 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25466
25619
  }
25467
25620
  }
25468
25621
  if (advanced?.removed) {
25469
- options.onPingDelete?.(payload.chatId);
25470
- return "deleted";
25622
+ if (payload.kind !== "welcome" || payload.epochVersion <= advanced.entry?.retirement?.epochVersion) {
25623
+ options.onPingDelete?.(payload.chatId);
25624
+ return "deleted";
25625
+ }
25626
+ entry = advanced.entry;
25627
+ restoringMembership = true;
25628
+ } else {
25629
+ entry = advanced;
25630
+ knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
25471
25631
  }
25472
- entry = advanced;
25473
- knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
25474
25632
  }
25475
25633
  if (payload.kind === "chat_deleted") {
25476
25634
  if (!entry || entry.current.manifest.epochId !== payload.epochId || entry.current.manifest.epochVersion !== payload.epochVersion || !knownMember) {
@@ -25490,7 +25648,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25490
25648
  return "deleted";
25491
25649
  }
25492
25650
  let epochState = entry?.current || null;
25493
- if (!entry && payload.kind === "welcome") {
25651
+ if ((!entry || restoringMembership) && payload.kind === "welcome") {
25494
25652
  epochState = await welcomedEpochFromPing(cloud, identity, senderProfile, payload, options);
25495
25653
  } else if (!entry) {
25496
25654
  throw new Error("chat mls welcome required");
@@ -25538,7 +25696,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25538
25696
  const routes = withSenderRoute(entry, payload, senderProfile, epochState);
25539
25697
  const preview2 = nextPreview(currentChat, record, message, payload);
25540
25698
  const nowMs2 = timestampMs(record?.ts, null) ?? timestampMs(payload.ts, Date.now()) ?? Date.now();
25541
- const membershipWelcome = !entry && payload.kind === "welcome";
25699
+ const membershipWelcome = (!entry || restoringMembership) && payload.kind === "welcome";
25542
25700
  const projectedTsMs = membershipWelcome ? Date.now() : nowMs2;
25543
25701
  let deliveryRegistered = entry?.deliveryRegistered === true;
25544
25702
  if (!deliveryRegistered)
@@ -25556,7 +25714,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25556
25714
  },
25557
25715
  routes,
25558
25716
  deliveryRegistered,
25559
- notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK)
25717
+ notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK),
25718
+ retirement: null
25560
25719
  } : makeOwnChatEntry(epochState, {
25561
25720
  routes,
25562
25721
  startMs: nowMs2,
@@ -25572,6 +25731,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25572
25731
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
25573
25732
  };
25574
25733
  }
25734
+ if (membershipWelcome && current.retirement && epochState.manifest.epochVersion <= current.retirement.epochVersion) {
25735
+ throw new Error("chat welcome is not newer than retirement");
25736
+ }
25575
25737
  if (current.current.manifest.epochVersion > epochState.manifest.epochVersion) {
25576
25738
  return { result: current };
25577
25739
  }
@@ -25580,7 +25742,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25580
25742
  current: nextEntry.current,
25581
25743
  routes: withSenderRoute(current, payload, senderProfile, epochState),
25582
25744
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
25583
- notificationTag: nextEntry.notificationTag
25745
+ notificationTag: nextEntry.notificationTag,
25746
+ retirement: membershipWelcome ? null : current.retirement
25584
25747
  };
25585
25748
  if (sameStableEntry(current, candidate) && !welcomeMlsState)
25586
25749
  return { result: current };
@@ -25590,6 +25753,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25590
25753
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
25591
25754
  };
25592
25755
  }) : entry;
25756
+ if (committedEntry?.retirement) {
25757
+ throw new Error("chat membership remains retired");
25758
+ }
25593
25759
  if (membershipWelcome && epochState.mlsSnapshot) {
25594
25760
  if (!epochState.mlsKeyPackageLastResort) {
25595
25761
  await cloud.user.mls.keyPackages.deleteState(identity.uid, epochState.mlsKeyPackageId).catch(() => false);
@@ -25694,15 +25860,19 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25694
25860
  });
25695
25861
  await cloud.delivery.revoke(capability).catch(() => false);
25696
25862
  }
25697
- if (options.wasRemoved?.(parent.chatId) !== true) {
25698
- markDiag(options.diag, "chat.owner.unlink", { reason: "membership" });
25699
- await cloud.user.chats.unlink(uid, entryId);
25700
- }
25863
+ markDiag(options.diag, "chat.owner.retire", { reason: "membership" });
25864
+ const retiredEntry = await retireChatMembershipOwner(cloud, identity, entry, {
25865
+ kind: CHAT_OWNER_RETIREMENT_KINDS.REMOVED,
25866
+ epochVersion: confirmed.nextEpochVersion,
25867
+ cutoffMs: Date.now(),
25868
+ successorTransitionCommitment: confirmed.transitionCommitment,
25869
+ successorTransitionMessageId: messageId
25870
+ });
25701
25871
  if (leaveProposalId) {
25702
25872
  options.leaveProposalObservedAt?.delete(`${parent.chatId}:${leaveProposalId}`);
25703
25873
  }
25704
25874
  options.onRemoved?.(parent.chatId, confirmed.nextEpochVersion);
25705
- return { removed: true, chatId: parent.chatId };
25875
+ return { removed: true, chatId: parent.chatId, entry: retiredEntry };
25706
25876
  }
25707
25877
  openStage = "mls-package-open";
25708
25878
  const opened = await openChatMlsEpochPackage(processed, packageRecord, {
@@ -25965,6 +26135,7 @@ var init_inbox2 = __esm(() => {
25965
26135
  init_membership();
25966
26136
  init_entry();
25967
26137
  init_owner();
26138
+ init_retirement();
25968
26139
  init_manifest();
25969
26140
  init_state2();
25970
26141
  init_transition();
@@ -26385,6 +26556,9 @@ async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
26385
26556
  if (entry.ownerRevision !== data?.revision) {
26386
26557
  throw new Error("chat owner revision mismatch");
26387
26558
  }
26559
+ if (entry.retirement) {
26560
+ return null;
26561
+ }
26388
26562
  if (entry.protocol !== CHAT_PROTOCOL_VERSION) {
26389
26563
  return null;
26390
26564
  }
@@ -27488,26 +27662,6 @@ var init_listsession = __esm(() => {
27488
27662
  CHAT_LIST_RESET_ERROR_CODES = new Set(["permission-denied", "unauthenticated"]);
27489
27663
  });
27490
27664
 
27491
- // ../../core/chat/retirement.js
27492
- function planChatRetirement(chat, selfChatPK) {
27493
- const self2 = cleanText(selfChatPK);
27494
- const members = Array.isArray(chat?.members) ? chat.members : [];
27495
- const remaining = members.filter((member) => cleanText(member?.chatPK) !== self2);
27496
- const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
27497
- return {
27498
- action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
27499
- remaining
27500
- };
27501
- }
27502
- var CHAT_RETIREMENT_ACTIONS;
27503
- var init_retirement = __esm(() => {
27504
- init_manifest();
27505
- CHAT_RETIREMENT_ACTIONS = Object.freeze({
27506
- DELETE: "delete",
27507
- LEAVE: "leave"
27508
- });
27509
- });
27510
-
27511
27665
  // ../../core/utils/appstate.js
27512
27666
  function isForegroundAppState(state) {
27513
27667
  const value = String(state || "").trim().toLowerCase();
@@ -27732,6 +27886,7 @@ function createChatSession({
27732
27886
  deleteAllChats,
27733
27887
  restoreDeletedChat,
27734
27888
  enterChatActivity,
27889
+ flushChatReadFrontier,
27735
27890
  leaveChatActivity,
27736
27891
  markChatReadState,
27737
27892
  markChatRead,
@@ -28658,9 +28813,13 @@ function createChatSession({
28658
28813
  recipientChatPK: chatPK,
28659
28814
  generation: chat.epochVersion
28660
28815
  });
28816
+ await retireChatMembershipOwner(cloud, transitionIdentity(), chat.ownEntry, {
28817
+ kind: CHAT_OWNER_RETIREMENT_KINDS.LEAVE,
28818
+ epochVersion: chat.epochVersion,
28819
+ cutoffMs: Date.now(),
28820
+ leaveProposalId: proposal.message.cid
28821
+ });
28661
28822
  await cloud.delivery.revoke(capability).catch(() => false);
28662
- await discardOwnerChatMlsSnapshot(cloud, transitionIdentity(), chat.entryId, chat.epochState);
28663
- await cloud.user.chats.unlink(uid, chat.entryId);
28664
28823
  if (retirementStarted) {
28665
28824
  deleteActions.commitRemovedRetirement(chatId, chat.epochVersion + 1);
28666
28825
  retirementStarted = false;
@@ -29017,11 +29176,12 @@ function createChatSession({
29017
29176
  const dropChat = (...args) => deleteActions.dropChat(...args);
29018
29177
  const dropUnavailableChat = (...args) => deleteActions.dropUnavailableChat(...args);
29019
29178
  const deleteChat = (...args) => deleteActions.deleteChat(...args);
29020
- const retireChats = async (affected) => {
29179
+ const purgePeerChats = async (affected) => {
29021
29180
  for (const chat of affected) {
29022
29181
  const retirement = planChatRetirement(chat, chatPK);
29023
29182
  if (retirement.action === CHAT_RETIREMENT_ACTIONS.LEAVE) {
29024
29183
  await leaveOwnedChat(chat, { optimistic: false });
29184
+ await cloud.user.chats.unlink(uid, chat.entryId);
29025
29185
  } else {
29026
29186
  await deleteChat(chat, { cleanup: false });
29027
29187
  }
@@ -29031,7 +29191,7 @@ function createChatSession({
29031
29191
  const retirePeerChat = async (peerChatPK, options = {}) => {
29032
29192
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
29033
29193
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK === peerChatPK));
29034
- await retireChats(affected);
29194
+ await purgePeerChats(affected);
29035
29195
  await options.onDeliveryRevoked?.();
29036
29196
  return affected.length > 0;
29037
29197
  };
@@ -29040,7 +29200,7 @@ function createChatSession({
29040
29200
  return 0;
29041
29201
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
29042
29202
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK !== chatPK && blockedUids.has(member.uid)));
29043
- return retireChats(affected);
29203
+ return purgePeerChats(affected);
29044
29204
  };
29045
29205
  const restoreDeletedChat = (...args) => deleteActions.restoreDeletedChat(...args);
29046
29206
  const wasChatDeletedLocally = (...args) => deleteActions.wasChatDeletedLocally(...args);
@@ -29054,6 +29214,7 @@ function createChatSession({
29054
29214
  });
29055
29215
  return liveActions.enterChatActivity(chatId);
29056
29216
  };
29217
+ const flushChatReadFrontier = (chatId) => liveActions.flushChatReadFrontier(chatId);
29057
29218
  const leaveChatActivity = (chatId) => {
29058
29219
  const flush = seenActions.flushChatRead(chatId);
29059
29220
  liveActions.leaveChatActivity(chatId);
@@ -29627,7 +29788,6 @@ var init_session3 = __esm(() => {
29627
29788
  init_create();
29628
29789
  init_membership();
29629
29790
  init_pool();
29630
- init_owner2();
29631
29791
  init_manifest();
29632
29792
  init_notifications();
29633
29793
  init_state2();
@@ -39359,6 +39519,8 @@ async function listenAccount(client, options = {}) {
39359
39519
  const includeChats = options.chats !== false;
39360
39520
  const includeTransactions = options.transactions !== false;
39361
39521
  const markRead = options.read !== false;
39522
+ const relayReads = markRead && options.relayReads === true;
39523
+ const readLiveIdleMs = Number.isFinite(Number(options.readLiveIdleMs)) ? Math.max(0, Math.floor(Number(options.readLiveIdleMs))) : 15000;
39362
39524
  const replay = options.replay === true;
39363
39525
  const persistentChatIdleMs = Number.isFinite(Number(options.persistentChatIdleMs)) ? Math.max(0, Math.floor(Number(options.persistentChatIdleMs))) : 0;
39364
39526
  const emit2 = typeof options.onEvent === "function" ? options.onEvent : () => {};
@@ -39369,8 +39531,12 @@ async function listenAccount(client, options = {}) {
39369
39531
  const chatActivityTimers = new Map;
39370
39532
  const subscribedWakeVersions = new Map;
39371
39533
  const idleWakeVersions = new Map;
39534
+ const messageBatchTasks = new Map;
39372
39535
  const pendingChats = new Map;
39373
39536
  const processedWakeVersions = new Map;
39537
+ const readLiveChats = new Set;
39538
+ const readLiveEntering = new Map;
39539
+ const readLiveTimers = new Map;
39374
39540
  const wantedWakeVersions = new Map;
39375
39541
  let activeChatIds = new Set;
39376
39542
  let chatSyncRequested = false;
@@ -39415,6 +39581,44 @@ async function listenAccount(client, options = {}) {
39415
39581
  const account = client.accountSummary();
39416
39582
  const runtime = client.runtime;
39417
39583
  const session = client.session;
39584
+ async function enterReadLive(chatId) {
39585
+ if (!relayReads)
39586
+ return false;
39587
+ if (readLiveChats.has(chatId))
39588
+ return true;
39589
+ const current = readLiveEntering.get(chatId);
39590
+ if (current)
39591
+ return current;
39592
+ const entering = runtime.chat.getSnapshot().enterChatActivity(chatId).then(async (entered) => {
39593
+ if (!entered)
39594
+ return false;
39595
+ if (closed) {
39596
+ await runtime.chat.getSnapshot().leaveChatActivity(chatId);
39597
+ return false;
39598
+ }
39599
+ readLiveChats.add(chatId);
39600
+ return true;
39601
+ }).finally(() => readLiveEntering.delete(chatId));
39602
+ readLiveEntering.set(chatId, entering);
39603
+ return entering;
39604
+ }
39605
+ function releaseReadLiveSoon(chatId) {
39606
+ if (!readLiveChats.has(chatId))
39607
+ return;
39608
+ const current = readLiveTimers.get(chatId);
39609
+ if (current)
39610
+ clearTimeout(current);
39611
+ const timer = setTimeout(() => {
39612
+ if (readLiveTimers.get(chatId) !== timer)
39613
+ return;
39614
+ readLiveTimers.delete(chatId);
39615
+ if (!readLiveChats.delete(chatId))
39616
+ return;
39617
+ runtime.chat.getSnapshot().leaveChatActivity(chatId);
39618
+ }, readLiveIdleMs);
39619
+ timer.unref?.();
39620
+ readLiveTimers.set(chatId, timer);
39621
+ }
39418
39622
  function projectedChat(chat) {
39419
39623
  const peerChatPK = getChatPeerPK(chat);
39420
39624
  const peerByChatPK = runtime.peers.getSnapshot().peerByChatPK;
@@ -39434,13 +39638,15 @@ async function listenAccount(client, options = {}) {
39434
39638
  const sourceMessages = batch?.sourceMessages?.length ? batch.sourceMessages : batch?.messages || [];
39435
39639
  return projectMessages(sourceMessages, session.chatPK, peerChatPK || session.chatPK, chat?.memberChatPKs).map((message) => simplifyAccountMessage(message, chat, session.chatPK, { raw: true }));
39436
39640
  }
39437
- function applyMessageBatch(chat, batch, emitChanges) {
39641
+ async function applyMessageBatch(chat, batch, emitChanges) {
39438
39642
  if (!batch?.ready)
39439
39643
  return;
39440
39644
  touchChatActivity(chat.id);
39441
39645
  const targetChat = projectedChat(chat);
39646
+ const rawMessages = batch?.sourceMessages?.length ? batch.sourceMessages : batch?.messages || [];
39442
39647
  const messages = projectMessages2(chat, batch);
39443
39648
  const currentKeys = new Set;
39649
+ const messageEvents = [];
39444
39650
  for (const message of messages) {
39445
39651
  const key = messageKey(message);
39446
39652
  if (!key)
@@ -39451,16 +39657,37 @@ async function listenAccount(client, options = {}) {
39451
39657
  seenMessages.set(key, version);
39452
39658
  if (!emitChanges || previous === version || incomingOnly && message.from !== "peer")
39453
39659
  continue;
39454
- emit2(formatMessageEvent(targetChat, message, { compact: compact2 }));
39455
- }
39456
- const latestPeer = [...messages].reverse().find((message) => message.from === "peer");
39457
- const latestPeerKey = latestPeer ? messageKey(latestPeer) : "";
39458
- if (markRead && latestPeerKey && readMessages.get(chat.id) !== latestPeerKey) {
39459
- readMessages.set(chat.id, latestPeerKey);
39460
- runtime.chat.getSnapshot().markChatReadState(chat.id, latestPeer.raw, {
39461
- messages: messages.map((message) => message.raw)
39462
- });
39660
+ messageEvents.push(formatMessageEvent(targetChat, message, { compact: compact2 }));
39661
+ }
39662
+ const latestPeer = [...rawMessages].reverse().find((message) => isPeerMsg(message, session.chatPK));
39663
+ const latestPeerKey = latestPeer ? messageKey({ ...latestPeer, chatId: chat.id }) : "";
39664
+ const latestPeerEvent = latestPeer ? formatMessageEvent(targetChat, simplifyAccountMessage(latestPeer, chat, session.chatPK, { raw: true }), { compact: compact2 }) : null;
39665
+ const readTarget = getLatestReadStateTarget(rawMessages);
39666
+ const readTargetKey = readTarget ? messageKey({ ...readTarget, chatId: chat.id }) : "";
39667
+ const alreadyProcessed = latestPeerEvent && options.isProcessedEvent?.(latestPeerEvent) === true;
39668
+ if (markRead && readTargetKey && !(alreadyProcessed && readTargetKey === latestPeerKey) && readMessages.get(chat.id) !== readTargetKey) {
39669
+ readMessages.set(chat.id, readTargetKey);
39670
+ if (relayReads) {
39671
+ try {
39672
+ await enterReadLive(chat.id);
39673
+ await runtime.chat.getSnapshot().markChatReadState(chat.id, readTarget, { messages: rawMessages });
39674
+ await runtime.chat.getSnapshot().flushChatReadFrontier?.(chat.id);
39675
+ } catch (error) {
39676
+ emit2(event("error", {
39677
+ peer: peerTarget(targetChat),
39678
+ message: error?.message || String(error)
39679
+ }));
39680
+ } finally {
39681
+ releaseReadLiveSoon(chat.id);
39682
+ }
39683
+ } else {
39684
+ runtime.chat.getSnapshot().markChatReadState(chat.id, readTarget, {
39685
+ messages: rawMessages
39686
+ });
39687
+ }
39463
39688
  }
39689
+ for (const messageEvent of messageEvents)
39690
+ emit2(messageEvent);
39464
39691
  const previousForChat = [...seenMessages.keys()].filter((key) => key.startsWith(`${chat.id}:`));
39465
39692
  for (const key of previousForChat) {
39466
39693
  if (currentKeys.has(key))
@@ -39479,6 +39706,21 @@ async function listenAccount(client, options = {}) {
39479
39706
  }
39480
39707
  }
39481
39708
  }
39709
+ function queueMessageBatch(chat, batch, emitChanges) {
39710
+ const previous = messageBatchTasks.get(chat.id) || Promise.resolve();
39711
+ const task = previous.catch(() => {}).then(() => applyMessageBatch(chat, batch, emitChanges));
39712
+ messageBatchTasks.set(chat.id, task);
39713
+ task.then(() => {
39714
+ if (messageBatchTasks.get(chat.id) === task) {
39715
+ messageBatchTasks.delete(chat.id);
39716
+ }
39717
+ }, () => {
39718
+ if (messageBatchTasks.get(chat.id) === task) {
39719
+ messageBatchTasks.delete(chat.id);
39720
+ }
39721
+ });
39722
+ return task;
39723
+ }
39482
39724
  function checkpointBatch(chat, page) {
39483
39725
  const sourceMessages = page?.history?.sourceMessages?.length ? page.history.sourceMessages : page?.batch?.sourceMessages?.length ? page.batch.sourceMessages : page?.batch?.messages || [];
39484
39726
  const batch = {
@@ -39565,13 +39807,15 @@ async function listenAccount(client, options = {}) {
39565
39807
  if (closed || !activeChatIds.has(chat.id) || !page.chat || !transientChats && chatSubscriptions.has(chat.id)) {
39566
39808
  return;
39567
39809
  }
39568
- applyMessageBatch(chat, page.batch, emitInitial);
39810
+ await queueMessageBatch(chat, page.batch, emitInitial);
39569
39811
  if (transientChats) {
39570
39812
  processedWakeVersions.set(chat.id, wakeVersion);
39571
39813
  return;
39572
39814
  }
39573
39815
  const batches = runtime.chat.getSnapshot().messageBatches;
39574
- const unsubscribe = batches.subscribeMessageBatch(chat.id, (batch) => applyMessageBatch(chat, batch, ready));
39816
+ const unsubscribe = batches.subscribeMessageBatch(chat.id, (batch) => {
39817
+ queueMessageBatch(chat, batch, ready).catch((error) => emit2(event("error", { peer: peerTarget(targetChat), message: error?.message || String(error) })));
39818
+ });
39575
39819
  chatSubscriptions.set(chat.id, () => {
39576
39820
  unsubscribe();
39577
39821
  page.release?.();
@@ -39705,6 +39949,13 @@ async function listenAccount(client, options = {}) {
39705
39949
  for (const timer of chatActivityTimers.values())
39706
39950
  clearTimeout(timer);
39707
39951
  chatActivityTimers.clear();
39952
+ for (const timer of readLiveTimers.values())
39953
+ clearTimeout(timer);
39954
+ readLiveTimers.clear();
39955
+ await Promise.allSettled(messageBatchTasks.values());
39956
+ await Promise.allSettled(readLiveEntering.values());
39957
+ await Promise.allSettled([...readLiveChats].map((chatId) => runtime.chat.getSnapshot().leaveChatActivity(chatId)));
39958
+ readLiveChats.clear();
39708
39959
  processedWakeVersions.clear();
39709
39960
  wantedWakeVersions.clear();
39710
39961
  }
@@ -39713,6 +39964,7 @@ var DEFAULT_CHAT_COUNT = 20, DEFAULT_MESSAGE_COUNT = 10, DEFAULT_REPLAY_MESSAGE_
39713
39964
  var init_listen = __esm(() => {
39714
39965
  init_actions2();
39715
39966
  init_ids();
39967
+ init_messages();
39716
39968
  init_presentation();
39717
39969
  init_messages3();
39718
39970
  });
@@ -41572,6 +41824,7 @@ function mergeLatestServerPages(primary, secondary) {
41572
41824
  return {
41573
41825
  ...secondary || {},
41574
41826
  ...primary || {},
41827
+ memberStatesByEpoch: mergeChatReceiptCheckpointsByEpoch(secondary?.memberStatesByEpoch, primary?.memberStatesByEpoch),
41575
41828
  expiredKeys: keySet([...primary?.expiredKeys || [], ...secondary?.expiredKeys || []]),
41576
41829
  deletedKeys: keySet([...primary?.deletedKeys || [], ...secondary?.deletedKeys || []])
41577
41830
  };
@@ -41719,6 +41972,7 @@ var init_screen = __esm(() => {
41719
41972
  init_messagekeys();
41720
41973
  init_ids();
41721
41974
  init_state();
41975
+ init_receipt();
41722
41976
  init_time();
41723
41977
  init_control();
41724
41978
  init_window();
@@ -42120,10 +42374,7 @@ var init_resolve = __esm(() => {
42120
42374
 
42121
42375
  // ../../core/chat/messages/serversync.js
42122
42376
  function mergeMemberStates(previous, current) {
42123
- return new Map([
42124
- ...previous instanceof Map ? previous : [],
42125
- ...current instanceof Map ? current : []
42126
- ]);
42377
+ return mergeChatReceiptCheckpointsByEpoch(previous, current);
42127
42378
  }
42128
42379
  function isEpochBatchCutover(latestServerPage, msgBatch) {
42129
42380
  return !!(latestServerPage?.epochId && msgBatch?.epochId && latestServerPage.epochId !== msgBatch.epochId);
@@ -42347,6 +42598,7 @@ var init_serversync = __esm(() => {
42347
42598
  init_messagekeys();
42348
42599
  init_messages();
42349
42600
  init_state();
42601
+ init_receipt();
42350
42602
  init_resolve();
42351
42603
  init_history();
42352
42604
  init_window();
@@ -42775,6 +43027,7 @@ function conversationStateSignature(state) {
42775
43027
  cacheMessageListSignature(state.latestMessages),
42776
43028
  cacheKeySignature(state.latestServerPage?.expiredKeys),
42777
43029
  cacheKeySignature(state.latestServerPage?.deletedKeys),
43030
+ cachedReceiptCheckpointSignature(state.latestServerPage?.memberStatesByEpoch),
42778
43031
  reactionControlCacheSignature(state)
42779
43032
  ].join(`
42780
43033
  `);
@@ -45774,7 +46027,7 @@ function createMessageRouteSession({
45774
46027
  return;
45775
46028
  const localTarget = getLatestLocalReadTarget(viewedMessages, current.selfChatPublicKey);
45776
46029
  const localTargetKey = messageKey2(localTarget);
45777
- const target = getLatestReadStateTarget(viewedMessages);
46030
+ const target = getLatestReadStateTarget(current.rawMessages?.length ? current.rawMessages : viewedMessages);
45778
46031
  const targetKey = messageKey2(target);
45779
46032
  const localChanged = !!localTargetKey && localTargetKey !== refs.lastSelfReadTarget.current;
45780
46033
  const stateChanged = !!targetKey && targetKey !== refs.lastSharedReadTarget.current;
@@ -47376,6 +47629,31 @@ function createRuntimeChatActions({
47376
47629
  const route = enteredChats.get(id);
47377
47630
  return { exited: route?.leave() === true, chatId: id || null };
47378
47631
  }
47632
+ async function enterLive(chatId) {
47633
+ const { runtime, chat } = await chatForId(chatId);
47634
+ const entered2 = await runtime.chat.getSnapshot().enterChatActivity(chat.id);
47635
+ return { entered: entered2 === true, chatId: chat.id };
47636
+ }
47637
+ async function leaveLive(chatId) {
47638
+ const id = cleanText(chatId).toLowerCase();
47639
+ if (!/^[0-9a-f]{64}$/u.test(id))
47640
+ throw new Error("chat id required");
47641
+ const runtime = await getRuntime();
47642
+ await runtime.chat.getSnapshot().leaveChatActivity(id);
47643
+ return { left: true, chatId: id };
47644
+ }
47645
+ async function markTyping(chatId, typing = true, compositionId = "") {
47646
+ const { runtime, chat } = await chatForId(chatId);
47647
+ const nextTyping = typing === true;
47648
+ const nextCompositionId = cleanText(compositionId).toLowerCase() || (nextTyping ? makeChatCompositionId() : "");
47649
+ const marked = runtime.chat.getSnapshot().markChatTyping(chat.id, nextTyping, nextCompositionId);
47650
+ return {
47651
+ marked: marked === true,
47652
+ typing: nextTyping,
47653
+ compositionId: nextCompositionId,
47654
+ chatId: chat.id
47655
+ };
47656
+ }
47379
47657
  async function readById(chatId, options = {}) {
47380
47658
  const window2 = await messageWindowById(chatId, options);
47381
47659
  try {
@@ -48026,6 +48304,9 @@ function createRuntimeChatActions({
48026
48304
  readById,
48027
48305
  enterById,
48028
48306
  exitById,
48307
+ enterLive,
48308
+ leaveLive,
48309
+ markTyping,
48029
48310
  sendToChat,
48030
48311
  sendAttachmentToChat,
48031
48312
  readAttachmentInChat,
@@ -48445,6 +48726,7 @@ var DEFAULT_TIMEOUT_MS4 = 15000, HISTORY_LOAD_ATTEMPTS = 4, messageWindowSequenc
48445
48726
  var init_runtime_actions = __esm(() => {
48446
48727
  init_messages();
48447
48728
  init_ids();
48729
+ init_live();
48448
48730
  init_state();
48449
48731
  init_actions2();
48450
48732
  init_fees();
@@ -48462,7 +48744,7 @@ var package_default;
48462
48744
  var init_package2 = __esm(() => {
48463
48745
  package_default = {
48464
48746
  name: "veyl",
48465
- version: "0.60.0",
48747
+ version: "0.61.0",
48466
48748
  private: true,
48467
48749
  license: "UNLICENSED",
48468
48750
  workspaces: {
@@ -48796,6 +49078,9 @@ class AccountRuntime {
48796
49078
  readById: (chatId, payload) => this.runtimeChat.readById(chatId, payload),
48797
49079
  enterById: (chatId, payload) => this.runtimeChat.enterById(chatId, payload),
48798
49080
  exitById: (chatId) => this.runtimeChat.exitById(chatId),
49081
+ enterLive: (chatId) => this.runtimeChat.enterLive(chatId),
49082
+ leaveLive: (chatId) => this.runtimeChat.leaveLive(chatId),
49083
+ markTyping: (chatId, typing, compositionId) => this.runtimeChat.markTyping(chatId, typing, compositionId),
48799
49084
  sendTo: (chatId, message, payload) => this.runtimeChat.sendToChat(chatId, message, payload),
48800
49085
  replyIn: (chatId, messageId, message, payload) => this.runtimeChat.replyInChat(chatId, messageId, message, payload),
48801
49086
  reactIn: (chatId, messageId, emoji, payload) => this.runtimeChat.reactInChat(chatId, messageId, emoji, payload),