@agentunion/fastaun-browser 0.5.19 → 0.5.21

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.
Files changed (154) hide show
  1. package/CHANGELOG.md +176 -78
  2. package/_packed_docs/CHANGELOG.md +176 -78
  3. package/_packed_docs/INDEX.md +288 -269
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  7. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  8. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  9. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  10. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  14. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
  18. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  19. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  20. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  21. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  22. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  23. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  24. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  25. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
  26. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
  27. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
  28. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
  29. package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
  30. package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
  31. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  32. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  33. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  34. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
  35. package/_packed_docs/sdk/INDEX.md +197 -167
  36. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  37. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  38. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
  39. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  40. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
  41. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
  42. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
  43. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
  44. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  45. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  46. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
  47. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
  48. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
  49. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
  50. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
  51. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
  52. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
  53. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
  54. package/dist/agent-md-schema.d.ts +16 -4
  55. package/dist/agent-md-schema.d.ts.map +1 -1
  56. package/dist/agent-md-schema.js +65 -30
  57. package/dist/agent-md-schema.js.map +1 -1
  58. package/dist/agent-md.d.ts +35 -3
  59. package/dist/agent-md.d.ts.map +1 -1
  60. package/dist/agent-md.js +384 -150
  61. package/dist/agent-md.js.map +1 -1
  62. package/dist/aid-store.d.ts +59 -0
  63. package/dist/aid-store.d.ts.map +1 -1
  64. package/dist/aid-store.js +202 -16
  65. package/dist/aid-store.js.map +1 -1
  66. package/dist/aid.d.ts +4 -1
  67. package/dist/aid.d.ts.map +1 -1
  68. package/dist/aid.js +24 -42
  69. package/dist/aid.js.map +1 -1
  70. package/dist/auth.d.ts +1 -1
  71. package/dist/auth.d.ts.map +1 -1
  72. package/dist/auth.js +89 -18
  73. package/dist/auth.js.map +1 -1
  74. package/dist/bundle.js +6410 -1848
  75. package/dist/cert-utils.d.ts +50 -1
  76. package/dist/cert-utils.d.ts.map +1 -1
  77. package/dist/cert-utils.js +123 -56
  78. package/dist/cert-utils.js.map +1 -1
  79. package/dist/client/delivery.d.ts +108 -16
  80. package/dist/client/delivery.d.ts.map +1 -1
  81. package/dist/client/delivery.js +1411 -184
  82. package/dist/client/delivery.js.map +1 -1
  83. package/dist/client/rpc-pipeline.d.ts +4 -1
  84. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  85. package/dist/client/rpc-pipeline.js +203 -117
  86. package/dist/client/rpc-pipeline.js.map +1 -1
  87. package/dist/client/v2-e2ee.d.ts +22 -3
  88. package/dist/client/v2-e2ee.d.ts.map +1 -1
  89. package/dist/client/v2-e2ee.js +1312 -684
  90. package/dist/client/v2-e2ee.js.map +1 -1
  91. package/dist/client.d.ts +23 -2
  92. package/dist/client.d.ts.map +1 -1
  93. package/dist/client.js +419 -101
  94. package/dist/client.js.map +1 -1
  95. package/dist/config.d.ts +4 -0
  96. package/dist/config.d.ts.map +1 -1
  97. package/dist/config.js +1 -0
  98. package/dist/config.js.map +1 -1
  99. package/dist/crypto.d.ts.map +1 -1
  100. package/dist/crypto.js +8 -6
  101. package/dist/crypto.js.map +1 -1
  102. package/dist/events.d.ts +24 -3
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +171 -29
  105. package/dist/events.js.map +1 -1
  106. package/dist/facades.d.ts +42 -1
  107. package/dist/facades.d.ts.map +1 -1
  108. package/dist/facades.js +368 -12
  109. package/dist/facades.js.map +1 -1
  110. package/dist/index.d.ts +6 -4
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +1 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/keystore/index.d.ts +14 -1
  115. package/dist/keystore/index.d.ts.map +1 -1
  116. package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
  117. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  118. package/dist/keystore/indexeddb-identity-store.js +30 -13
  119. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  120. package/dist/keystore/indexeddb-shared.d.ts +19 -2
  121. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  122. package/dist/keystore/indexeddb-shared.js +193 -6
  123. package/dist/keystore/indexeddb-shared.js.map +1 -1
  124. package/dist/keystore/indexeddb-token-store.d.ts +15 -2
  125. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  126. package/dist/keystore/indexeddb-token-store.js +308 -27
  127. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  128. package/dist/org-protocol.d.ts +45 -0
  129. package/dist/org-protocol.d.ts.map +1 -1
  130. package/dist/org-protocol.js +827 -21
  131. package/dist/org-protocol.js.map +1 -1
  132. package/dist/ownership-v21.d.ts +43 -9
  133. package/dist/ownership-v21.d.ts.map +1 -1
  134. package/dist/ownership-v21.js +183 -81
  135. package/dist/ownership-v21.js.map +1 -1
  136. package/dist/ownership-workflow.d.ts +36 -1
  137. package/dist/ownership-workflow.d.ts.map +1 -1
  138. package/dist/ownership-workflow.js +610 -89
  139. package/dist/ownership-workflow.js.map +1 -1
  140. package/dist/seq-tracker.d.ts +2 -0
  141. package/dist/seq-tracker.d.ts.map +1 -1
  142. package/dist/seq-tracker.js +3 -2
  143. package/dist/seq-tracker.js.map +1 -1
  144. package/dist/tools/cross-sdk-agent.d.ts +61 -3
  145. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  146. package/dist/tools/cross-sdk-agent.js +524 -45
  147. package/dist/tools/cross-sdk-agent.js.map +1 -1
  148. package/dist/transport.d.ts +4 -0
  149. package/dist/transport.d.ts.map +1 -1
  150. package/dist/transport.js +64 -30
  151. package/dist/transport.js.map +1 -1
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { importCertPublicKeyEcdsa } from '../crypto.js';
2
+ import { _INTERNAL_MESSAGE_FLOW_LIMITS } from '../config.js';
2
3
  import { E2EEError, StateError, ValidationError } from '../errors.js';
3
4
  import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
4
5
  import { V2Session, V2KeyStore } from '../v2/session/index.js';
@@ -113,12 +114,10 @@ function deduplicateForwardPageMessages(value, afterSeq) {
113
114
  }
114
115
  return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
115
116
  }
116
- function contiguousPullSeq(afterSeq, seqs, proof) {
117
+ function contiguousPullSeq(afterSeq, seqs, _proof) {
117
118
  // Forward 原始页中的缺号视为服务端永久空号;页完成后按页内最大
118
119
  // seq 推进 A,避免单页空洞阻塞后续 pull/ACK。
119
- let contiguous = seqs.length > 0 ? Math.max(afterSeq, ...seqs) : afterSeq;
120
- const serverProof = safeNonNegativeSeq(proof);
121
- return serverProof !== null ? Math.max(contiguous, serverProof) : contiguous;
120
+ return seqs.length > 0 ? Math.max(afterSeq, ...seqs) : afterSeq;
122
121
  }
123
122
  function deduplicatePulledResults(messages) {
124
123
  const seen = new Set();
@@ -138,7 +137,7 @@ function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, h
138
137
  // 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
139
138
  if (rawCount <= 0 || nextAfter <= nextAfterSeq)
140
139
  return false;
141
- if (hasMore || serverHead > nextAfter || pendingAckSeq > 0 || observedUpper > nextAfter)
140
+ if (hasMore || serverHead > nextAfter || observedUpper > nextAfter)
142
141
  return true;
143
142
  // 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
144
143
  return !hasMoreField && limit > 0 && rawCount >= limit;
@@ -309,6 +308,7 @@ function v2E2eeMeta(envelope) {
309
308
  suite,
310
309
  encryption_mode: `v2_${suite || 'unknown'}`,
311
310
  forward_secrecy: true,
311
+ need_render: envelope.need_render !== false,
312
312
  };
313
313
  const protectedHeaders = metadataWithoutAuth(envelope.protected_headers);
314
314
  if (protectedHeaders && Object.keys(protectedHeaders).length > 0) {
@@ -331,6 +331,8 @@ function v2E2eeMeta(envelope) {
331
331
  function attachV2EnvelopeMetadata(message, meta) {
332
332
  if (!meta)
333
333
  return;
334
+ if (typeof meta.need_render === 'boolean')
335
+ message.need_render = meta.need_render;
334
336
  const payloadType = typeof meta.payload_type === 'string' ? meta.payload_type.trim() : '';
335
337
  if (payloadType)
336
338
  message.payload_type = payloadType;
@@ -801,11 +803,12 @@ export class V2E2EECoordinator {
801
803
  return null;
802
804
  }
803
805
  }
804
- pendingSenderIKMessageKey(msg, groupId) {
806
+ pendingSenderIKMessageKey(msg, groupId, fromAid = '') {
805
807
  const client = this.client;
806
808
  const messageId = String(msg.message_id ?? '').trim();
807
809
  const seq = String(msg.seq ?? '').trim();
808
- const prefix = groupId ? `group:${groupId}` : `p2p:${client._aid ?? ''}`;
810
+ const prefix = groupId ? `group:${groupId}` : client._p2pPeerPullEnabled
811
+ ? client._delivery.peerNamespace(String(fromAid || msg.sender_aid || msg.from_aid || msg.from || '')) : `p2p:${client._aid ?? ''}`;
809
812
  return `${prefix}:${messageId || seq || Math.random().toString(36).slice(2)}`;
810
813
  }
811
814
  pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
@@ -896,7 +899,7 @@ export class V2E2EECoordinator {
896
899
  return;
897
900
  const senderDeviceId = String(args.senderDeviceId ?? '');
898
901
  const groupId = String(args.groupId ?? '').trim();
899
- const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId);
902
+ const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId, fromAid);
900
903
  client._v2SenderIKPending.set(messageKey, {
901
904
  msg: { ...args.msg },
902
905
  fromAid,
@@ -920,8 +923,9 @@ export class V2E2EECoordinator {
920
923
  }
921
924
  async resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey, generation) {
922
925
  const client = this.client;
923
- const generationCurrent = () => generation === undefined
924
- || client._delivery.isInlineGenerationCurrent?.(generation) !== false;
926
+ const runtimeGeneration = Number(client._v2RuntimeGeneration ?? 0);
927
+ const generationCurrent = () => Number(client._v2RuntimeGeneration ?? 0) === runtimeGeneration
928
+ && (generation === undefined || client._delivery.isInlineGenerationCurrent?.(generation) !== false);
925
929
  try {
926
930
  if (!generationCurrent())
927
931
  return;
@@ -932,6 +936,8 @@ export class V2E2EECoordinator {
932
936
  peer_aid: fromAid,
933
937
  e2ee_wrap_capabilities: v2WrapCapabilities(),
934
938
  });
939
+ if (!generationCurrent())
940
+ return;
935
941
  const peers = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
936
942
  for (const dev of peers)
937
943
  client._cacheV2PeerIKFromDevice(dev, fromAid);
@@ -945,6 +951,8 @@ export class V2E2EECoordinator {
945
951
  group_id: groupId,
946
952
  e2ee_wrap_capabilities: v2WrapCapabilities(),
947
953
  });
954
+ if (!generationCurrent())
955
+ return;
948
956
  const devices = (Array.isArray(gbs?.devices) ? gbs.devices : []);
949
957
  const audit = (Array.isArray(gbs?.audit_recipients) ? gbs.audit_recipients : []);
950
958
  for (const dev of devices)
@@ -980,6 +988,8 @@ export class V2E2EECoordinator {
980
988
  }
981
989
  if (!generationCurrent())
982
990
  return;
991
+ if (plaintext && client._p2pPeerPullEnabled && !entry.groupId)
992
+ plaintext.need_render = entry.msg.need_render !== false;
983
993
  if (plaintext === null) {
984
994
  if (retryStatus.deferred) {
985
995
  client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
@@ -996,12 +1006,17 @@ export class V2E2EECoordinator {
996
1006
  }
997
1007
  if (!entry.groupId && String(entry.msg.storage ?? '') === 'volatile') {
998
1008
  await this.publishPendingV2VolatileMessage(entry.msg, plaintext, 'pending_retry');
1009
+ if (!generationCurrent())
1010
+ return;
999
1011
  client._v2SenderIKPending.delete(key);
1012
+ if (client._p2pPeerPullEnabled)
1013
+ client._delivery.schedulePeerRecovery(entry.fromAid, 0, true);
1000
1014
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
1001
1015
  continue;
1002
1016
  }
1003
1017
  const seq = Number(entry.msg.seq ?? 0);
1004
1018
  if (entry.groupId) {
1019
+ plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
1005
1020
  if (client._delivery.isSelfSentGroupMessage?.(entry.msg)) {
1006
1021
  client._v2SenderIKPending.delete(key);
1007
1022
  continue;
@@ -1011,39 +1026,72 @@ export class V2E2EECoordinator {
1011
1026
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
1012
1027
  }
1013
1028
  else {
1014
- await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
1029
+ if (client._p2pPeerPullEnabled) {
1030
+ await client._delivery.initializePeer(entry.fromAid);
1031
+ if (!generationCurrent())
1032
+ return;
1033
+ }
1034
+ const ns = client._p2pPeerPullEnabled ? client._delivery.peerNamespace(entry.fromAid) : `p2p:${client._aid ?? ''}`;
1035
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false, 'pending_retry');
1036
+ if (!generationCurrent())
1037
+ return;
1038
+ if (client._p2pPeerPullEnabled)
1039
+ client._delivery.schedulePeerRecovery(entry.fromAid, seq, false);
1015
1040
  }
1016
1041
  client._v2SenderIKPending.delete(key);
1017
1042
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
1018
1043
  }
1019
1044
  }
1020
1045
  finally {
1021
- client._v2SenderIKFetching.delete(fetchKey);
1046
+ if (generationCurrent())
1047
+ client._v2SenderIKFetching.delete(fetchKey);
1022
1048
  }
1023
1049
  }
1024
1050
  async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
1025
- const delivery = this.client._delivery;
1051
+ const client = this.client;
1052
+ const delivery = client._delivery;
1053
+ const sender = String(rawMessage.sender_aid ?? rawMessage.from_aid ?? plaintext.from ?? '');
1054
+ const ns = client._p2pPeerPullEnabled ? delivery.peerNamespace(sender) : '';
1055
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1056
+ const inlineGeneration = delivery.captureInlineGeneration?.();
1057
+ const guard = () => { if (ns)
1058
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration); };
1059
+ const ack = async () => {
1060
+ guard();
1061
+ if (!ns) {
1062
+ await delivery.ackVolatileMessages([rawMessage], true, false);
1063
+ return;
1064
+ }
1065
+ await this.ackPeerV2({ sender_aid: sender, volatile_cursors: rawMessage.volatile_cursor ? [rawMessage.volatile_cursor] : [], volatile_ids: rawMessage.message_id ? [rawMessage.message_id] : [], _rpc_background: true });
1066
+ guard();
1067
+ if (rawMessage.volatile_cursor)
1068
+ delivery.setPeerVolatileCursor(sender, String(rawMessage.volatile_cursor));
1069
+ };
1026
1070
  const claim = delivery.beginVolatileDelivery(rawMessage);
1027
1071
  if (claim === 'inflight')
1028
1072
  return;
1029
1073
  if (claim === 'seen') {
1030
- await delivery.ackVolatileMessages([rawMessage], true, false);
1074
+ await ack();
1031
1075
  return;
1032
1076
  }
1033
1077
  try {
1034
1078
  const publicMessage = delivery.publicVolatileMessage(plaintext);
1035
- for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
1079
+ for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist', 'need_render']) {
1036
1080
  if (key in rawMessage)
1037
1081
  publicMessage[key] = rawMessage[key];
1038
1082
  }
1039
1083
  const appEvent = delivery.p2pAppEventForMessage(publicMessage);
1040
- await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
1084
+ if (ns)
1085
+ await client._publishAppEvent(appEvent.event, appEvent.payload, source, ns);
1086
+ else
1087
+ await client._publishAppEvent(appEvent.event, appEvent.payload, source);
1088
+ guard();
1041
1089
  delivery.markVolatileSeen(rawMessage);
1042
1090
  }
1043
1091
  finally {
1044
1092
  delivery.finishVolatileDelivery(rawMessage);
1045
1093
  }
1046
- await delivery.ackVolatileMessages([rawMessage], true, false);
1094
+ await ack();
1047
1095
  }
1048
1096
  async sendV2(to, payload, opts) {
1049
1097
  const client = this.client;
@@ -1061,6 +1109,7 @@ export class V2E2EECoordinator {
1061
1109
  messageId: opts?.messageId,
1062
1110
  timestamp: opts?.timestamp,
1063
1111
  deviceId: targetDeviceId || undefined,
1112
+ needRender: opts?.needRender !== false,
1064
1113
  protectedHeaders: opts?.protectedHeaders,
1065
1114
  context: opts?.context,
1066
1115
  useCache,
@@ -1082,6 +1131,7 @@ export class V2E2EECoordinator {
1082
1131
  payload,
1083
1132
  message_id: opts?.messageId,
1084
1133
  timestamp: opts?.timestamp,
1134
+ need_render: opts?.needRender !== false,
1085
1135
  protected_headers: opts?.protectedHeaders,
1086
1136
  context: opts?.context,
1087
1137
  }, result, true);
@@ -1097,6 +1147,7 @@ export class V2E2EECoordinator {
1097
1147
  payload,
1098
1148
  message_id: opts?.messageId,
1099
1149
  timestamp: opts?.timestamp,
1150
+ need_render: opts?.needRender !== false,
1100
1151
  protected_headers: opts?.protectedHeaders,
1101
1152
  context: opts?.context,
1102
1153
  }, result, true);
@@ -1112,13 +1163,14 @@ export class V2E2EECoordinator {
1112
1163
  history_error: { code: 'decrypt_failed', message: reason },
1113
1164
  };
1114
1165
  }
1115
- ensureDeliveryCurrent(ns, generation, inlineGeneration) {
1166
+ ensureDeliveryCurrent(ns, generation, inlineGeneration, namespaceGeneration) {
1116
1167
  if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
1117
1168
  || (inlineGeneration !== undefined && !this.client._delivery.isInlineGenerationCurrent(inlineGeneration))
1169
+ || (namespaceGeneration !== undefined && this.client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
1118
1170
  || this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
1119
1171
  throw new StateError('pull invalidated');
1120
1172
  }
1121
- async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
1173
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false, applicationPage, deferredEffects) {
1122
1174
  const client = this.client;
1123
1175
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
1124
1176
  let plaintext = null;
@@ -1144,13 +1196,16 @@ export class V2E2EECoordinator {
1144
1196
  else if (isJsonObject(payload)
1145
1197
  && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1146
1198
  msg = { ...msg, envelope_json: JSON.stringify(payload) };
1147
- plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1199
+ plaintext = deferredEffects
1200
+ ? await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source, deferredEffects)
1201
+ : await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
1148
1202
  }
1149
1203
  }
1150
1204
  else {
1151
1205
  plaintext = source === 'inline_push'
1152
1206
  ? await client._decryptV2Message(msg, false, false, false, false)
1153
- : await client._decryptV2Message(msg, false);
1207
+ : deferredEffects ? await client._decryptV2Message(msg, false, true, true, true, undefined, false, source, deferredEffects)
1208
+ : await client._decryptV2Message(msg, false);
1154
1209
  }
1155
1210
  if (!plaintext) {
1156
1211
  if (history)
@@ -1164,15 +1219,18 @@ export class V2E2EECoordinator {
1164
1219
  if (publish) {
1165
1220
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1166
1221
  if (orderedPublish) {
1167
- await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload, source);
1222
+ await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload, source, undefined, applicationPage);
1223
+ }
1224
+ else if (event.event === 'message.recalled' && typeof client._delivery.publishMessageRecallTombstone === 'function') {
1225
+ await client._delivery.publishMessageRecallTombstone(seq, plaintext, source, applicationPage);
1168
1226
  }
1169
1227
  else {
1170
- await client._publishAppEvent(event.event, event.payload, source);
1228
+ await client._publishAppEvent(event.event, event.payload, source, applicationPage?.namespace ?? '', undefined, applicationPage);
1171
1229
  }
1172
1230
  }
1173
1231
  return plaintext;
1174
1232
  }
1175
- async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
1233
+ async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false, applicationPage) {
1176
1234
  const client = this.client;
1177
1235
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
1178
1236
  let plaintext = null;
@@ -1190,10 +1248,13 @@ export class V2E2EECoordinator {
1190
1248
  };
1191
1249
  if (publish) {
1192
1250
  if (orderedPublish) {
1193
- await client._delivery.publishOrderedMessage('group.message_recalled', `group:${groupId}`, seq, recallPayload, source);
1251
+ await client._delivery.publishOrderedMessage('group.message_recalled', `group:${groupId}`, seq, recallPayload, source, undefined, applicationPage);
1252
+ }
1253
+ else if (typeof client._delivery.publishGroupRecallTombstone === 'function') {
1254
+ await client._delivery.publishGroupRecallTombstone(groupId, seq, recallPayload, source, applicationPage);
1194
1255
  }
1195
1256
  else {
1196
- await client._publishAppEvent('group.message_recalled', recallPayload, source);
1257
+ await client._publishAppEvent('group.message_recalled', recallPayload, source, applicationPage?.namespace ?? '', undefined, applicationPage);
1197
1258
  }
1198
1259
  }
1199
1260
  return recallPayload;
@@ -1244,15 +1305,16 @@ export class V2E2EECoordinator {
1244
1305
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1245
1306
  return null;
1246
1307
  }
1308
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
1247
1309
  if (publish && !history && client._delivery.isSelfSentGroupMessage?.(msg)) {
1248
1310
  return plaintext;
1249
1311
  }
1250
1312
  if (publish) {
1251
1313
  if (orderedPublish) {
1252
- await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext, source);
1314
+ await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext, source, undefined, applicationPage);
1253
1315
  }
