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