1254
1316
  else {
1255
- await client._publishAppEvent('group.message_created', plaintext, source);
1317
+ await client._publishAppEvent('group.message_created', plaintext, source, applicationPage?.namespace ?? '', undefined, applicationPage);
1256
1318
  }
1257
1319
  }
1258
1320
  return plaintext;
@@ -1282,11 +1344,19 @@ export class V2E2EECoordinator {
1282
1344
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
1283
1345
  const page = validateTailPage(result, afterSeq, limit);
1284
1346
  const decoded = [];
1285
- for (const msg of page.messages) {
1286
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1287
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1288
- if (plaintext)
1289
- decoded.push(plaintext);
1347
+ const applicationPage = client._delivery.beginApplicationPage?.(ns, 'tail');
1348
+ try {
1349
+ for (const msg of page.messages) {
1350
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1351
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish, applicationPage);
1352
+ if (plaintext)
1353
+ decoded.push(plaintext);
1354
+ }
1355
+ await client._delivery.flushApplicationPage?.(applicationPage);
1356
+ }
1357
+ catch (error) {
1358
+ client._delivery.abortApplicationPage?.(applicationPage, error);
1359
+ throw error;
1290
1360
  }
1291
1361
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1292
1362
  if (ns) {
@@ -1294,7 +1364,7 @@ export class V2E2EECoordinator {
1294
1364
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1295
1365
  };
1296
1366
  if (deferPublish)
1297
- client._delivery.deferTailWindow(ns, proof);
1367
+ client._delivery.deferTailWindow(ns, proof, applicationPage);
1298
1368
  else {
1299
1369
  client._seqTracker.commitTailWindow(ns, proof);
1300
1370
  client._persistSeq(ns);
@@ -1308,6 +1378,491 @@ export class V2E2EECoordinator {
1308
1378
  client._delivery.onPullWorkSettled?.();
1309
1379
  return { ...result, messages: decoded, raw_count: page.messages.length };
1310
1380
  }
1381
+ peerPageChains = new Map();
1382
+ peerRequestId = 0;
1383
+ async serializePeerPage(ns, operation) {
1384
+ const previous = this.peerPageChains.get(ns) ?? Promise.resolve();
1385
+ let release;
1386
+ const current = new Promise((resolve) => { release = resolve; });
1387
+ this.peerPageChains.set(ns, current);
1388
+ await previous.catch(() => undefined);
1389
+ try {
1390
+ return await operation();
1391
+ }
1392
+ finally {
1393
+ release();
1394
+ if (this.peerPageChains.get(ns) === current)
1395
+ this.peerPageChains.delete(ns);
1396
+ }
1397
+ }
1398
+ hasPeerPageActivity(ns) {
1399
+ return this.peerPageChains.has(ns);
1400
+ }
1401
+ validatePeerPage(result, sender, after, limit, tail, committedAck, preparing = false) {
1402
+ if (result.cursor_scope !== 'peer' || result.sender_aid !== sender)
1403
+ throw new ValidationError('peer response scope mismatch');
1404
+ const scan = strictWindowSeq(result.scan_after_seq ?? (tail ? after : undefined), 'scan_after_seq');
1405
+ const covered = strictWindowSeq(result.covered_through_seq, 'covered_through_seq');
1406
+ const next = strictWindowSeq(result.next_after_seq ?? (tail ? covered : undefined), 'next_after_seq');
1407
+ const head = strictWindowSeq(result.head_seq, 'head_seq');
1408
+ const safeStart = Math.max(after, committedAck, strictWindowSeq(result.retention_floor_seq ?? 0, 'retention_floor_seq'));
1409
+ if (scan < after || (!preparing && scan > safeStart) || covered < scan || covered > head || next !== covered)
1410
+ throw new ValidationError('invalid peer coverage proof');
1411
+ if (!tail && (typeof result.has_more !== 'boolean' || (result.persistent_has_more !== undefined && typeof result.persistent_has_more !== 'boolean')
1412
+ || (result.volatile_has_more !== undefined && typeof result.volatile_has_more !== 'boolean')
1413
+ || (!tail && covered < head && result.persistent_has_more !== true)
1414
+ || (result.persistent_has_more !== undefined && result.has_more !== (result.persistent_has_more === true || result.volatile_has_more === true))))
1415
+ throw new ValidationError('invalid peer continuation proof');
1416
+ const start = tail ? strictWindowSeq(result.window_start_seq, 'window_start_seq') : scan + 1;
1417
+ if (tail && (covered !== head || start > head + 1 || start <= scan))
1418
+ throw new ValidationError('invalid peer tail proof');
1419
+ if (!Array.isArray(result.messages) || result.messages.length > limit)
1420
+ throw new ValidationError('invalid peer page limit');
1421
+ const messages = result.messages;
1422
+ const seen = new Map();
1423
+ for (const row of messages) {
1424
+ if (String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== sender)
1425
+ throw new ValidationError('peer message sender mismatch');
1426
+ if (this.client._delivery.isVolatileMessage?.(row) || row.storage === 'volatile')
1427
+ continue;
1428
+ const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
1429
+ if (seq <= scan || seq > covered || (tail && seq < start))
1430
+ throw new ValidationError('peer message outside coverage');
1431
+ const serialized = JSON.stringify(row);
1432
+ if (seen.has(seq) && seen.get(seq) !== serialized)
1433
+ throw new ValidationError('conflicting peer delivery');
1434
+ seen.set(seq, serialized);
1435
+ }
1436
+ return { messages, covered, head, start };
1437
+ }
1438
+ async pullPeerV2(sender, params = {}, background = false, budget = { remaining: 100 }, options = {}) {
1439
+ const client = this.client;
1440
+ if (!sender)
1441
+ throw new ValidationError('sender_aid is required');
1442
+ if (params.cursor_scope !== undefined && params.cursor_scope !== 'peer')
1443
+ throw new ValidationError('cursor_scope must equal peer');
1444
+ const limit = strictWindowSeq(params.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, 'limit', true);
1445
+ if (limit > 200)
1446
+ throw new ValidationError('limit must be <= 200');
1447
+ if (params.window_mode !== undefined && params.window_mode !== 'tail')
1448
+ throw new ValidationError('window_mode must equal tail');
1449
+ if (params.window_mode === 'tail' && (params.force === true || params.ack_up_to_seq !== undefined))
1450
+ throw new ValidationError('Tail does not accept force or ack_up_to_seq');
1451
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1452
+ const inlineGeneration = client._delivery.captureInlineGeneration?.();
1453
+ await client._delivery.initializePeer(sender);
1454
+ const ns = client._delivery.peerNamespace(sender);
1455
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1456
+ const requestId = ++this.peerRequestId;
1457
+ const messages = [];
1458
+ let prefetched = null;
1459
+ const cancelled = new AbortController();
1460
+ const reservations = new Set();
1461
+ const preparedApplicationPages = new Set();
1462
+ let durableAck = client._delivery.peerDurableAck?.(ns) ?? client._seqTracker.getContiguousSeq(ns);
1463
+ let confirmedAck = 0;
1464
+ const fetchPage = async (after, cursor, isTail) => {
1465
+ const reservation = await client._dispatcher?._reserveMessageCapacity?.(limit, cancelled.signal);
1466
+ if (reservation)
1467
+ reservations.add(reservation);
1468
+ try {
1469
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1470
+ if (cancelled.signal.aborted)
1471
+ throw new StateError('pull invalidated');
1472
+ const request = { cursor_scope: 'peer', sender_aid: sender, after_seq: after, limit,
1473
+ ...(isTail ? { window_mode: 'tail' } : {}), ...(params.force === true ? { force: true } : {}),
1474
+ ...(!isTail && cursor ? { after_volatile_cursor: cursor } : {}), ...(background ? { _rpc_background: true } : { _rpc_foreground: true }),
1475
+ };
1476
+ const key = `${ns}|peer=${background ? 'background' : 'application'}|request=${requestId}|after=${after}|tail=${isTail}|force=${params.force === true}|volatile=${cursor}`;
1477
+ const raw = await client._runPullSerialized(key, async () => {
1478
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1479
+ if (cancelled.signal.aborted)
1480
+ throw new StateError('pull invalidated');
1481
+ if (!isTail && params.force !== true && durableAck > 0)
1482
+ request.ack_up_to_seq = durableAck;
1483
+ return await client._callRawV2Rpc('message.v2.pull', request, key);
1484
+ }, background);
1485
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1486
+ if (cancelled.signal.aborted)
1487
+ throw new StateError('pull invalidated');
1488
+ return { raw, reservation };
1489
+ }
1490
+ catch (error) {
1491
+ reservation?.release();
1492
+ if (reservation)
1493
+ reservations.delete(reservation);
1494
+ throw error;
1495
+ }
1496
+ };
1497
+ const decodeRow = async (row, source, deferredEffects) => {
1498
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1499
+ if (cancelled.signal.aborted)
1500
+ throw new StateError('pull invalidated');
1501
+ if (row.version !== 'v1' || this.isEncryptedEnvelopePayload(row.legacy_v1?.payload ?? row.payload)) {
1502
+ await client._ensureV2SessionReady?.('message.pull');
1503
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1504
+ }
1505
+ const plaintext = await this.decodeP2PWindowMessage(row, false, false, source, false, undefined, deferredEffects);
1506
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1507
+ if (!plaintext)
1508
+ throw new E2EEError(`peer delivery could not be decrypted: seq=${Number(row.seq)}`);
1509
+ plaintext.need_render = row.need_render !== false;
1510
+ return plaintext;
1511
+ };
1512
+ const startNextRaw = (raw, after, cursor) => {
1513
+ const proof = this.validatePeerPage(raw, sender, after, limit, false, durableAck, true);
1514
+ const persistentMore = raw.persistent_has_more === true || (raw.persistent_has_more === undefined && raw.has_more === true && raw.volatile_has_more !== true);
1515
+ if (params.force === true || !persistentMore || raw.volatile_has_more === true || proof.covered <= after || budget.remaining <= 0
1516
+ || proof.messages.some((row) => client._delivery.isVolatileMessage?.(row) || row.storage === 'volatile'))
1517
+ return undefined;
1518
+ budget.remaining -= 1;
1519
+ return { after: proof.covered, cursor, response: fetchPage(proof.covered, cursor, false).then((result) => ({ result }), (error) => ({ error })) };
1520
+ };
1521
+ const preparePage = async (pending) => {
1522
+ const outcome = await pending.response;
1523
+ if (outcome.error)
1524
+ throw outcome.error;
1525
+ const page = outcome.result;
1526
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1527
+ if (cancelled.signal.aborted)
1528
+ throw new StateError('pull invalidated');
1529
+ const proof = this.validatePeerPage(page.raw, sender, pending.after, limit, false, durableAck, true);
1530
+ const applicationPage = client._delivery.beginApplicationPage?.(ns, 'pull', page.reservation);
1531
+ if (applicationPage)
1532
+ preparedApplicationPages.add(applicationPage);
1533
+ const next = startNextRaw(page.raw, pending.after, pending.cursor);
1534
+ const decoded = new Map();
1535
+ const deferredEffects = [];
1536
+ try {
1537
+ for (const row of [...proof.messages].sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0))) {
1538
+ if (client._delivery.isVolatileMessage?.(row) || row.storage === 'volatile' || decoded.has(Number(row.seq)))
1539
+ continue;
1540
+ decoded.set(Number(row.seq), await decodeRow(row, 'pull', deferredEffects));
1541
+ }
1542
+ }
1543
+ catch (error) {
1544
+ return { ...page, decoded, next, applicationPage, deferredEffects, failure: { error } };
1545
+ }
1546
+ return { ...page, decoded, next, applicationPage, deferredEffects };
1547
+ };
1548
+ let complete = false;
1549
+ let head = 0;
1550
+ let explicitAfter = params.after_seq === undefined ? undefined : strictWindowSeq(params.after_seq, 'after_seq');
1551
+ let explicitVolatileCursor = params.after_volatile_cursor === undefined ? undefined : String(params.after_volatile_cursor);
1552
+ let tail = params.window_mode === 'tail' || (params.window_mode === undefined && params.force !== true && await client._delivery.ownPeerType?.() === 'human'
1553
+ && client._seqTracker.getSyncState(ns).head <= client._seqTracker.getContiguousSeq(ns));
1554
+ let failed = false;
1555
+ try {
1556
+ while ((budget.remaining > 0 || prefetched !== null) && !complete) {
1557
+ const page = await this.serializePeerPage(ns, async () => {
1558
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1559
+ const cached = prefetched;
1560
+ prefetched = null;
1561
+ if (!cached && budget.remaining <= 0)
1562
+ return null;
1563
+ if (!cached)
1564
+ budget.remaining -= 1;
1565
+ const oldAck = client._delivery.peerDurableAck?.(ns) ?? client._seqTracker.getContiguousSeq(ns);
1566
+ durableAck = oldAck;
1567
+ const after = cached?.after ?? explicitAfter ?? oldAck;
1568
+ if (!cached && params.force !== true && explicitAfter !== undefined && explicitAfter > oldAck)
1569
+ throw new ValidationError('after_seq exceeds committed peer cursor; use force for explicit rereads');
1570
+ const volatileCursor = cached?.cursor ?? String(explicitVolatileCursor ?? client._delivery.peerVolatileCursor?.(sender) ?? '');
1571
+ let prepared;
1572
+ try {
1573
+ if (cached) {
1574
+ const outcome = await cached.response;
1575
+ if (outcome.error)
1576
+ throw outcome.error;
1577
+ prepared = outcome.result;
1578
+ }
1579
+ else
1580
+ prepared = { ...await fetchPage(after, volatileCursor, tail), decoded: new Map() };
1581
+ }
1582
+ catch (error) {
1583
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1584
+ if (volatileCursor && String(error).toLowerCase().includes('volatile cursor epoch mismatch')) {
1585
+ explicitVolatileCursor = undefined;
1586
+ client._delivery.setPeerVolatileCursor(sender, '');
1587
+ return { retry: true };
1588
+ }
1589
+ throw error;
1590
+ }
1591
+ try {
1592
+ const raw = prepared.raw;
1593
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1594
+ const proof = this.validatePeerPage(raw, sender, after, limit, tail, tail || params.force === true ? after : oldAck);
1595
+ for (const effect of prepared.deferredEffects ?? []) {
1596
+ await effect();
1597
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1598
+ }
1599
+ if (prepared.failure)
1600
+ throw prepared.failure.error;
1601
+ const serverAck = safeNonNegativeSeq(raw.server_ack_seq ?? raw.peer_ack_seq);
1602
+ if (serverAck !== null && params.force !== true && !tail) {
1603
+ confirmedAck = Math.max(confirmedAck, Math.min(serverAck, durableAck));
1604
+ this.clearForwardAck(ns, confirmedAck);
1605
+ }
1606
+ const persistentMore = raw.persistent_has_more === true || (raw.persistent_has_more === undefined && raw.has_more === true && raw.volatile_has_more !== true);
1607
+ const volatileMore = raw.volatile_has_more === true;
1608
+ const nextRaw = prepared.next ?? (!tail ? startNextRaw(raw, after, volatileCursor) : undefined);
1609
+ if (nextRaw) {
1610
+ prefetched = { after: nextRaw.after, cursor: nextRaw.cursor, response: preparePage(nextRaw).then((result) => ({ result }), (error) => ({ error })) };
1611
+ }
1612
+ const decoded = [];
1613
+ const seen = new Set();
1614
+ const volatileRows = [];
1615
+ const applicationPage = prepared.applicationPage ?? client._delivery.beginApplicationPage?.(ns, tail ? 'tail' : 'pull', prepared.reservation);
1616
+ try {
1617
+ await client._withPullResponseProcessing(ns, async () => {
1618
+ for (const row of [...proof.messages].sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0))) {
1619
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1620
+ if (client._delivery.isVolatileMessage?.(row) || row.storage === 'volatile') {
1621
+ volatileRows.push(row);
1622
+ continue;
1623
+ }
1624
+ const seq = Number(row.seq);
1625
+ if (seen.has(seq))
1626
+ continue;
1627
+ seen.add(seq);
1628
+ const plaintext = prepared.decoded.get(seq) ?? await decodeRow(row, tail ? 'tail' : 'pull');
1629
+ const event = client._delivery.p2pAppEventForMessage(plaintext);
1630
+ if (!client._delivery.isSelfSentP2PMessage?.(plaintext)) {
1631
+ if (event.event === 'message.recalled')
1632
+ await client._delivery.publishMessageRecallTombstone(seq, plaintext, tail ? 'tail' : 'pull', applicationPage);
1633
+ else
1634
+ await client._publishAppEvent(event.event, event.payload, tail ? 'tail' : 'pull', ns, undefined, applicationPage);
1635
+ }
1636
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1637
+ decoded.push(plaintext);
1638
+ }
1639
+ if (volatileRows.length > 0)
1640
+ decoded.push(...await this.publishPeerVolatileMessages(sender, volatileRows, background, generation, inlineGeneration, applicationPage));
1641
+ });
1642
+ await client._delivery.flushApplicationPage?.(applicationPage);
1643
+ }
1644
+ catch (error) {
1645
+ client._delivery.abortApplicationPage?.(applicationPage, error);
1646
+ throw error;
1647
+ }
1648
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1649
+ if (params.force !== true) {
1650
+ const snapshot = client._seqTracker.snapshotNamespace(ns);
1651
+ try {
1652
+ if (tail && proof.start > oldAck + 1) {
1653
+ client._seqTracker.commitTailWindow(ns, { windowStartSeq: proof.start, headSeq: proof.head, coveredThroughSeq: proof.covered, pageLimit: limit, sparse: true, messageCount: seen.size });
1654
+ }
1655
+ else
1656
+ client._seqTracker.forceContiguousSeq(ns, Math.max(oldAck, proof.covered));
1657
+ await client._commitSeqTrackerState(ns);
1658
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1659
+ durableAck = client._delivery.peerDurableAck?.(ns) ?? client._seqTracker.getContiguousSeq(ns);
1660
+ if (durableAck > confirmedAck)
1661
+ this.recordForwardAck(ns, durableAck);
1662
+ }
1663
+ catch (error) {
1664
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
1665
+ && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
1666
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
1667
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
1668
+ throw error;
1669
+ }
1670
+ const ack = client._seqTracker.getContiguousSeq(ns);
1671
+ if (ack > 0) {
1672
+ if (!tail)
1673
+ client._delivery.consumePeerBackgroundPullPriority?.(ns, proof.covered, persistentMore && typeof raw.next_pending_seq === 'number' && Number.isSafeInteger(raw.next_pending_seq)
1674
+ && raw.next_pending_seq > proof.covered && raw.next_pending_seq <= proof.head ? raw.next_pending_seq : undefined);
1675
+ }
1676
+ }
1677
+ return { decoded, proof, persistentMore, volatileMore, after, tail, volatileCursor,
1678
+ nextVolatileCursor: String(client._delivery.peerVolatileCursor?.(sender) ?? '') };
1679
+ }
1680
+ finally {
1681
+ if (prepared.applicationPage) {
1682
+ client._delivery.abortApplicationPage?.(prepared.applicationPage, new StateError('peer page abandoned'));
1683
+ preparedApplicationPages.delete(prepared.applicationPage);
1684
+ }
1685
+ prepared.reservation?.release();
1686
+ if (prepared.reservation)
1687
+ reservations.delete(prepared.reservation);
1688
+ }
1689
+ });
1690
+ if (!page)
1691
+ break;
1692
+ if (page.retry)
1693
+ continue;
1694
+ explicitVolatileCursor = undefined;
1695
+ if (options.retainMessages !== false)
1696
+ messages.push(...page.decoded);
1697
+ head = Math.max(head, page.proof.head);
1698
+ if (page.tail) {
1699
+ tail = false;
1700
+ explicitAfter = undefined;
1701
+ if (params.window_mode === 'tail') {
1702
+ complete = client._seqTracker.getContiguousSeq(ns) >= page.proof.head;
1703
+ break;
1704
+ }
1705
+ }
1706
+ else {
1707
+ explicitAfter = page.proof.covered;
1708
+ complete = !page.persistentMore && !page.volatileMore;
1709
+ if (!complete && page.proof.covered <= page.after && page.nextVolatileCursor === page.volatileCursor)
1710
+ break;
1711
+ }
1712
+ }
1713
+ }
1714
+ catch (error) {
1715
+ failed = true;
1716
+ throw error;
1717
+ }
1718
+ finally {
1719
+ cancelled.abort();
1720
+ for (const applicationPage of preparedApplicationPages)
1721
+ client._delivery.abortApplicationPage?.(applicationPage, new StateError('pull invalidated'));
1722
+ preparedApplicationPages.clear();
1723
+ for (const reservation of reservations)
1724
+ reservation.release();
1725
+ reservations.clear();
1726
+ if (params.force !== true && this.pendingForwardAck(ns, durableAck) > confirmedAck) {
1727
+ try {
1728
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1729
+ await this.ackPeerV2({ cursor_scope: 'peer', sender_aid: sender, up_to_seq: durableAck, ...(background ? { _rpc_background: true } : {}) });
1730
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1731
+ this.clearForwardAck(ns, durableAck);
1732
+ }
1733
+ catch (error) {
1734
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
1735
+ && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
1736
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
1737
+ client._delivery.schedulePeerRecovery?.(sender, head, true);
1738
+ if (!failed)
1739
+ throw error;
1740
+ }
1741
+ }
1742
+ }
1743
+ const ack = client._seqTracker.getContiguousSeq(ns);
1744
+ if (!complete && params.force !== true)
1745
+ client._delivery.schedulePeerRecovery(sender, head, true);
1746
+ return { sender_aid: sender, messages, complete, ack_seq: ack, head_seq: head };
1747
+ }
1748
+ async publishPeerVolatileMessages(sender, rows, background, generation, inlineGeneration, applicationPage) {
1749
+ const client = this.client;
1750
+ const ns = client._delivery.peerNamespace(sender);
1751
+ const published = [];
1752
+ const processed = [];
1753
+ for (const row of rows) {
1754
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1755
+ await client._delivery.waitVolatileApplicationDelivery?.(row, applicationPage);
1756
+ const claim = client._delivery.beginVolatileDelivery(row, applicationPage);
1757
+ if (claim === 'inflight')
1758
+ throw new StateError('volatile delivery in progress', { retryable: true });
1759
+ try {
1760
+ let plaintext;
1761
+ if (client._isEncryptedPushMessage(row)) {
1762
+ await client._ensureV2SessionReady?.('message.pull');
1763
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1764
+ plaintext = await client._decryptV2Message(row, false);
1765
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1766
+ }
1767
+ else {
1768
+ const legacy = isJsonObject(row.legacy_v1) ? row.legacy_v1 : {};
1769
+ plaintext = { ...row, from: row.from_aid ?? row.from, payload: legacy.payload ?? row.payload, encrypted: false };
1770
+ }
1771
+ if (!plaintext)
1772
+ throw new E2EEError('volatile peer message could not be decrypted');
1773
+ plaintext.storage = 'volatile';
1774
+ plaintext.need_render = row.need_render !== false;
1775
+ const message = client._delivery.publicVolatileMessage(plaintext);
1776
+ if (claim !== 'seen') {
1777
+ await client._publishAppEvent('message.received', message, 'pull', client._delivery.peerNamespace(sender), undefined, applicationPage);
1778
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1779
+ client._delivery.markVolatileSeen(row, applicationPage);
1780
+ client._delivery.finishVolatileDelivery(row, applicationPage);
1781
+ }
1782
+ processed.push(row);
1783
+ published.push(message);
1784
+ }
1785
+ catch (error) {
1786
+ if (claim !== 'seen')
1787
+ client._delivery.finishVolatileDelivery(row, applicationPage);
1788
+ throw error;
1789
+ }
1790
+ }
1791
+ const cursors = processed.map((row) => String(row.volatile_cursor ?? '')).filter(Boolean);
1792
+ const ids = processed.map((row) => String(row.message_id ?? '')).filter(Boolean);
1793
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1794
+ if (cursors.length || ids.length) {
1795
+ const ack = async () => { await this.ackPeerV2({ sender_aid: sender, volatile_cursors: cursors, volatile_ids: ids, ...(background ? { _rpc_background: true } : {}) }); };
1796
+ if (applicationPage?.batch)
1797
+ client._delivery.deferApplicationAfterDelivery(applicationPage, ack);
1798
+ else
1799
+ await ack();
1800
+ }
1801
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1802
+ if (cursors.length)
1803
+ client._delivery.setPeerVolatileCursor(sender, cursors[cursors.length - 1], applicationPage);
1804
+ return published;
1805
+ }
1806
+ async ackPeerV2(params) {
1807
+ const client = this.client;
1808
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1809
+ const inlineGeneration = client._delivery.captureInlineGeneration?.();
1810
+ const session = client._v2Session;
1811
+ const sender = String(params.sender_aid ?? params.peer_aid ?? '').trim();
1812
+ if (!sender || (params.cursor_scope !== undefined && params.cursor_scope !== 'peer'))
1813
+ throw new ValidationError('peer ACK requires sender_aid and cursor_scope=peer');
1814
+ const ns = client._delivery.peerNamespace(sender);
1815
+ const ack = client._delivery.peerDurableAck?.(ns) ?? client._seqTracker.getContiguousSeq(ns);
1816
+ const requested = params.up_to_seq ?? params.seq ?? ack;
1817
+ const seq = strictWindowSeq(requested, 'up_to_seq');
1818
+ const target = Math.min(seq, ack);
1819
+ const result = await client._callRawV2Rpc('message.v2.ack', { cursor_scope: 'peer', sender_aid: sender,
1820
+ ...(target > 0 ? { up_to_seq: target } : {}), ...(Array.isArray(params.volatile_cursors) ? { volatile_cursors: params.volatile_cursors } : {}),
1821
+ ...(Array.isArray(params.volatile_ids) ? { volatile_ids: params.volatile_ids } : {}),
1822
+ ...(params._rpc_background ? { _rpc_background: true } : { _rpc_foreground: true }),
1823
+ });
1824
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1825
+ const actual = safeNonNegativeSeq(result.peer_ack_seq ?? result.effective_ack_seq ?? result.server_ack_seq);
1826
+ if (target > 0 && (actual === null || actual < target))
1827
+ throw new E2EEError('peer ACK watermark below target');
1828
+ if (actual !== null)
1829
+ this.clearForwardAck(ns, actual);
1830
+ const safeDevice = safeNonNegativeSeq(result.device_safe_ack_seq);
1831
+ if (safeDevice !== null && safeDevice > 0 && client._v2Session === session)
1832
+ await session?.maybeDestroyOldSPKs(safeDevice);
1833
+ return { ...result, ack_seq: actual ?? 0 };
1834
+ }
1835
+ async historyPeerV2(params) {
1836
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
1837
+ const inlineGeneration = this.client._delivery.captureInlineGeneration?.();
1838
+ const sender = String(params.sender_aid ?? params.peer_aid ?? '').trim();
1839
+ if (!sender || (params.cursor_scope !== undefined && params.cursor_scope !== 'peer'))
1840
+ throw new ValidationError('peer History requires sender_aid');
1841
+ const before = strictWindowSeq(params.before_seq, 'before_seq', true);
1842
+ const limit = strictWindowSeq(params.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, 'limit', true);
1843
+ if (limit > 200)
1844
+ throw new ValidationError('limit must be <= 200');
1845
+ const request = { cursor_scope: 'peer', sender_aid: sender, before_seq: before, limit, ...(params.snapshot_head_seq !== undefined ? { snapshot_head_seq: params.snapshot_head_seq } : {}) };
1846
+ const ns = this.client._delivery.peerNamespace(sender);
1847
+ const result = await this.client._runPullSerialized(`${ns}|history|before=${before}|limit=${limit}|snapshot=${String(params.snapshot_head_seq ?? '')}`, () => this.client._callRawV2Rpc('message.history', request), false);
1848
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1849
+ if (result.cursor_scope !== 'peer' || result.sender_aid !== sender)
1850
+ throw new ValidationError('peer History scope mismatch');
1851
+ const rows = validateHistoryPage(result, before);
1852
+ if (rows.length > limit || rows.some((row) => String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== sender))
1853
+ throw new ValidationError('invalid peer History messages');
1854
+ const messages = [];
1855
+ for (const row of rows) {
1856
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1857
+ const value = await this.decodeP2PWindowMessage(row, false, true, 'history');
1858
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1859
+ if (value) {
1860
+ value.need_render = row.need_render !== false;
1861
+ messages.push(value);
1862
+ }
1863
+ }
1864
+ return { ...result, messages };
1865
+ }
1311
1866
  async historyV2Internal(params) {
1312
1867
  const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
1313
1868
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
@@ -1379,13 +1934,13 @@ export class V2E2EECoordinator {
1379
1934
  let pageCount = 0;
1380
1935
  let lastAutoAckSeq = 0;
1381
1936
  let pendingAckSeq = deferredForwardAck;
1937
+ let committedAckSeq = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1382
1938
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1383
1939
  let pipelineRunning = false;
1384
1940
  let pipelineError = null;
1385
- const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1941
+ let pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1386
1942
  let pipelineProducer = null;
1387
1943
  let pipelineStopped = false;
1388
- let pipelineResume = null;
1389
1944
  const startPipeline = (startAfterSeq, startVolatileCursor) => {
1390
1945
  if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
1391
1946
  return;
@@ -1396,30 +1951,32 @@ export class V2E2EECoordinator {
1396
1951
  for (let produced = pageCount; produced < maxPages; produced += 1) {
1397
1952
  if (pipelineStopped)
1398
1953
  break;
1954
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1399
1955
  const requestedVolatileCursor = producerVolatileCursor;
1956
+ const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
1957
+ const inlineAckGeneration = ackUpToSeq > 0
1958
+ ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null : null;
1400
1959
  const result = await client._callRawV2Rpc('message.v2.pull', {
1401
1960
  after_seq: producerAfterSeq,
1402
1961
  limit,
1403
1962
  ...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
1404
1963
  ...(opts?.force ? { force: true } : {}),
1964
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1405
1965
  }, pullGateKey);
1406
1966
  if (pipelineStopped)
1407
1967
  break;
1968
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1408
1969
  const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1409
- const completed = new Promise((resolve) => { pipelineResume = resolve; });
1410
1970
  const enqueued = await pipelineQueue.put({
1411
1971
  result,
1412
1972
  afterSeq: producerAfterSeq,
1413
- ackUpToSeq: 0,
1414
- inlineAckGeneration: null,
1973
+ ackUpToSeq,
1974
+ inlineAckGeneration,
1415
1975
  requestedVolatileCursor,
1416
1976
  weight: rawPageMessages.length,
1417
1977
  }, rawPageMessages.length);
1418
1978
  if (!enqueued || pipelineStopped)
1419
1979
  break;
1420
- await completed;
1421
- if (pipelineStopped)
1422
- break;
1423
1980
  const volatileMessages = client._delivery.volatileMessages(rawPageMessages);
1424
1981
  if (volatileMessages.length > 0)
1425
1982
  break;
@@ -1454,8 +2011,6 @@ export class V2E2EECoordinator {
1454
2011
  };
1455
2012
  const stopPipeline = async () => {
1456
2013
  pipelineStopped = true;
1457
- pipelineResume?.();
1458
- pipelineResume = null;
1459
2014
  pipelineQueue.close();
1460
2015
  if (pipelineProducer) {
1461
2016
  const producer = pipelineProducer;
@@ -1466,320 +2021,333 @@ export class V2E2EECoordinator {
1466
2021
  };
1467
2022
  try {
1468
2023
  while (pageCount < maxPages) {
1469
- let pipelinePage;
1470
- if (pipelineRunning) {
1471
- pipelinePage = await pipelineQueue.take();
1472
- if (!pipelinePage) {
1473
- pipelineRunning = false;
1474
- if (pipelineError)
1475
- throw pipelineError;
1476
- break;
1477
- }
1478
- }
1479
- pageCount += 1;
1480
- const volatileCursorBeforePage = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1481
- const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1482
- const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1483
- ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1484
- : null);
1485
- let result;
1486
- if (pipelinePage) {
1487
- nextAfterSeq = pipelinePage.afterSeq;
1488
- result = pipelinePage.result;
1489
- }
1490
- else {
1491
- try {
1492
- result = await client._callRawV2Rpc('message.v2.pull', {
1493
- after_seq: nextAfterSeq,
1494
- limit,
1495
- ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1496
- ...(opts?.force ? { force: true } : {}),
1497
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1498
- }, pullGateKey);
1499
- }
1500
- catch (exc) {
1501
- const text = exc instanceof Error ? exc.message : String(exc);
1502
- if (!volatileCursor || !text.toLowerCase().includes('volatile cursor epoch mismatch'))
1503
- throw exc;
1504
- client._delivery.resetVolatilePullCursor();
1505
- volatileCursor = '';
1506
- pageCount -= 1;
1507
- continue;
1508
- }
1509
- }
1510
- if (ackUpToSeq > 0) {
1511
- const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1512
- if (actualAckSeq >= ackUpToSeq) {
1513
- if (pendingInlineGeneration !== null) {
1514
- client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2024
+ let applicationPage;
2025
+ try {
2026
+ let pipelinePage;
2027
+ if (pipelineRunning) {
2028
+ pipelinePage = await pipelineQueue.take();
2029
+ if (!pipelinePage) {
2030
+ pipelineRunning = false;
2031
+ if (pipelineError)
2032
+ throw pipelineError;
2033
+ break;
1515
2034
  }
1516
- pendingAckSeq = 0;
1517
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1518
- this.clearForwardAck(ns, ackUpToSeq);
2035
+ }
2036
+ pageCount += 1;
2037
+ const volatileCursorBeforePage = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
2038
+ const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
2039
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2040
+ ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
2041
+ : null);
2042
+ let result;
2043
+ if (pipelinePage) {
2044
+ nextAfterSeq = pipelinePage.afterSeq;
2045
+ result = pipelinePage.result;
1519
2046
  }
1520
2047
  else {
1521
- client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1522
- await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1523
- pendingAckSeq = 0;
1524
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2048
+ try {
2049
+ result = await client._callRawV2Rpc('message.v2.pull', {
2050
+ after_seq: nextAfterSeq,
2051
+ limit,
2052
+ ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
2053
+ ...(opts?.force ? { force: true } : {}),
2054
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2055
+ }, pullGateKey);
2056
+ }
2057
+ catch (exc) {
2058
+ const text = exc instanceof Error ? exc.message : String(exc);
2059
+ if (!volatileCursor || !text.toLowerCase().includes('volatile cursor epoch mismatch'))
2060
+ throw exc;
2061
+ client._delivery.resetVolatilePullCursor();
2062
+ volatileCursor = '';
2063
+ pageCount -= 1;
2064
+ continue;
2065
+ }
1525
2066
  }
1526
- }
1527
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1528
- const volatileMessages = client._delivery.volatileMessages(pageMessages);
1529
- if (volatileMessages.length > 0) {
1530
- decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1531
- volatileCursor = client._delivery.volatilePullCursor();
1532
- }
1533
- const volatileAdvanced = Boolean(volatileCursor) && volatileCursor !== volatileCursorBeforePage;
1534
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1535
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1536
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1537
- const hasMore = result.has_more === true;
1538
- const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1539
- client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
1540
- const seqs = messages
1541
- .map((msg) => Number(msg.seq ?? 0))
1542
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1543
- const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1544
- let pageTrackerSnapshot;
1545
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1546
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1547
- const deferredKeyFetches = new Map();
1548
- let blockedSeq = 0;
1549
- const pageDecryptedBefore = decrypted.length;
1550
- for (const msg of messages) {
1551
- const seq = Number(msg.seq ?? 0);
1552
- if (!Number.isFinite(seq) || seq <= 0)
1553
- continue;
1554
- const version = String(msg.version ?? 'v2');
1555
- if (version === 'v1') {
1556
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1557
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1558
- const payloadType = isJsonObject(legacyPayload)
1559
- ? String(legacyPayload.type ?? '').trim()
1560
- : '';
1561
- if (legacyPayload !== undefined && legacyPayload !== null
1562
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1563
- const v1Msg = {
1564
- message_id: String(msg.message_id ?? ''),
1565
- from: String(msg.from_aid ?? ''),
1566
- to: String(legacy.to ?? client._aid ?? ''),
1567
- seq: msg.seq,
1568
- type: String(msg.type ?? ''),
1569
- timestamp: msg.t_server,
1570
- payload: legacyPayload,
1571
- encrypted: false,
1572
- };
1573
- const direction = String(msg.direction ?? '').trim();
1574
- if (direction)
1575
- v1Msg.direction = direction;
1576
- else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1577
- v1Msg.direction = 'inbound';
2067
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2068
+ if (!opts?.force)
2069
+ client._delivery.observeBackgroundPullResponse?.(ns, nextAfterSeq, result, limit);
2070
+ if (ackUpToSeq > 0) {
2071
+ const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
2072
+ if (actualAckSeq >= ackUpToSeq) {
2073
+ if (pendingInlineGeneration !== null) {
2074
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1578
2075
  }
1579
- if (msg.sender_device_id !== undefined)
1580
- v1Msg.sender_device_id = msg.sender_device_id;
1581
- attachGatewayProximity(v1Msg, msg);
1582
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1583
- if (ns)
1584
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1585
- else
1586
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1587
- decrypted.push(v1Msg);
2076
+ if (pendingAckSeq <= ackUpToSeq)
2077
+ pendingAckSeq = 0;
2078
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2079
+ this.clearForwardAck(ns, ackUpToSeq);
1588
2080
  }
1589
- else if (isJsonObject(legacyPayload)
1590
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1591
- const deferStatus = {};
1592
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1593
- if (deferStatus.deferred) {
1594
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1595
- if (deferStatus.fromAid) {
1596
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1597
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2081
+ else {
2082
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2083
+ pendingAckSeq = Math.max(pendingAckSeq, ackUpToSeq);
2084
+ this.recordForwardAck(ns, ackUpToSeq);
2085
+ }
2086
+ }
2087
+ applicationPage = client._delivery.beginApplicationPage?.(ns);
2088
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2089
+ const volatileMessages = client._delivery.volatileMessages(pageMessages);
2090
+ if (volatileMessages.length > 0) {
2091
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true, applicationPage));
2092
+ }
2093
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2094
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2095
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2096
+ const hasMore = result.has_more === true;
2097
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
2098
+ client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
2099
+ const seqs = messages
2100
+ .map((msg) => Number(msg.seq ?? 0))
2101
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2102
+ const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
2103
+ let pageTrackerSnapshot;
2104
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
2105
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2106
+ if (!pipelineRunning && volatileMessages.length === 0 && shouldContinueForwardPage({
2107
+ rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
2108
+ hasMoreField, hasMore, serverHead, pendingAckSeq: 0,
2109
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2110
+ ? client._seqTracker.getMaxSeenSeq(ns) : pageMaxSeq,
2111
+ }))
2112
+ startPipeline(pageMaxSeq, volatileCursor);
2113
+ const deferredKeyFetches = new Map();
2114
+ let blockedSeq = 0;
2115
+ const pageDecryptedBefore = decrypted.length;
2116
+ for (const msg of messages) {
2117
+ const seq = Number(msg.seq ?? 0);
2118
+ if (!Number.isFinite(seq) || seq <= 0)
2119
+ continue;
2120
+ const version = String(msg.version ?? 'v2');
2121
+ if (version === 'v1') {
2122
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2123
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
2124
+ const payloadType = isJsonObject(legacyPayload)
2125
+ ? String(legacyPayload.type ?? '').trim()
2126
+ : '';
2127
+ if (legacyPayload !== undefined && legacyPayload !== null
2128
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2129
+ const v1Msg = {
2130
+ message_id: String(msg.message_id ?? ''),
2131
+ from: String(msg.from_aid ?? ''),
2132
+ to: String(legacy.to ?? client._aid ?? ''),
2133
+ seq: msg.seq,
2134
+ type: String(msg.type ?? ''),
2135
+ timestamp: msg.t_server,
2136
+ payload: legacyPayload,
2137
+ encrypted: false,
2138
+ };
2139
+ const direction = String(msg.direction ?? '').trim();
2140
+ if (direction)
2141
+ v1Msg.direction = direction;
2142
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
2143
+ v1Msg.direction = 'inbound';
1598
2144
  }
1599
- }
1600
- if (plaintext) {
2145
+ if (msg.sender_device_id !== undefined)
2146
+ v1Msg.sender_device_id = msg.sender_device_id;
2147
+ attachGatewayProximity(v1Msg, msg);
2148
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1601
2149
  if (ns)
1602
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
2150
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false, 'pull', undefined, applicationPage);
1603
2151
  else
1604
- await client._publishAppEvent('message.received', plaintext, 'pull');
1605
- decrypted.push(plaintext);
2152
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull', ns, undefined, applicationPage);
2153
+ decrypted.push(v1Msg);
1606
2154
  }
2155
+ else if (isJsonObject(legacyPayload)
2156
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2157
+ const deferStatus = {};
2158
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
2159
+ if (deferStatus.deferred) {
2160
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2161
+ if (deferStatus.fromAid) {
2162
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2163
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2164
+ }
2165
+ }
2166
+ if (plaintext) {
2167
+ if (ns)
2168
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false, 'pull', undefined, applicationPage);
2169
+ else
2170
+ await client._publishAppEvent('message.received', plaintext, 'pull', ns, undefined, applicationPage);
2171
+ decrypted.push(plaintext);
2172
+ }
2173
+ }
2174
+ else {
2175
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
2176
+ }
2177
+ continue;
2178
+ }
2179
+ if (version !== 'v2') {
2180
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
2181
+ continue;
2182
+ }
2183
+ const msgSpkId = String(msg.spk_id ?? '');
2184
+ if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
2185
+ client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
2186
+ }
2187
+ const deferStatus = {};
2188
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2189
+ if (deferStatus.deferred) {
2190
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2191
+ }
2192
+ if (deferStatus.deferred && deferStatus.fromAid) {
2193
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2194
+ deferredKeyFetches.set(key, {
2195
+ fromAid: deferStatus.fromAid,
2196
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2197
+ groupId: deferStatus.groupId ?? '',
2198
+ });
2199
+ }
2200
+ if (plaintext === null)
2201
+ continue;
2202
+ if (ns) {
2203
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false, 'pull', undefined, applicationPage);
2204
+ decrypted.push(plaintext);
1607
2205
  }
1608
2206
  else {
1609
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
2207
+ await client._publishAppEvent('message.received', plaintext, 'pull', ns, undefined, applicationPage);
2208
+ decrypted.push(plaintext);
1610
2209
  }
1611
- continue;
1612
- }
1613
- if (version !== 'v2') {
1614
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1615
- continue;
1616
- }
1617
- const msgSpkId = String(msg.spk_id ?? '');
1618
- if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1619
- client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1620
- }
1621
- const deferStatus = {};
1622
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1623
- if (deferStatus.deferred) {
1624
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1625
- }
1626
- if (deferStatus.deferred && deferStatus.fromAid) {
1627
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1628
- deferredKeyFetches.set(key, {
1629
- fromAid: deferStatus.fromAid,
1630
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1631
- groupId: deferStatus.groupId ?? '',
1632
- });
1633
2210
  }
1634
- if (plaintext === null)
1635
- continue;
2211
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
2212
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
2213
+ const serverAckSeq = parsedServerAckSeq ?? 0;
2214
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1636
2215
  if (ns) {
1637
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1638
- decrypted.push(plaintext);
1639
- }
1640
- else {
1641
- await client._publishAppEvent('message.received', plaintext, 'pull');
1642
- decrypted.push(plaintext);
1643
- }
1644
- }
1645
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1646
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1647
- const serverAckSeq = parsedServerAckSeq ?? 0;
1648
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1649
- if (ns) {
1650
- let commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1651
- commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
1652
- let ackSeq = pageContigBefore;
1653
- let contigAdvanced = false;
1654
- try {
1655
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1656
- const state = client._seqTracker.getSyncState?.(ns);
1657
- const deliveryThrough = client._delivery.pendingTailDeliveryThrough?.(ns, state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget) ?? (state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget);
1658
- await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough);
1659
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1660
- pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
1661
- client._delivery.commitPendingTailWindow?.(ns, commitTarget);
1662
- if (commitTarget > pageContigBefore) {
1663
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1664
- if (retentionFloor > pageContigBefore) {
1665
- client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
2216
+ let commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2217
+ commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
2218
+ let ackSeq = pageContigBefore;
2219
+ let contigAdvanced = false;
2220
+ try {
2221
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2222
+ const state = client._seqTracker.getSyncState?.(ns);
2223
+ const deliveryThrough = client._delivery.pendingTailDeliveryThrough?.(ns, state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget) ?? (state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget);
2224
+ await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough, applicationPage);
2225
+ await client._delivery.flushApplicationPage?.(applicationPage);
2226
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2227
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
2228
+ client._delivery.commitPendingTailWindow?.(ns, commitTarget);
2229
+ if (commitTarget > pageContigBefore) {
2230
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
2231
+ if (retentionFloor > pageContigBefore) {
2232
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
2233
+ }
2234
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2235
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
2236
+ }
2237
+ if (seqs.length > 0) {
2238
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2239
+ }
1666
2240
  }
1667
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1668
- client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
2241
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
2242
+ contigAdvanced = ackSeq !== pageContigBefore;
2243
+ if (contigAdvanced) {
2244
+ await client._commitSeqTrackerState(ns);
2245
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2246
+ if (pageCount === 1 && deferredServerCursor > 0)
2247
+ this.clearForwardCursor(ns, ackSeq);
1669
2248
  }
1670
- if (seqs.length > 0) {
1671
- client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2249
+ client._delivery.completePendingTailWindow?.(ns);
2250
+ committedAckSeq = ackSeq;
2251
+ }
2252
+ catch (exc) {
2253
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
2254
+ && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
2255
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2256
+ && pageTrackerSnapshot !== undefined
2257
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2258
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
1672
2259
  }
2260
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2261
+ throw exc;
1673
2262
  }
1674
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1675
- contigAdvanced = ackSeq !== pageContigBefore;
1676
- if (contigAdvanced) {
1677
- await client._commitSeqTrackerState(ns);
1678
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1679
- if (pageCount === 1 && deferredServerCursor > 0)
1680
- this.clearForwardCursor(ns, ackSeq);
2263
+ for (const fetch of deferredKeyFetches.values()) {
2264
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1681
2265
  }
1682
- client._delivery.completePendingTailWindow?.(ns);
1683
- }
1684
- catch (exc) {
1685
- if (Number(client._v2RuntimeGeneration ?? 0) === generation
1686
- && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
1687
- && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
1688
- && pageTrackerSnapshot !== undefined
1689
- && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1690
- client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
2266
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
2267
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
2268
+ }
2269
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
2270
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
2271
+ const knownServerAckSeq = hasServerAckSeq
2272
+ ? serverAckSeq
2273
+ : (pageCount === 1 ? deferredServerCursor : 0);
2274
+ const ackNeeded = ackSeq > 0
2275
+ && ackSeq > lastAutoAckSeq
2276
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
2277
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
2278
+ if (ackNeeded) {
2279
+ this.recordForwardAck(ns, ackSeq);
2280
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1691
2281
  }
1692
- client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1693
- throw exc;
1694
2282
  }
1695
- for (const fetch of deferredKeyFetches.values()) {
1696
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2283
+ if (!ns)
2284
+ await client._delivery.flushApplicationPage?.(applicationPage);
2285
+ if (volatileMessages.length > 0)
2286
+ volatileCursor = client._delivery.volatilePullCursor();
2287
+ const volatileAdvanced = Boolean(volatileCursor) && volatileCursor !== volatileCursorBeforePage;
2288
+ if (ns)
2289
+ client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
2290
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2291
+ const rawCount = messages.length;
2292
+ if (blockedSeq > 0)
2293
+ break;
2294
+ let shouldContinue = shouldContinueForwardPage({
2295
+ rawCount,
2296
+ nextAfterSeq,
2297
+ nextAfter,
2298
+ limit,
2299
+ hasMoreField,
2300
+ hasMore,
2301
+ serverHead,
2302
+ pendingAckSeq,
2303
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2304
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2305
+ });
2306
+ const volatileHasMoreField = Object.prototype.hasOwnProperty.call(result, 'volatile_has_more');
2307
+ const volatileHasMore = result.volatile_has_more === true;
2308
+ if (!shouldContinue && volatileAdvanced && (volatileHasMore || hasMore || (!volatileHasMoreField && limit > 0 && pageMessages.length >= limit))) {
2309
+ shouldContinue = true;
1697
2310
  }
1698
- if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1699
- this.recordForwardCursor(ns, serverAckSeq, ackSeq);
2311
+ const shouldContinueForPipeline = shouldContinueForwardPage({
2312
+ rawCount,
2313
+ nextAfterSeq,
2314
+ nextAfter,
2315
+ limit,
2316
+ hasMoreField,
2317
+ hasMore,
2318
+ serverHead,
2319
+ pendingAckSeq: 0,
2320
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2321
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2322
+ });
2323
+ if (!shouldContinue)
2324
+ break;
2325
+ if (pipelinePage && volatileMessages.length > 0) {
2326
+ await stopPipeline();
2327
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2328
+ pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2329
+ pipelineStopped = false;
2330
+ pipelineError = null;
1700
2331
  }
1701
- // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1702
- // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1703
- const knownServerAckSeq = hasServerAckSeq
1704
- ? serverAckSeq
1705
- : (pageCount === 1 ? deferredServerCursor : 0);
1706
- const ackNeeded = ackSeq > 0
1707
- && ackSeq > lastAutoAckSeq
1708
- && ((hasServerAckSeq && ackSeq > serverAckSeq)
1709
- || (contigAdvanced && ackSeq > knownServerAckSeq));
1710
- if (ackNeeded) {
1711
- this.recordForwardAck(ns, ackSeq);
1712
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1713
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1714
- if (canContinuePage && !pipelineRunning) {
1715
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1716
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1717
- }
1718
- else {
1719
- await this.confirmForwardP2PAck(ackSeq);
1720
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1721
- }
2332
+ if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2333
+ startPipeline(nextAfter, volatileCursor);
1722
2334
  }
1723
- }
1724
- if (ns)
1725
- client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
1726
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1727
- const rawCount = messages.length;
1728
- if (blockedSeq > 0)
1729
- break;
1730
- let shouldContinue = shouldContinueForwardPage({
1731
- rawCount,
1732
- nextAfterSeq,
1733
- nextAfter,
1734
- limit,
1735
- hasMoreField,
1736
- hasMore,
1737
- serverHead,
1738
- pendingAckSeq,
1739
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1740
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1741
- });
1742
- const volatileHasMoreField = Object.prototype.hasOwnProperty.call(result, 'volatile_has_more');
1743
- const volatileHasMore = result.volatile_has_more === true;
1744
- if (!shouldContinue && volatileAdvanced && (volatileHasMore || hasMore || (!volatileHasMoreField && limit > 0 && pageMessages.length >= limit))) {
1745
- shouldContinue = true;
1746
- }
1747
- const shouldContinueForPipeline = shouldContinueForwardPage({
1748
- rawCount,
1749
- nextAfterSeq,
1750
- nextAfter,
1751
- limit,
1752
- hasMoreField,
1753
- hasMore,
1754
- serverHead,
1755
- pendingAckSeq: 0,
1756
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1757
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1758
- });
1759
- if (!shouldContinue)
1760
- break;
1761
- if (pipelinePage) {
1762
- pipelineResume?.();
1763
- pipelineResume = null;
1764
- }
1765
- if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1766
- if (pendingAckSeq > 0) {
1767
- await this.confirmForwardP2PAck(pendingAckSeq);
1768
- pendingAckSeq = 0;
2335
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2336
+ const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
2337
+ after_seq: nextAfter, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
2338
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2339
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2340
+ pullGateKey = nextKey;
1769
2341
  }
1770
- startPipeline(nextAfter, volatileCursor);
2342
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2343
+ if (!pipelineRunning && tailDelayMs > 0)
2344
+ await v2Sleep(tailDelayMs);
2345
+ nextAfterSeq = nextAfter;
1771
2346
  }
1772
- if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1773
- const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1774
- after_seq: nextAfter, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1775
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1776
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1777
- pullGateKey = nextKey;
2347
+ catch (error) {
2348
+ client._delivery.abortApplicationPage?.(applicationPage, error);
2349
+ throw error;
1778
2350
  }
1779
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1780
- if (tailDelayMs > 0)
1781
- await v2Sleep(tailDelayMs);
1782
- nextAfterSeq = nextAfter;
1783
2351
  }
1784
2352
  }
1785
2353
  finally {
@@ -1787,15 +2355,16 @@ export class V2E2EECoordinator {
1787
2355
  }
1788
2356
  if (pageCount >= maxPages) {
1789
2357
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
1790
- if (pendingAckSeq > 0) {
1791
- await this.confirmForwardP2PAck(pendingAckSeq);
1792
- pendingAckSeq = 0;
1793
- }
2358
+ }
2359
+ if (pendingAckSeq > 0) {
2360
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2361
+ await this.confirmForwardP2PAck(pendingAckSeq);
2362
+ pendingAckSeq = 0;
1794
2363
  }
1795
2364
  client._delivery.onPullWorkSettled?.();
1796
2365
  return deduplicatePulledResults(decrypted);
1797
2366
  }
1798
- async publishV2VolatileMessages(messages, source, advanceCursor) {
2367
+ async publishV2VolatileMessages(messages, source, advanceCursor, applicationPage) {
1799
2368
  const client = this.client;
1800
2369
  if (!client._v2Session)
1801
2370
  return [];
@@ -1803,7 +2372,8 @@ export class V2E2EECoordinator {
1803
2372
  const plaintextMessages = [];
1804
2373
  let complete = true;
1805
2374
  for (const rawMessage of messages) {
1806
- const claim = client._delivery.beginVolatileDelivery(rawMessage);
2375
+ await client._delivery.waitVolatileApplicationDelivery?.(rawMessage, applicationPage);
2376
+ const claim = client._delivery.beginVolatileDelivery(rawMessage, applicationPage);
1807
2377
  if (claim === 'seen') {
1808
2378
  processed.push(rawMessage);
1809
2379
  continue;
@@ -1848,24 +2418,27 @@ export class V2E2EECoordinator {
1848
2418
  publicMessage[key] = rawMessage[key];
1849
2419
  }
1850
2420
  const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
1851
- await client._publishAppEvent(appEvent.event, appEvent.payload, source);
1852
- client._delivery.markVolatileSeen(rawMessage);
2421
+ await client._publishAppEvent(appEvent.event, appEvent.payload, source, applicationPage?.namespace ?? '', undefined, applicationPage);
2422
+ client._delivery.markVolatileSeen(rawMessage, applicationPage);
1853
2423
  processed.push(rawMessage);
1854
2424
  plaintextMessages.push(publicMessage);
1855
2425
  }
1856
2426
  finally {
1857
- client._delivery.finishVolatileDelivery(rawMessage);
2427
+ client._delivery.finishVolatileDelivery(rawMessage, applicationPage);
1858
2428
  }
1859
2429
  }
1860
2430
  if (processed.length > 0) {
1861
- await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
2431
+ await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length, applicationPage);
1862
2432
  }
1863
2433
  return plaintextMessages;
1864
2434
  }
1865
2435
  async confirmForwardP2PAck(upToSeq, inlineGeneration) {
1866
2436
  const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
2437
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
2438
+ const deliveryGeneration = this.client._delivery.captureInlineGeneration?.();
1867
2439
  try {
1868
2440
  const result = await this.ackV2ForForward(upToSeq, inlineGeneration);
2441
+ this.ensureDeliveryCurrent(ns, generation, deliveryGeneration);
1869
2442
  const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
1870
2443
  if (actualAckSeq < upToSeq) {
1871
2444
  throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
@@ -1873,7 +2446,10 @@ export class V2E2EECoordinator {
1873
2446
  this.clearForwardAck(ns, upToSeq);
1874
2447
  }
1875
2448
  catch (exc) {
1876
- this.recordForwardAck(ns, upToSeq);
2449
+ if (Number(this.client._v2RuntimeGeneration ?? 0) === generation
2450
+ && (deliveryGeneration === undefined || this.client._delivery.isInlineGenerationCurrent(deliveryGeneration))
2451
+ && this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
2452
+ this.recordForwardAck(ns, upToSeq);
1877
2453
  throw exc;
1878
2454
  }
1879
2455
  }
@@ -1953,6 +2529,7 @@ export class V2E2EECoordinator {
1953
2529
  payload,
1954
2530
  messageId: opts?.messageId,
1955
2531
  timestamp: opts?.timestamp,
2532
+ needRender: opts?.needRender !== false,
1956
2533
  protectedHeaders: opts?.protectedHeaders,
1957
2534
  context: opts?.context,
1958
2535
  useCache,
@@ -1976,6 +2553,7 @@ export class V2E2EECoordinator {
1976
2553
  payload,
1977
2554
  message_id: opts?.messageId,
1978
2555
  timestamp: opts?.timestamp,
2556
+ need_render: opts?.needRender !== false,
1979
2557
  protected_headers: opts?.protectedHeaders,
1980
2558
  context: opts?.context,
1981
2559
  }, groupAid), result, true);
@@ -1997,6 +2575,7 @@ export class V2E2EECoordinator {
1997
2575
  payload,
1998
2576
  message_id: opts?.messageId,
1999
2577
  timestamp: opts?.timestamp,
2578
+ need_render: opts?.needRender !== false,
2000
2579
  protected_headers: opts?.protectedHeaders,
2001
2580
  context: opts?.context,
2002
2581
  }, groupAid), result, true);
@@ -2004,7 +2583,7 @@ export class V2E2EECoordinator {
2004
2583
  throw exc;
2005
2584
  }
2006
2585
  }
2007
- async pullGroupV2TailInternal(params, deferPublish = false) {
2586
+ async pullGroupV2TailInternal(params, deferPublish = false, namespaceLocked = false) {
2008
2587
  const client = this.client;
2009
2588
  const generation = Number(client._v2RuntimeGeneration ?? 0);
2010
2589
  const inlineGeneration = client._delivery.captureInlineGeneration?.();
@@ -2035,33 +2614,57 @@ export class V2E2EECoordinator {
2035
2614
  limit,
2036
2615
  _group_cursor_params: explicitCursorParams,
2037
2616
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2038
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2617
+ if (!namespaceLocked)
2618
+ return await client._rpcPipeline.runPullNamespaceSerialized(pullGateKey, () => this.pullGroupV2TailInternal(params, deferPublish, true), false);
2619
+ client._delivery.rememberCommittedSeqState?.(ns);
2620
+ const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
2621
+ const result = await client._runPullSerialized(pullGateKey, () => client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2039
2622
  group_id: groupId, window_mode: 'tail', after_seq: afterSeq, limit,
2040
2623
  _group_cursor_params: explicitCursorParams, _rpc_foreground: true,
2041
- }, groupAid), pullGateKey);
2624
+ }, groupAid), pullGateKey), false);
2042
2625
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2043
2626
  if (resultAid)
2044
2627
  groupAid = resultAid;
2045
2628
  const page = validateTailPage(result, afterSeq, limit);
2046
2629
  const decoded = [];
2047
- for (const msg of page.messages) {
2048
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2049
- client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2050
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2051
- if (plaintext)
2052
- decoded.push(plaintext);
2630
+ const applicationPage = client._delivery.beginApplicationPage?.(ns, 'tail');
2631
+ try {
2632
+ for (const msg of page.messages) {
2633
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2634
+ client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2635
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish, applicationPage);
2636
+ if (plaintext)
2637
+ decoded.push(plaintext);
2638
+ }
2639
+ await client._delivery.flushApplicationPage?.(applicationPage);
2053
2640
  }
2054
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2641
+ catch (error) {
2642
+ client._delivery.abortApplicationPage?.(applicationPage, error);
2643
+ throw error;
2644
+ }
2645
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration, namespaceGeneration);
2055
2646
  const proof = {
2056
2647
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
2057
2648
  };
2058
2649
  if (deferPublish)
2059
- client._delivery.deferTailWindow(ns, proof);
2650
+ client._delivery.deferTailWindow(ns, proof, applicationPage);
2060
2651
  else {
2061
- client._seqTracker.commitTailWindow(ns, proof);
2062
- client._persistSeq(ns);
2063
- await client._saveSeqTrackerState?.();
2064
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2652
+ const snapshot = client._seqTracker.snapshotNamespace?.(ns);
2653
+ try {
2654
+ client._seqTracker.commitTailWindow(ns, proof);
2655
+ await client._commitSeqTrackerState(ns);
2656
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration, namespaceGeneration);
2657
+ }
2658
+ catch (error) {
2659
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
2660
+ && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
2661
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
2662
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2663
+ && snapshot !== undefined && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2664
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
2665
+ }
2666
+ throw error;
2667
+ }
2065
2668
  }
2066
2669
  const ack = client._seqTracker.getContiguousSeq(ns);
2067
2670
  if (ownsCursor)
@@ -2076,9 +2679,10 @@ export class V2E2EECoordinator {
2076
2679
  let groupAid = String(params.group_aid ?? '').trim();
2077
2680
  const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
2078
2681
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2079
- const result = await this.client._callRawV2Rpc('group.history', withExplicitGroupAid({
2682
+ const key = pullGateKeyForClient(this.client, 'group.history', params, `group:${groupId}|history|before=${beforeSeq}|limit=${limit}`);
2683
+ const result = await this.client._runPullSerialized(key, () => this.client._callRawV2Rpc('group.history', withExplicitGroupAid({
2080
2684
  group_id: groupId, before_seq: beforeSeq, limit, _rpc_foreground: true,
2081
- }, groupAid));
2685
+ }, groupAid), key), false);
2082
2686
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2083
2687
  if (resultAid)
2084
2688
  groupAid = resultAid;
@@ -2093,7 +2697,6 @@ export class V2E2EECoordinator {
2093
2697
  }
2094
2698
  async pullGroupV2Internal(params) {
2095
2699
  await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
2096
- gateLocked: true,
2097
2700
  explicitAfterSeq: true,
2098
2701
  force: params.force === true,
2099
2702
  });
@@ -2121,7 +2724,7 @@ export class V2E2EECoordinator {
2121
2724
  group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
2122
2725
  _group_cursor_params: opts?.cursorParams,
2123
2726
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2124
- return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2727
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2125
2728
  ...(opts ?? {}),
2126
2729
  gateLocked: true,
2127
2730
  }), false);
@@ -2131,7 +2734,7 @@ export class V2E2EECoordinator {
2131
2734
  _group_cursor_params: opts?.cursorParams,
2132
2735
  group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
2133
2736
  window_mode: 'tail', after_seq: afterSeq, limit,
2134
- });
2737
+ }, false, true);
2135
2738
  return Array.isArray(result.messages) ? result.messages : [];
2136
2739
  }
2137
2740
  if (!opts?.gateLocked) {
@@ -2139,21 +2742,21 @@ export class V2E2EECoordinator {
2139
2742
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2140
2743
  _group_cursor_params: opts?.cursorParams,
2141
2744
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2142
- return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2745
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2143
2746
  ...(opts ?? {}),
2144
2747
  gateLocked: true,
2145
- }), true);
2748
+ }), opts?.background !== false);
2146
2749
  }
2147
2750
  const decrypted = [];
2148
2751
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
2149
2752
  const cursorParams = opts?.cursorParams ?? {};
2150
2753
  const ownsCursor = opts?.ownsCursor !== false;
2754
+ if (ownsCursor)
2755
+ client._delivery.rememberCommittedSeqState?.(ns);
2756
+ const namespaceGeneration = ownsCursor ? client._delivery.captureSeqNamespaceGeneration?.(ns) : undefined;
2151
2757
  if (ownsCursor)
2152
2758
  client._delivery.onPullStarted?.(ns);
2153
- let pullGateKey = pullGateKeyForClient(client, 'group.v2.pull', {
2154
- group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2155
- _group_cursor_params: cursorParams,
2156
- }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2759
+ const background = opts?.background !== false;
2157
2760
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
2158
2761
  ? afterSeq
2159
2762
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
@@ -2164,13 +2767,27 @@ export class V2E2EECoordinator {
2164
2767
  let pageCount = 0;
2165
2768
  let lastAutoAckSeq = 0;
2166
2769
  let pendingAckSeq = deferredForwardAck;
2770
+ let committedAckSeq = ownsCursor ? client._seqTracker.getContiguousSeq(ns) : 0;
2771
+ const fetchPage = async (after, ack) => {
2772
+ const key = pullGateKeyForClient(client, 'group.v2.pull', {
2773
+ group_id: gid, after_seq: after, force: opts?.force === true, limit, _group_cursor_params: cursorParams,
2774
+ }, `${ns}|forward|after=${after}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2775
+ return await client._runPullSerialized(key, async () => {
2776
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2777
+ return await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2778
+ group_id: wireGroupId, after_seq: after, limit, ...cursorParams,
2779
+ ...(opts?.force ? { force: true } : {}),
2780
+ ...(ack > 0 ? { ack_up_to_seq: Math.min(ack, committedAckSeq) } : {}),
2781
+ _rpc_background: background, _rpc_foreground: !background,
2782
+ }, groupAid), key);
2783
+ }, background);
2784
+ };
2167
2785
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
2168
2786
  let pipelineRunning = false;
2169
2787
  let pipelineError = null;
2170
2788
  const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2171
2789
  let pipelineProducer = null;
2172
2790
  let pipelineStopped = false;
2173
- let pipelineResume = null;
2174
2791
  const startPipeline = (startAfterSeq) => {
2175
2792
  if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
2176
2793
  return;
@@ -2180,30 +2797,25 @@ export class V2E2EECoordinator {
2180
2797
  for (let produced = pageCount; produced < maxPages; produced += 1) {
2181
2798
  if (pipelineStopped)
2182
2799
  break;
2183
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2184
- group_id: wireGroupId,
2185
- after_seq: producerAfterSeq,
2186
- limit,
2187
- ...cursorParams,
2188
- ...(opts?.force ? { force: true } : {}),
2189
- }, groupAid), pullGateKey);
2800
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2801
+ const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
2802
+ const inlineAckGeneration = ackUpToSeq > 0
2803
+ ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null : null;
2804
+ const result = await fetchPage(producerAfterSeq, ackUpToSeq);
2190
2805
  if (pipelineStopped)
2191
2806
  break;
2807
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2192
2808
  const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2193
- const completed = new Promise((resolve) => { pipelineResume = resolve; });
2194
2809
  const enqueued = await pipelineQueue.put({
2195
2810
  result,
2196
2811
  afterSeq: producerAfterSeq,
2197
- ackUpToSeq: 0,
2198
- inlineAckGeneration: null,
2812
+ ackUpToSeq,
2813
+ inlineAckGeneration,
2199
2814
  requestedVolatileCursor: '',
2200
2815
  weight: pageMessages.length,
2201
2816
  }, pageMessages.length);
2202
2817
  if (!enqueued || pipelineStopped)
2203
2818
  break;
2204
- await completed;
2205
- if (pipelineStopped)
2206
- break;
2207
2819
  const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
2208
2820
  const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
2209
2821
  const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
@@ -2232,8 +2844,6 @@ export class V2E2EECoordinator {
2232
2844
  };
2233
2845
  const stopPipeline = async () => {
2234
2846
  pipelineStopped = true;
2235
- pipelineResume?.();
2236
- pipelineResume = null;
2237
2847
  pipelineQueue.close();
2238
2848
  if (pipelineProducer) {
2239
2849
  const producer = pipelineProducer;
@@ -2244,97 +2854,130 @@ export class V2E2EECoordinator {
2244
2854
  };
2245
2855
  try {
2246
2856
  while (pageCount < maxPages) {
2247
- let pipelinePage;
2248
- if (pipelineRunning) {
2249
- pipelinePage = await pipelineQueue.take();
2250
- if (!pipelinePage) {
2251
- pipelineRunning = false;
2252
- if (pipelineError)
2253
- throw pipelineError;
2254
- break;
2255
- }
2256
- }
2257
- pageCount += 1;
2258
- const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2259
- const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2260
- ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
2261
- : null);
2262
- let result;
2263
- if (pipelinePage) {
2264
- nextAfterSeq = pipelinePage.afterSeq;
2265
- result = pipelinePage.result;
2266
- }
2267
- else {
2268
- result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2269
- group_id: wireGroupId,
2270
- after_seq: nextAfterSeq,
2271
- limit,
2272
- ...cursorParams,
2273
- ...(opts?.force ? { force: true } : {}),
2274
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2275
- }, groupAid), pullGateKey);
2276
- }
2277
- if (ackUpToSeq > 0) {
2278
- const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
2279
- if (actualAckSeq >= ackUpToSeq) {
2280
- if (pendingInlineGeneration !== null) {
2281
- client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2857
+ let applicationPage;
2858
+ try {
2859
+ let pipelinePage;
2860
+ if (pipelineRunning) {
2861
+ pipelinePage = await pipelineQueue.take();
2862
+ if (!pipelinePage) {
2863
+ pipelineRunning = false;
2864
+ if (pipelineError)
2865
+ throw pipelineError;
2866
+ break;
2282
2867
  }
2283
- pendingAckSeq = 0;
2284
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2285
- this.clearForwardAck(ns, ackUpToSeq);
2868
+ }
2869
+ pageCount += 1;
2870
+ const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2871
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2872
+ ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
2873
+ : null);
2874
+ let result;
2875
+ if (pipelinePage) {
2876
+ nextAfterSeq = pipelinePage.afterSeq;
2877
+ result = pipelinePage.result;
2286
2878
  }
2287
2879
  else {
2288
- client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2289
- await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2290
- pendingAckSeq = 0;
2291
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2880
+ result = await fetchPage(nextAfterSeq, ackUpToSeq);
2292
2881
  }
2293
- }
2294
- const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2295
- if (resultGroupAid)
2296
- groupAid = resultGroupAid;
2297
- const eventGroupAid = resultGroupAid || groupAid || gid;
2298
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2299
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2300
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2301
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2302
- const hasMore = result.has_more === true;
2303
- const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2304
- const seqs = messages
2305
- .map((msg) => Number(msg.seq ?? 0))
2306
- .filter((seq) => Number.isFinite(seq) && seq > 0);
2307
- const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2308
- let pageTrackerSnapshot;
2309
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
2310
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2311
- const deferredKeyFetches = new Map();
2312
- let blockedSeq = 0;
2313
- const pageDecryptedBefore = decrypted.length;
2314
- for (const msg of messages) {
2315
- const seq = Number(msg.seq ?? 0);
2316
- if (!Number.isFinite(seq) || seq <= 0)
2317
- continue;
2318
- const version = String(msg.version ?? 'v2');
2319
- if (version === 'v1') {
2320
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2321
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2322
- const payloadObj = isJsonObject(payload) ? payload : null;
2323
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2324
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2325
- && client._delivery.recallEventFromGroupMessage(msg)) {
2326
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2327
- ...msg,
2328
- group_id: gid,
2329
- group_aid: eventGroupAid,
2330
- }, 'pull');
2331
- client._markPublishedSeq(ns, seq);
2332
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2333
- continue;
2882
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2883
+ if (ownsCursor && !opts?.force)
2884
+ client._delivery.observeBackgroundPullResponse?.(ns, nextAfterSeq, result, limit);
2885
+ if (ackUpToSeq > 0) {
2886
+ const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
2887
+ if (actualAckSeq >= ackUpToSeq) {
2888
+ if (pendingInlineGeneration !== null) {
2889
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2890
+ }
2891
+ if (pendingAckSeq <= ackUpToSeq)
2892
+ pendingAckSeq = 0;
2893
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2894
+ this.clearForwardAck(ns, ackUpToSeq);
2895
+ }
2896
+ else {
2897
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2898
+ pendingAckSeq = Math.max(pendingAckSeq, ackUpToSeq);
2899
+ this.recordForwardAck(ns, ackUpToSeq);
2334
2900
  }
2335
- if (payloadObj) {
2336
- const payloadType = String(payloadObj.type ?? '').trim();
2337
- if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2901
+ }
2902
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2903
+ if (resultGroupAid)
2904
+ groupAid = resultGroupAid;
2905
+ const eventGroupAid = resultGroupAid || groupAid || gid;
2906
+ applicationPage = client._delivery.beginApplicationPage?.(ns);
2907
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2908
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2909
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2910
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2911
+ const hasMore = result.has_more === true;
2912
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2913
+ const seqs = messages
2914
+ .map((msg) => Number(msg.seq ?? 0))
2915
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2916
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2917
+ let pageTrackerSnapshot;
2918
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
2919
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2920
+ if (!pipelineRunning && shouldContinueForwardPage({
2921
+ rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
2922
+ hasMoreField, hasMore, serverHead, pendingAckSeq: 0,
2923
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2924
+ ? client._seqTracker.getMaxSeenSeq(ns) : pageMaxSeq,
2925
+ }))
2926
+ startPipeline(pageMaxSeq);
2927
+ const deferredKeyFetches = new Map();
2928
+ let blockedSeq = 0;
2929
+ const pageDecryptedBefore = decrypted.length;
2930
+ for (const msg of messages) {
2931
+ const seq = Number(msg.seq ?? 0);
2932
+ if (!Number.isFinite(seq) || seq <= 0)
2933
+ continue;
2934
+ const version = String(msg.version ?? 'v2');
2935
+ if (version === 'v1') {
2936
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2937
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2938
+ const payloadObj = isJsonObject(payload) ? payload : null;
2939
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2940
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2941
+ && client._delivery.recallEventFromGroupMessage(msg)) {
2942
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
2943
+ ...msg,
2944
+ group_id: gid,
2945
+ group_aid: eventGroupAid,
2946
+ }, 'pull', applicationPage);
2947
+ client._markPublishedSeq(ns, seq, applicationPage);
2948
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2949
+ continue;
2950
+ }
2951
+ if (payloadObj) {
2952
+ const payloadType = String(payloadObj.type ?? '').trim();
2953
+ if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2954
+ let v1Msg = {
2955
+ message_id: String(msg.message_id ?? ''),
2956
+ from: String(msg.from_aid ?? ''),
2957
+ group_id: gid,
2958
+ group_aid: eventGroupAid,
2959
+ seq: msg.seq,
2960
+ type: String(msg.type ?? ''),
2961
+ timestamp: msg.t_server,
2962
+ payload,
2963
+ encrypted: false,
2964
+ need_render: msg.need_render !== false,
2965
+ };
2966
+ if (msg.sender_device_id !== undefined)
2967
+ v1Msg.sender_device_id = msg.sender_device_id;
2968
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2969
+ attachGatewayProximity(v1Msg, msg);
2970
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2971
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false, 'pull', undefined, applicationPage);
2972
+ }
2973
+ else {
2974
+ client._markPublishedSeq(ns, seq, applicationPage);
2975
+ }
2976
+ decrypted.push(v1Msg);
2977
+ continue;
2978
+ }
2979
+ }
2980
+ else if (payload !== undefined && payload !== null) {
2338
2981
  let v1Msg = {
2339
2982
  message_id: String(msg.message_id ?? ''),
2340
2983
  from: String(msg.from_aid ?? ''),
@@ -2345,234 +2988,196 @@ export class V2E2EECoordinator {
2345
2988
  timestamp: msg.t_server,
2346
2989
  payload,
2347
2990
  encrypted: false,
2991
+ need_render: msg.need_render !== false,
2348
2992
  };
2349
2993
  if (msg.sender_device_id !== undefined)
2350
2994
  v1Msg.sender_device_id = msg.sender_device_id;
2351
2995
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2352
2996
  attachGatewayProximity(v1Msg, msg);
2353
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2354
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2355
- }
2356
- else {
2357
- client._markPublishedSeq(ns, seq);
2358
- }
2997
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false, 'pull', undefined, applicationPage);
2359
2998
  decrypted.push(v1Msg);
2360
2999
  continue;
2361
3000
  }
3001
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
3002
+ const deferStatus = {};
3003
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
3004
+ if (deferStatus.deferred) {
3005
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
3006
+ if (deferStatus.fromAid) {
3007
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3008
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3009
+ }
3010
+ }
3011
+ if (plaintext) {
3012
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3013
+ plaintext.group_id = gid;
3014
+ plaintext.group_aid = eventGroupAid;
3015
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3016
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false, 'pull', undefined, applicationPage);
3017
+ }
3018
+ else {
3019
+ client._markPublishedSeq(ns, seq, applicationPage);
3020
+ }
3021
+ decrypted.push(plaintext);
3022
+ }
3023
+ }
3024
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
3025
+ continue;
3026
+ }
3027
+ if (version !== 'v2') {
3028
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
3029
+ continue;
3030
+ }
3031
+ const deferStatus = {};
3032
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
3033
+ if (deferStatus.deferred) {
3034
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2362
3035
  }
2363
- else if (payload !== undefined && payload !== null) {
2364
- let v1Msg = {
2365
- message_id: String(msg.message_id ?? ''),
2366
- from: String(msg.from_aid ?? ''),
2367
- group_id: gid,
2368
- group_aid: eventGroupAid,
2369
- seq: msg.seq,
2370
- type: String(msg.type ?? ''),
2371
- timestamp: msg.t_server,
2372
- payload,
2373
- encrypted: false,
2374
- };
2375
- if (msg.sender_device_id !== undefined)
2376
- v1Msg.sender_device_id = msg.sender_device_id;
2377
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2378
- attachGatewayProximity(v1Msg, msg);
2379
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2380
- decrypted.push(v1Msg);
3036
+ if (deferStatus.deferred && deferStatus.fromAid) {
3037
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
3038
+ deferredKeyFetches.set(key, {
3039
+ fromAid: deferStatus.fromAid,
3040
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
3041
+ groupId: deferStatus.groupId ?? '',
3042
+ });
3043
+ }
3044
+ if (plaintext === null)
2381
3045
  continue;
3046
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
3047
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
3048
+ plaintext.group_id = gid;
3049
+ plaintext.group_aid = eventGroupAid;
3050
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
3051
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false, 'pull', undefined, applicationPage);
3052
+ }
3053
+ else {
3054
+ client._markPublishedSeq(ns, seq, applicationPage);
2382
3055
  }
2383
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
2384
- const deferStatus = {};
2385
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2386
- if (deferStatus.deferred) {
2387
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2388
- if (deferStatus.fromAid) {
2389
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2390
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
3056
+ decrypted.push(plaintext);
3057
+ }
3058
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
3059
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
3060
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
3061
+ const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
3062
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
3063
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
3064
+ let commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
3065
+ commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
3066
+ let ackSeq = pageContigBefore;
3067
+ let contigAdvanced = false;
3068
+ if (ownsCursor) {
3069
+ try {
3070
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration, namespaceGeneration);
3071
+ const state = client._seqTracker.getSyncState?.(ns);
3072
+ const deliveryThrough = client._delivery.pendingTailDeliveryThrough?.(ns, state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget) ?? (state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget);
3073
+ await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough, applicationPage);
3074
+ await client._delivery.flushApplicationPage?.(applicationPage);
3075
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration, namespaceGeneration);
3076
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
3077
+ client._delivery.commitPendingTailWindow?.(ns, commitTarget);
3078
+ if (commitTarget > pageContigBefore) {
3079
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
3080
+ if (effectiveFloor > pageContigBefore) {
3081
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2391
3082
  }
2392
- }
2393
- if (plaintext) {
2394
- plaintext.group_id = gid;
2395
- plaintext.group_aid = eventGroupAid;
2396
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2397
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
3083
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
3084
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2398
3085
  }
2399
- else {
2400
- client._markPublishedSeq(ns, seq);
3086
+ if (seqs.length > 0) {
3087
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2401
3088
  }
2402
- decrypted.push(plaintext);
2403
- }
2404
- }
2405
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2406
- continue;
2407
- }
2408
- if (version !== 'v2') {
2409
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2410
- continue;
2411
- }
2412
- const deferStatus = {};
2413
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2414
- if (deferStatus.deferred) {
2415
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2416
- }
2417
- if (deferStatus.deferred && deferStatus.fromAid) {
2418
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2419
- deferredKeyFetches.set(key, {
2420
- fromAid: deferStatus.fromAid,
2421
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2422
- groupId: deferStatus.groupId ?? '',
2423
- });
2424
- }
2425
- if (plaintext === null)
2426
- continue;
2427
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2428
- plaintext.group_id = gid;
2429
- plaintext.group_aid = eventGroupAid;
2430
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2431
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2432
- }
2433
- else {
2434
- client._markPublishedSeq(ns, seq);
2435
- }
2436
- decrypted.push(plaintext);
2437
- }
2438
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2439
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2440
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2441
- const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2442
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2443
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2444
- let commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2445
- commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
2446
- let ackSeq = pageContigBefore;
2447
- let contigAdvanced = false;
2448
- if (ownsCursor) {
2449
- try {
2450
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2451
- const state = client._seqTracker.getSyncState?.(ns);
2452
- const deliveryThrough = client._delivery.pendingTailDeliveryThrough?.(ns, state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget) ?? (state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget);
2453
- await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough);
2454
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2455
- pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
2456
- client._delivery.commitPendingTailWindow?.(ns, commitTarget);
2457
- if (commitTarget > pageContigBefore) {
2458
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
2459
- if (effectiveFloor > pageContigBefore) {
2460
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2461
3089
  }
2462
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2463
- client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2464
- }
2465
- if (seqs.length > 0) {
2466
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
3090
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
3091
+ contigAdvanced = ackSeq !== pageContigBefore;
3092
+ if (contigAdvanced) {
3093
+ await client._commitSeqTrackerState(ns);
3094
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration, namespaceGeneration);
3095
+ if (pageCount === 1 && deferredServerCursor > 0)
3096
+ this.clearForwardCursor(ns, ackSeq);
2467
3097
  }
3098
+ client._delivery.completePendingTailWindow?.(ns);
3099
+ committedAckSeq = ackSeq;
2468
3100
  }
2469
- ackSeq = client._seqTracker.getContiguousSeq(ns);
2470
- contigAdvanced = ackSeq !== pageContigBefore;
2471
- if (contigAdvanced) {
2472
- await client._commitSeqTrackerState(ns);
2473
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2474
- if (pageCount === 1 && deferredServerCursor > 0)
2475
- this.clearForwardCursor(ns, ackSeq);
3101
+ catch (exc) {
3102
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
3103
+ && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
3104
+ && client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
3105
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
3106
+ && pageTrackerSnapshot !== undefined
3107
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
3108
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
3109
+ }
3110
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
3111
+ throw exc;
2476
3112
  }
2477
- client._delivery.completePendingTailWindow?.(ns);
2478
3113
  }
2479
- catch (exc) {
2480
- if (Number(client._v2RuntimeGeneration ?? 0) === generation
2481
- && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
2482
- && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2483
- && pageTrackerSnapshot !== undefined
2484
- && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2485
- client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
2486
- }
2487
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2488
- throw exc;
3114
+ if (!ownsCursor)
3115
+ await client._delivery.flushApplicationPage?.(applicationPage);
3116
+ for (const fetch of deferredKeyFetches.values()) {
3117
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2489
3118
  }
2490
- }
2491
- for (const fetch of deferredKeyFetches.values()) {
2492
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2493
- }
2494
- client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
2495
- if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2496
- this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2497
- }
2498
- // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2499
- // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2500
- const knownServerCursorSeq = hasServerCursor
2501
- ? cursorCurrentSeq
2502
- : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2503
- const ackNeeded = ackSeq > 0
2504
- && ackSeq > lastAutoAckSeq
2505
- && ownsCursor
2506
- && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2507
- || (contigAdvanced && ackSeq > knownServerCursorSeq));
2508
- if (ackNeeded) {
2509
- this.recordForwardAck(ns, ackSeq);
2510
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2511
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2512
- if (canContinuePage && !pipelineRunning) {
2513
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2514
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
3119
+ client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
3120
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
3121
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2515
3122
  }
2516
- else {
2517
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2518
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
3123
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
3124
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
3125
+ const knownServerCursorSeq = hasServerCursor
3126
+ ? cursorCurrentSeq
3127
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
3128
+ const ackNeeded = ackSeq > 0
3129
+ && ackSeq > lastAutoAckSeq
3130
+ && ownsCursor
3131
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
3132
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
3133
+ if (ackNeeded) {
3134
+ this.recordForwardAck(ns, ackSeq);
3135
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2519
3136
  }
2520
- }
2521
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2522
- if (!ownsCursor)
2523
- break;
2524
- const rawCount = messages.length;
2525
- if (blockedSeq > 0)
2526
- break;
2527
- const shouldContinue = shouldContinueForwardPage({
2528
- rawCount,
2529
- nextAfterSeq,
2530
- nextAfter,
2531
- limit,
2532
- hasMoreField,
2533
- hasMore,
2534
- serverHead,
2535
- pendingAckSeq,
2536
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2537
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2538
- });
2539
- const shouldContinueForPipeline = shouldContinueForwardPage({
2540
- rawCount,
2541
- nextAfterSeq,
2542
- nextAfter,
2543
- limit,
2544
- hasMoreField,
2545
- hasMore,
2546
- serverHead,
2547
- pendingAckSeq: 0,
2548
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2549
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2550
- });
2551
- if (!shouldContinue)
2552
- break;
2553
- if (pipelinePage) {
2554
- pipelineResume?.();
2555
- pipelineResume = null;
2556
- }
2557
- if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2558
- if (pendingAckSeq > 0) {
2559
- await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2560
- pendingAckSeq = 0;
3137
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
3138
+ if (!ownsCursor)
3139
+ break;
3140
+ const rawCount = messages.length;
3141
+ if (blockedSeq > 0)
3142
+ break;
3143
+ const shouldContinue = shouldContinueForwardPage({
3144
+ rawCount,
3145
+ nextAfterSeq,
3146
+ nextAfter,
3147
+ limit,
3148
+ hasMoreField,
3149
+ hasMore,
3150
+ serverHead,
3151
+ pendingAckSeq,
3152
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
3153
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
3154
+ });
3155
+ const shouldContinueForPipeline = shouldContinueForwardPage({
3156
+ rawCount,
3157
+ nextAfterSeq,
3158
+ nextAfter,
3159
+ limit,
3160
+ hasMoreField,
3161
+ hasMore,
3162
+ serverHead,
3163
+ pendingAckSeq: 0,
3164
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
3165
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
3166
+ });
3167
+ if (!shouldContinue)
3168
+ break;
3169
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
3170
+ startPipeline(nextAfter);
2561
3171
  }
2562
- startPipeline(nextAfter);
3172
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
3173
+ if (!pipelineRunning && tailDelayMs > 0)
3174
+ await v2Sleep(tailDelayMs);
3175
+ nextAfterSeq = nextAfter;
2563
3176
  }
2564
- if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2565
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2566
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2567
- _group_cursor_params: cursorParams,
2568
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2569
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2570
- pullGateKey = nextKey;
3177
+ catch (error) {
3178
+ client._delivery.abortApplicationPage?.(applicationPage, error);
3179
+ throw error;
2571
3180
  }
2572
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2573
- if (tailDelayMs > 0)
2574
- await v2Sleep(tailDelayMs);
2575
- nextAfterSeq = nextAfter;
2576
3181
  }
2577
3182
  }
2578
3183
  finally {
@@ -2580,18 +3185,22 @@ export class V2E2EECoordinator {
2580
3185
  }
2581
3186
  if (pageCount >= maxPages) {
2582
3187
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
2583
- if (pendingAckSeq > 0) {
2584
- await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2585
- pendingAckSeq = 0;
2586
- }
3188
+ }
3189
+ if (pendingAckSeq > 0) {
3190
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
3191
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid, background);
3192
+ pendingAckSeq = 0;
2587
3193
  }
2588
3194
  client._delivery.onPullWorkSettled?.();
2589
3195
  return deduplicatePulledResults(decrypted);
2590
3196
  }
2591
- async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
3197
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid, background = false) {
2592
3198
  const ns = `group:${String(groupId ?? '').trim()}`;
3199
+ const generation = Number(this.client._v2RuntimeGeneration ?? 0);
3200
+ const deliveryGeneration = this.client._delivery.captureInlineGeneration?.();
2593
3201
  try {
2594
- const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
3202
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid, background);
3203
+ this.ensureDeliveryCurrent(ns, generation, deliveryGeneration);
2595
3204
  const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
2596
3205
  if (actualAckSeq < upToSeq) {
2597
3206
  throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
@@ -2599,17 +3208,20 @@ export class V2E2EECoordinator {
2599
3208
  this.clearForwardAck(ns, upToSeq);
2600
3209
  }
2601
3210
  catch (exc) {
2602
- this.recordForwardAck(ns, upToSeq);
3211
+ if (Number(this.client._v2RuntimeGeneration ?? 0) === generation
3212
+ && (deliveryGeneration === undefined || this.client._delivery.isInlineGenerationCurrent(deliveryGeneration))
3213
+ && this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
3214
+ this.recordForwardAck(ns, upToSeq);
2603
3215
  throw exc;
2604
3216
  }
2605
3217
  }
2606
3218
  async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
2607
3219
  return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, false);
2608
3220
  }
2609
- async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
2610
- return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, undefined, true);
3221
+ async ackGroupV2ForForward(groupId, upToSeq, groupAid, background = false) {
3222
+ return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, undefined, true, background);
2611
3223
  }
2612
- async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark) {
3224
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark, background) {
2613
3225
  const client = this.client;
2614
3226
  const gid = typeof groupId === 'string' ? groupId.trim() : '';
2615
3227
  if (!gid)
@@ -2631,7 +3243,9 @@ export class V2E2EECoordinator {
2631
3243
  return { acked: 0 };
2632
3244
  // 与 P2P 一致,必须将 ACK 响应限定到请求开始时对应的 inline generation。
2633
3245
  const ackGeneration = inlineGeneration ?? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null;
2634
- const raw = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
3246
+ const raw = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq,
3247
+ ...(background === undefined ? {} : { _rpc_background: background, _rpc_foreground: !background }),
3248
+ }, explicitGroupAid));
2635
3249
  const actualAckSeq = requireServerWatermark
2636
3250
  ? (client._delivery?.resolveForwardAckSeq?.(raw, seq) ?? 0)
2637
3251
  : (client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq);
@@ -3182,11 +3796,19 @@ export class V2E2EECoordinator {
3182
3796
  });
3183
3797
  }
3184
3798
  }
3799
+ const targetDeviceId = String(opts.deviceId ?? '').trim();
3185
3800
  if (peerDevices.length === 0) {
3801
+ if (targetDeviceId && useCache) {
3802
+ this.deleteBootstrapCacheEntry(to);
3803
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
3804
+ }
3186
3805
  throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
3187
3806
  }
3188
- const targetDeviceId = String(opts.deviceId ?? '').trim();
3189
3807
  if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
3808
+ if (useCache) {
3809
+ this.deleteBootstrapCacheEntry(to);
3810
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
3811
+ }
3190
3812
  throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
3191
3813
  }
3192
3814
  let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
@@ -3277,11 +3899,16 @@ export class V2E2EECoordinator {
3277
3899
  return isPeerTarget || target.role === 'self_sync';
3278
3900
  });
3279
3901
  if (!peerTargetFound) {
3902
+ if (useCache) {
3903
+ this.deleteBootstrapCacheEntry(to);
3904
+ return this.buildV2P2PEnvelope({ ...opts, useCache: false });
3905
+ }
3280
3906
  throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
3281
3907
  }
3282
3908
  }
3283
3909
  const sender = await session.getSenderIdentity();
3284
3910
  const envelope = await encryptP2PMessage(sender, targetSet, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
3911
+ envelope.need_render = opts.needRender !== false;
3285
3912
  return envelope;
3286
3913
  }
3287
3914
  async buildV2GroupEnvelope(opts) {
@@ -3404,6 +4031,7 @@ export class V2E2EECoordinator {
3404
4031
  }
3405
4032
  const sender = await session.getSenderIdentity();
3406
4033
  const envelope = await encryptGroupMessage(sender, groupId, epoch, targetSet.targets, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context }, stateCommitment);
4034
+ envelope.need_render = opts.needRender !== false;
3407
4035
  return envelope;
3408
4036
  }
3409
4037
  handleGroupChangedSpk(data, groupId, action) {