@agentunion/fastaun-browser 0.5.20 → 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 (130) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  5. 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
  6. 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
  7. 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
  8. 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
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. 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
  14. 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
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. 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
  25. 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
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. 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 +106 -43
  30. 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 +180 -9
  31. 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 -708
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. 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 +1 -1
  34. 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 +2 -2
  35. package/dist/agent-md-schema.d.ts +1 -0
  36. package/dist/agent-md-schema.d.ts.map +1 -1
  37. package/dist/agent-md-schema.js +5 -4
  38. package/dist/agent-md-schema.js.map +1 -1
  39. package/dist/agent-md.d.ts +25 -1
  40. package/dist/agent-md.d.ts.map +1 -1
  41. package/dist/agent-md.js +148 -71
  42. package/dist/agent-md.js.map +1 -1
  43. package/dist/aid-store.d.ts +13 -0
  44. package/dist/aid-store.d.ts.map +1 -1
  45. package/dist/aid-store.js +55 -8
  46. package/dist/aid-store.js.map +1 -1
  47. package/dist/aid.d.ts.map +1 -1
  48. package/dist/aid.js +2 -1
  49. package/dist/aid.js.map +1 -1
  50. package/dist/auth.d.ts.map +1 -1
  51. package/dist/auth.js +4 -0
  52. package/dist/auth.js.map +1 -1
  53. package/dist/bundle.js +4981 -1219
  54. package/dist/cert-utils.d.ts +1 -0
  55. package/dist/cert-utils.d.ts.map +1 -1
  56. package/dist/cert-utils.js +21 -0
  57. package/dist/cert-utils.js.map +1 -1
  58. package/dist/client/delivery.d.ts +101 -16
  59. package/dist/client/delivery.d.ts.map +1 -1
  60. package/dist/client/delivery.js +1172 -160
  61. package/dist/client/delivery.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +4 -1
  63. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  64. package/dist/client/rpc-pipeline.js +122 -48
  65. package/dist/client/rpc-pipeline.js.map +1 -1
  66. package/dist/client/v2-e2ee.d.ts +22 -3
  67. package/dist/client/v2-e2ee.d.ts.map +1 -1
  68. package/dist/client/v2-e2ee.js +1258 -649
  69. package/dist/client/v2-e2ee.js.map +1 -1
  70. package/dist/client.d.ts +23 -2
  71. package/dist/client.d.ts.map +1 -1
  72. package/dist/client.js +391 -87
  73. package/dist/client.js.map +1 -1
  74. package/dist/config.d.ts +4 -0
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +1 -0
  77. package/dist/config.js.map +1 -1
  78. package/dist/events.d.ts +19 -1
  79. package/dist/events.d.ts.map +1 -1
  80. package/dist/events.js +146 -23
  81. package/dist/events.js.map +1 -1
  82. package/dist/facades.d.ts +42 -1
  83. package/dist/facades.d.ts.map +1 -1
  84. package/dist/facades.js +363 -7
  85. package/dist/facades.js.map +1 -1
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +1 -1
  89. package/dist/index.js.map +1 -1
  90. package/dist/keystore/index.d.ts +14 -1
  91. package/dist/keystore/index.d.ts.map +1 -1
  92. package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
  93. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  94. package/dist/keystore/indexeddb-identity-store.js +30 -13
  95. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  96. package/dist/keystore/indexeddb-shared.d.ts +19 -2
  97. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  98. package/dist/keystore/indexeddb-shared.js +193 -6
  99. package/dist/keystore/indexeddb-shared.js.map +1 -1
  100. package/dist/keystore/indexeddb-token-store.d.ts +15 -2
  101. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  102. package/dist/keystore/indexeddb-token-store.js +308 -27
  103. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  104. package/dist/org-protocol.d.ts +45 -0
  105. package/dist/org-protocol.d.ts.map +1 -1
  106. package/dist/org-protocol.js +757 -11
  107. package/dist/org-protocol.js.map +1 -1
  108. package/dist/ownership-v21.d.ts +21 -4
  109. package/dist/ownership-v21.d.ts.map +1 -1
  110. package/dist/ownership-v21.js +49 -16
  111. package/dist/ownership-v21.js.map +1 -1
  112. package/dist/ownership-workflow.d.ts +30 -0
  113. package/dist/ownership-workflow.d.ts.map +1 -1
  114. package/dist/ownership-workflow.js +412 -54
  115. package/dist/ownership-workflow.js.map +1 -1
  116. package/dist/seq-tracker.d.ts +2 -0
  117. package/dist/seq-tracker.d.ts.map +1 -1
  118. package/dist/seq-tracker.js +3 -2
  119. package/dist/seq-tracker.js.map +1 -1
  120. package/dist/tools/cross-sdk-agent.d.ts +60 -3
  121. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  122. package/dist/tools/cross-sdk-agent.js +433 -37
  123. package/dist/tools/cross-sdk-agent.js.map +1 -1
  124. package/dist/transport.d.ts +3 -0
  125. package/dist/transport.d.ts.map +1 -1
  126. package/dist/transport.js +62 -30
  127. package/dist/transport.js.map +1 -1
  128. package/dist/version.d.ts +1 -1
  129. package/dist/version.js +1 -1
  130. 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';
@@ -307,6 +308,7 @@ function v2E2eeMeta(envelope) {
307
308
  suite,
308
309
  encryption_mode: `v2_${suite || 'unknown'}`,
309
310
  forward_secrecy: true,
311
+ need_render: envelope.need_render !== false,
310
312
  };
311
313
  const protectedHeaders = metadataWithoutAuth(envelope.protected_headers);
312
314
  if (protectedHeaders && Object.keys(protectedHeaders).length > 0) {
@@ -329,6 +331,8 @@ function v2E2eeMeta(envelope) {
329
331
  function attachV2EnvelopeMetadata(message, meta) {
330
332
  if (!meta)
331
333
  return;
334
+ if (typeof meta.need_render === 'boolean')
335
+ message.need_render = meta.need_render;
332
336
  const payloadType = typeof meta.payload_type === 'string' ? meta.payload_type.trim() : '';
333
337
  if (payloadType)
334
338
  message.payload_type = payloadType;
@@ -799,11 +803,12 @@ export class V2E2EECoordinator {
799
803
  return null;
800
804
  }
801
805
  }
802
- pendingSenderIKMessageKey(msg, groupId) {
806
+ pendingSenderIKMessageKey(msg, groupId, fromAid = '') {
803
807
  const client = this.client;
804
808
  const messageId = String(msg.message_id ?? '').trim();
805
809
  const seq = String(msg.seq ?? '').trim();
806
- 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 ?? ''}`;
807
812
  return `${prefix}:${messageId || seq || Math.random().toString(36).slice(2)}`;
808
813
  }
809
814
  pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
@@ -894,7 +899,7 @@ export class V2E2EECoordinator {
894
899
  return;
895
900
  const senderDeviceId = String(args.senderDeviceId ?? '');
896
901
  const groupId = String(args.groupId ?? '').trim();
897
- const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId);
902
+ const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId, fromAid);
898
903
  client._v2SenderIKPending.set(messageKey, {
899
904
  msg: { ...args.msg },
900
905
  fromAid,
@@ -918,8 +923,9 @@ export class V2E2EECoordinator {
918
923
  }
919
924
  async resolveSenderIKPending(fromAid, senderDeviceId, groupId, fetchKey, generation) {
920
925
  const client = this.client;
921
- const generationCurrent = () => generation === undefined
922
- || 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);
923
929
  try {
924
930
  if (!generationCurrent())
925
931
  return;
@@ -930,6 +936,8 @@ export class V2E2EECoordinator {
930
936
  peer_aid: fromAid,
931
937
  e2ee_wrap_capabilities: v2WrapCapabilities(),
932
938
  });
939
+ if (!generationCurrent())
940
+ return;
933
941
  const peers = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
934
942
  for (const dev of peers)
935
943
  client._cacheV2PeerIKFromDevice(dev, fromAid);
@@ -943,6 +951,8 @@ export class V2E2EECoordinator {
943
951
  group_id: groupId,
944
952
  e2ee_wrap_capabilities: v2WrapCapabilities(),
945
953
  });
954
+ if (!generationCurrent())
955
+ return;
946
956
  const devices = (Array.isArray(gbs?.devices) ? gbs.devices : []);
947
957
  const audit = (Array.isArray(gbs?.audit_recipients) ? gbs.audit_recipients : []);
948
958
  for (const dev of devices)
@@ -978,6 +988,8 @@ export class V2E2EECoordinator {
978
988
  }
979
989
  if (!generationCurrent())
980
990
  return;
991
+ if (plaintext && client._p2pPeerPullEnabled && !entry.groupId)
992
+ plaintext.need_render = entry.msg.need_render !== false;
981
993
  if (plaintext === null) {
982
994
  if (retryStatus.deferred) {
983
995
  client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
@@ -994,12 +1006,17 @@ export class V2E2EECoordinator {
994
1006
  }
995
1007
  if (!entry.groupId && String(entry.msg.storage ?? '') === 'volatile') {
996
1008
  await this.publishPendingV2VolatileMessage(entry.msg, plaintext, 'pending_retry');
1009
+ if (!generationCurrent())
1010
+ return;
997
1011
  client._v2SenderIKPending.delete(key);
1012
+ if (client._p2pPeerPullEnabled)
1013
+ client._delivery.schedulePeerRecovery(entry.fromAid, 0, true);
998
1014
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
999
1015
  continue;
1000
1016
  }
1001
1017
  const seq = Number(entry.msg.seq ?? 0);
1002
1018
  if (entry.groupId) {
1019
+ plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
1003
1020
  if (client._delivery.isSelfSentGroupMessage?.(entry.msg)) {
1004
1021
  client._v2SenderIKPending.delete(key);
1005
1022
  continue;
@@ -1009,39 +1026,72 @@ export class V2E2EECoordinator {
1009
1026
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
1010
1027
  }
1011
1028
  else {
1012
- 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);
1013
1040
  }
1014
1041
  client._v2SenderIKPending.delete(key);
1015
1042
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
1016
1043
  }
1017
1044
  }
1018
1045
  finally {
1019
- client._v2SenderIKFetching.delete(fetchKey);
1046
+ if (generationCurrent())
1047
+ client._v2SenderIKFetching.delete(fetchKey);
1020
1048
  }
1021
1049
  }
1022
1050
  async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
1023
- 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
+ };
1024
1070
  const claim = delivery.beginVolatileDelivery(rawMessage);
1025
1071
  if (claim === 'inflight')
1026
1072
  return;
1027
1073
  if (claim === 'seen') {
1028
- await delivery.ackVolatileMessages([rawMessage], true, false);
1074
+ await ack();
1029
1075
  return;
1030
1076
  }
1031
1077
  try {
1032
1078
  const publicMessage = delivery.publicVolatileMessage(plaintext);
1033
- 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']) {
1034
1080
  if (key in rawMessage)
1035
1081
  publicMessage[key] = rawMessage[key];
1036
1082
  }
1037
1083
  const appEvent = delivery.p2pAppEventForMessage(publicMessage);
1038
- 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();
1039
1089
  delivery.markVolatileSeen(rawMessage);
1040
1090
  }
1041
1091
  finally {
1042
1092
  delivery.finishVolatileDelivery(rawMessage);
1043
1093
  }
1044
- await delivery.ackVolatileMessages([rawMessage], true, false);
1094
+ await ack();
1045
1095
  }
1046
1096
  async sendV2(to, payload, opts) {
1047
1097
  const client = this.client;
@@ -1059,6 +1109,7 @@ export class V2E2EECoordinator {
1059
1109
  messageId: opts?.messageId,
1060
1110
  timestamp: opts?.timestamp,
1061
1111
  deviceId: targetDeviceId || undefined,
1112
+ needRender: opts?.needRender !== false,
1062
1113
  protectedHeaders: opts?.protectedHeaders,
1063
1114
  context: opts?.context,
1064
1115
  useCache,
@@ -1080,6 +1131,7 @@ export class V2E2EECoordinator {
1080
1131
  payload,
1081
1132
  message_id: opts?.messageId,
1082
1133
  timestamp: opts?.timestamp,
1134
+ need_render: opts?.needRender !== false,
1083
1135
  protected_headers: opts?.protectedHeaders,
1084
1136
  context: opts?.context,
1085
1137
  }, result, true);
@@ -1095,6 +1147,7 @@ export class V2E2EECoordinator {
1095
1147
  payload,
1096
1148
  message_id: opts?.messageId,
1097
1149
  timestamp: opts?.timestamp,
1150
+ need_render: opts?.needRender !== false,
1098
1151
  protected_headers: opts?.protectedHeaders,
1099
1152
  context: opts?.context,
1100
1153
  }, result, true);
@@ -1110,13 +1163,14 @@ export class V2E2EECoordinator {
1110
1163
  history_error: { code: 'decrypt_failed', message: reason },
1111
1164
  };
1112
1165
  }
1113
- ensureDeliveryCurrent(ns, generation, inlineGeneration) {
1166
+ ensureDeliveryCurrent(ns, generation, inlineGeneration, namespaceGeneration) {
1114
1167
  if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
1115
1168
  || (inlineGeneration !== undefined && !this.client._delivery.isInlineGenerationCurrent(inlineGeneration))
1169
+ || (namespaceGeneration !== undefined && this.client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
1116
1170
  || this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
1117
1171
  throw new StateError('pull invalidated');
1118
1172
  }
1119
- async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
1173
+ async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false, applicationPage, deferredEffects) {
1120
1174
  const client = this.client;
1121
1175
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
1122
1176
  let plaintext = null;
@@ -1142,13 +1196,16 @@ export class V2E2EECoordinator {
1142
1196
  else if (isJsonObject(payload)
1143
1197
  && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1144
1198
  msg = { ...msg, envelope_json: JSON.stringify(payload) };
1145
- 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);
1146
1202
  }
1147
1203
  }
1148
1204
  else {
1149
1205
  plaintext = source === 'inline_push'
1150
1206
  ? await client._decryptV2Message(msg, false, false, false, false)
1151
- : 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);
1152
1209
  }
1153
1210
  if (!plaintext) {
1154
1211
  if (history)
@@ -1162,15 +1219,18 @@ export class V2E2EECoordinator {
1162
1219
  if (publish) {
1163
1220
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1164
1221
  if (orderedPublish) {
1165
- 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);
1166
1226
  }
1167
1227
  else {
1168
- await client._publishAppEvent(event.event, event.payload, source);
1228
+ await client._publishAppEvent(event.event, event.payload, source, applicationPage?.namespace ?? '', undefined, applicationPage);
1169
1229
  }
1170
1230
  }
1171
1231
  return plaintext;
1172
1232
  }
1173
- async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
1233
+ async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false, applicationPage) {
1174
1234
  const client = this.client;
1175
1235
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
1176
1236
  let plaintext = null;
@@ -1188,10 +1248,13 @@ export class V2E2EECoordinator {
1188
1248
  };
1189
1249
  if (publish) {
1190
1250
  if (orderedPublish) {
1191
- 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);
1192
1255
  }
1193
1256
  else {
1194
- await client._publishAppEvent('group.message_recalled', recallPayload, source);
1257
+ await client._publishAppEvent('group.message_recalled', recallPayload, source, applicationPage?.namespace ?? '', undefined, applicationPage);
1195
1258
  }
1196
1259
  }
1197
1260
  return recallPayload;
@@ -1242,15 +1305,16 @@ export class V2E2EECoordinator {
1242
1305
  client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
1243
1306
  return null;
1244
1307
  }
1308
+ plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
1245
1309
  if (publish && !history && client._delivery.isSelfSentGroupMessage?.(msg)) {
1246
1310
  return plaintext;
1247
1311
  }
1248
1312
  if (publish) {
1249
1313
  if (orderedPublish) {
1250
- 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);
1251
1315
  }
1252
1316
  else {
1253
- await client._publishAppEvent('group.message_created', plaintext, source);
1317
+ await client._publishAppEvent('group.message_created', plaintext, source, applicationPage?.namespace ?? '', undefined, applicationPage);
1254
1318
  }
1255
1319
  }
1256
1320
  return plaintext;
@@ -1280,11 +1344,19 @@ export class V2E2EECoordinator {
1280
1344
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
1281
1345
  const page = validateTailPage(result, afterSeq, limit);
1282
1346
  const decoded = [];
1283
- for (const msg of page.messages) {
1284
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1285
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1286
- if (plaintext)
1287
- 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;
1288
1360
  }
1289
1361
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1290
1362
  if (ns) {
@@ -1292,7 +1364,7 @@ export class V2E2EECoordinator {
1292
1364
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1293
1365
  };
1294
1366
  if (deferPublish)
1295
- client._delivery.deferTailWindow(ns, proof);
1367
+ client._delivery.deferTailWindow(ns, proof, applicationPage);
1296
1368
  else {
1297
1369
  client._seqTracker.commitTailWindow(ns, proof);
1298
1370
  client._persistSeq(ns);
@@ -1306,6 +1378,491 @@ export class V2E2EECoordinator {
1306
1378
  client._delivery.onPullWorkSettled?.();
1307
1379
  return { ...result, messages: decoded, raw_count: page.messages.length };
1308
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
+ }
1309
1866
  async historyV2Internal(params) {
1310
1867
  const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
1311
1868
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
@@ -1464,321 +2021,333 @@ export class V2E2EECoordinator {
1464
2021
  };
1465
2022
  try {
1466
2023
  while (pageCount < maxPages) {
1467
- let pipelinePage;
1468
- if (pipelineRunning) {
1469
- pipelinePage = await pipelineQueue.take();
1470
- if (!pipelinePage) {
1471
- pipelineRunning = false;
1472
- if (pipelineError)
1473
- throw pipelineError;
1474
- break;
1475
- }
1476
- }
1477
- pageCount += 1;
1478
- const volatileCursorBeforePage = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1479
- const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1480
- const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1481
- ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1482
- : null);
1483
- let result;
1484
- if (pipelinePage) {
1485
- nextAfterSeq = pipelinePage.afterSeq;
1486
- result = pipelinePage.result;
1487
- }
1488
- else {
1489
- try {
1490
- result = await client._callRawV2Rpc('message.v2.pull', {
1491
- after_seq: nextAfterSeq,
1492
- limit,
1493
- ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1494
- ...(opts?.force ? { force: true } : {}),
1495
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1496
- }, pullGateKey);
1497
- }
1498
- catch (exc) {
1499
- const text = exc instanceof Error ? exc.message : String(exc);
1500
- if (!volatileCursor || !text.toLowerCase().includes('volatile cursor epoch mismatch'))
1501
- throw exc;
1502
- client._delivery.resetVolatilePullCursor();
1503
- volatileCursor = '';
1504
- pageCount -= 1;
1505
- continue;
1506
- }
1507
- }
1508
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1509
- if (!opts?.force)
1510
- client._delivery.observeBackgroundPullResponse?.(ns, nextAfterSeq, result, limit);
1511
- if (ackUpToSeq > 0) {
1512
- const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1513
- if (actualAckSeq >= ackUpToSeq) {
1514
- if (pendingInlineGeneration !== null) {
1515
- 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;
1516
2034
  }
1517
- if (pendingAckSeq <= ackUpToSeq)
1518
- pendingAckSeq = 0;
1519
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1520
- 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;
1521
2046
  }
1522
2047
  else {
1523
- client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1524
- pendingAckSeq = Math.max(pendingAckSeq, ackUpToSeq);
1525
- this.recordForwardAck(ns, 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
+ }
1526
2066
  }
1527
- }
1528
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1529
- const volatileMessages = client._delivery.volatileMessages(pageMessages);
1530
- if (volatileMessages.length > 0) {
1531
- decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1532
- volatileCursor = client._delivery.volatilePullCursor();
1533
- }
1534
- const volatileAdvanced = Boolean(volatileCursor) && volatileCursor !== volatileCursorBeforePage;
1535
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1536
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1537
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1538
- const hasMore = result.has_more === true;
1539
- const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1540
- 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 ?? '')}`);
1541
- const seqs = messages
1542
- .map((msg) => Number(msg.seq ?? 0))
1543
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1544
- const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1545
- let pageTrackerSnapshot;
1546
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1547
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1548
- if (!pipelineRunning && volatileMessages.length === 0 && shouldContinueForwardPage({
1549
- rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
1550
- hasMoreField, hasMore, serverHead, pendingAckSeq: 0,
1551
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1552
- ? client._seqTracker.getMaxSeenSeq(ns) : pageMaxSeq,
1553
- }))
1554
- startPipeline(pageMaxSeq, volatileCursor);
1555
- const deferredKeyFetches = new Map();
1556
- let blockedSeq = 0;
1557
- const pageDecryptedBefore = decrypted.length;
1558
- for (const msg of messages) {
1559
- const seq = Number(msg.seq ?? 0);
1560
- if (!Number.isFinite(seq) || seq <= 0)
1561
- continue;
1562
- const version = String(msg.version ?? 'v2');
1563
- if (version === 'v1') {
1564
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1565
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1566
- const payloadType = isJsonObject(legacyPayload)
1567
- ? String(legacyPayload.type ?? '').trim()
1568
- : '';
1569
- if (legacyPayload !== undefined && legacyPayload !== null
1570
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1571
- const v1Msg = {
1572
- message_id: String(msg.message_id ?? ''),
1573
- from: String(msg.from_aid ?? ''),
1574
- to: String(legacy.to ?? client._aid ?? ''),
1575
- seq: msg.seq,
1576
- type: String(msg.type ?? ''),
1577
- timestamp: msg.t_server,
1578
- payload: legacyPayload,
1579
- encrypted: false,
1580
- };
1581
- const direction = String(msg.direction ?? '').trim();
1582
- if (direction)
1583
- v1Msg.direction = direction;
1584
- else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1585
- 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);
1586
2075
  }
1587
- if (msg.sender_device_id !== undefined)
1588
- v1Msg.sender_device_id = msg.sender_device_id;
1589
- attachGatewayProximity(v1Msg, msg);
1590
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1591
- if (ns)
1592
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1593
- else
1594
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1595
- decrypted.push(v1Msg);
2076
+ if (pendingAckSeq <= ackUpToSeq)
2077
+ pendingAckSeq = 0;
2078
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2079
+ this.clearForwardAck(ns, ackUpToSeq);
2080
+ }
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);
1596
2085
  }
1597
- else if (isJsonObject(legacyPayload)
1598
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1599
- const deferStatus = {};
1600
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1601
- if (deferStatus.deferred) {
1602
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1603
- if (deferStatus.fromAid) {
1604
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1605
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
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';
1606
2144
  }
1607
- }
1608
- 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);
1609
2149
  if (ns)
1610
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
2150
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false, 'pull', undefined, applicationPage);
1611
2151
  else
1612
- await client._publishAppEvent('message.received', plaintext, 'pull');
1613
- decrypted.push(plaintext);
2152
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull', ns, undefined, applicationPage);
2153
+ decrypted.push(v1Msg);
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)`);
1614
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);
1615
2205
  }
1616
2206
  else {
1617
- 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);
1618
2209
  }
1619
- continue;
1620
2210
  }
1621
- if (version !== 'v2') {
1622
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1623
- continue;
1624
- }
1625
- const msgSpkId = String(msg.spk_id ?? '');
1626
- if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1627
- client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1628
- }
1629
- const deferStatus = {};
1630
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1631
- if (deferStatus.deferred) {
1632
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1633
- }
1634
- if (deferStatus.deferred && deferStatus.fromAid) {
1635
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1636
- deferredKeyFetches.set(key, {
1637
- fromAid: deferStatus.fromAid,
1638
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1639
- groupId: deferStatus.groupId ?? '',
1640
- });
1641
- }
1642
- if (plaintext === null)
1643
- 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));
1644
2215
  if (ns) {
1645
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1646
- decrypted.push(plaintext);
1647
- }
1648
- else {
1649
- await client._publishAppEvent('message.received', plaintext, 'pull');
1650
- decrypted.push(plaintext);
1651
- }
1652
- }
1653
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1654
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1655
- const serverAckSeq = parsedServerAckSeq ?? 0;
1656
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1657
- if (ns) {
1658
- let commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1659
- commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
1660
- let ackSeq = pageContigBefore;
1661
- let contigAdvanced = false;
1662
- try {
1663
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1664
- const state = client._seqTracker.getSyncState?.(ns);
1665
- 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);
1666
- await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough);
1667
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1668
- pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
1669
- client._delivery.commitPendingTailWindow?.(ns, commitTarget);
1670
- if (commitTarget > pageContigBefore) {
1671
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1672
- if (retentionFloor > pageContigBefore) {
1673
- 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
+ }
1674
2240
  }
1675
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1676
- 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);
1677
2248
  }
1678
- if (seqs.length > 0) {
1679
- 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);
1680
2259
  }
2260
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2261
+ throw exc;
1681
2262
  }
1682
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1683
- contigAdvanced = ackSeq !== pageContigBefore;
1684
- if (contigAdvanced) {
1685
- await client._commitSeqTrackerState(ns);
1686
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1687
- if (pageCount === 1 && deferredServerCursor > 0)
1688
- this.clearForwardCursor(ns, ackSeq);
2263
+ for (const fetch of deferredKeyFetches.values()) {
2264
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1689
2265
  }
1690
- client._delivery.completePendingTailWindow?.(ns);
1691
- committedAckSeq = ackSeq;
1692
- }
1693
- catch (exc) {
1694
- if (Number(client._v2RuntimeGeneration ?? 0) === generation
1695
- && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
1696
- && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
1697
- && pageTrackerSnapshot !== undefined
1698
- && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1699
- 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);
1700
2281
  }
1701
- client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1702
- throw exc;
1703
2282
  }
1704
- for (const fetch of deferredKeyFetches.values()) {
1705
- 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;
1706
2310
  }
1707
- if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1708
- 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;
1709
2331
  }
1710
- // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1711
- // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1712
- const knownServerAckSeq = hasServerAckSeq
1713
- ? serverAckSeq
1714
- : (pageCount === 1 ? deferredServerCursor : 0);
1715
- const ackNeeded = ackSeq > 0
1716
- && ackSeq > lastAutoAckSeq
1717
- && ((hasServerAckSeq && ackSeq > serverAckSeq)
1718
- || (contigAdvanced && ackSeq > knownServerAckSeq));
1719
- if (ackNeeded) {
1720
- this.recordForwardAck(ns, ackSeq);
1721
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2332
+ if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2333
+ startPipeline(nextAfter, volatileCursor);
1722
2334
  }
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;
2341
+ }
2342
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2343
+ if (!pipelineRunning && tailDelayMs > 0)
2344
+ await v2Sleep(tailDelayMs);
2345
+ nextAfterSeq = nextAfter;
1723
2346
  }
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 && volatileMessages.length > 0) {
1762
- await stopPipeline();
1763
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1764
- pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1765
- pipelineStopped = false;
1766
- pipelineError = null;
1767
- }
1768
- if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1769
- startPipeline(nextAfter, volatileCursor);
1770
- }
1771
- if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1772
- const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1773
- after_seq: nextAfter, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1774
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1775
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1776
- pullGateKey = nextKey;
2347
+ catch (error) {
2348
+ client._delivery.abortApplicationPage?.(applicationPage, error);
2349
+ throw error;
1777
2350
  }
1778
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1779
- if (!pipelineRunning && tailDelayMs > 0)
1780
- await v2Sleep(tailDelayMs);
1781
- nextAfterSeq = nextAfter;
1782
2351
  }
1783
2352
  }
1784
2353
  finally {
@@ -1795,7 +2364,7 @@ export class V2E2EECoordinator {
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,17 +2418,17 @@ 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
  }
@@ -1959,6 +2529,7 @@ export class V2E2EECoordinator {
1959
2529
  payload,
1960
2530
  messageId: opts?.messageId,
1961
2531
  timestamp: opts?.timestamp,
2532
+ needRender: opts?.needRender !== false,
1962
2533
  protectedHeaders: opts?.protectedHeaders,
1963
2534
  context: opts?.context,
1964
2535
  useCache,
@@ -1982,6 +2553,7 @@ export class V2E2EECoordinator {
1982
2553
  payload,
1983
2554
  message_id: opts?.messageId,
1984
2555
  timestamp: opts?.timestamp,
2556
+ need_render: opts?.needRender !== false,
1985
2557
  protected_headers: opts?.protectedHeaders,
1986
2558
  context: opts?.context,
1987
2559
  }, groupAid), result, true);
@@ -2003,6 +2575,7 @@ export class V2E2EECoordinator {
2003
2575
  payload,
2004
2576
  message_id: opts?.messageId,
2005
2577
  timestamp: opts?.timestamp,
2578
+ need_render: opts?.needRender !== false,
2006
2579
  protected_headers: opts?.protectedHeaders,
2007
2580
  context: opts?.context,
2008
2581
  }, groupAid), result, true);
@@ -2010,7 +2583,7 @@ export class V2E2EECoordinator {
2010
2583
  throw exc;
2011
2584
  }
2012
2585
  }
2013
- async pullGroupV2TailInternal(params, deferPublish = false) {
2586
+ async pullGroupV2TailInternal(params, deferPublish = false, namespaceLocked = false) {
2014
2587
  const client = this.client;
2015
2588
  const generation = Number(client._v2RuntimeGeneration ?? 0);
2016
2589
  const inlineGeneration = client._delivery.captureInlineGeneration?.();
@@ -2041,33 +2614,57 @@ export class V2E2EECoordinator {
2041
2614
  limit,
2042
2615
  _group_cursor_params: explicitCursorParams,
2043
2616
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2044
- 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({
2045
2622
  group_id: groupId, window_mode: 'tail', after_seq: afterSeq, limit,
2046
2623
  _group_cursor_params: explicitCursorParams, _rpc_foreground: true,
2047
- }, groupAid), pullGateKey);
2624
+ }, groupAid), pullGateKey), false);
2048
2625
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2049
2626
  if (resultAid)
2050
2627
  groupAid = resultAid;
2051
2628
  const page = validateTailPage(result, afterSeq, limit);
2052
2629
  const decoded = [];
2053
- for (const msg of page.messages) {
2054
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2055
- client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2056
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2057
- if (plaintext)
2058
- 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);
2059
2640
  }
2060
- 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);
2061
2646
  const proof = {
2062
2647
  windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
2063
2648
  };
2064
2649
  if (deferPublish)
2065
- client._delivery.deferTailWindow(ns, proof);
2650
+ client._delivery.deferTailWindow(ns, proof, applicationPage);
2066
2651
  else {
2067
- client._seqTracker.commitTailWindow(ns, proof);
2068
- client._persistSeq(ns);
2069
- await client._saveSeqTrackerState?.();
2070
- 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
+ }
2071
2668
  }
2072
2669
  const ack = client._seqTracker.getContiguousSeq(ns);
2073
2670
  if (ownsCursor)
@@ -2082,9 +2679,10 @@ export class V2E2EECoordinator {
2082
2679
  let groupAid = String(params.group_aid ?? '').trim();
2083
2680
  const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
2084
2681
  const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
2085
- 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({
2086
2684
  group_id: groupId, before_seq: beforeSeq, limit, _rpc_foreground: true,
2087
- }, groupAid));
2685
+ }, groupAid), key), false);
2088
2686
  const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2089
2687
  if (resultAid)
2090
2688
  groupAid = resultAid;
@@ -2099,7 +2697,6 @@ export class V2E2EECoordinator {
2099
2697
  }
2100
2698
  async pullGroupV2Internal(params) {
2101
2699
  await this.pullGroupV2(params.group_id, params.after_seq, params.limit, {
2102
- gateLocked: true,
2103
2700
  explicitAfterSeq: true,
2104
2701
  force: params.force === true,
2105
2702
  });
@@ -2127,7 +2724,7 @@ export class V2E2EECoordinator {
2127
2724
  group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
2128
2725
  _group_cursor_params: opts?.cursorParams,
2129
2726
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
2130
- return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2727
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2131
2728
  ...(opts ?? {}),
2132
2729
  gateLocked: true,
2133
2730
  }), false);
@@ -2137,7 +2734,7 @@ export class V2E2EECoordinator {
2137
2734
  _group_cursor_params: opts?.cursorParams,
2138
2735
  group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
2139
2736
  window_mode: 'tail', after_seq: afterSeq, limit,
2140
- });
2737
+ }, false, true);
2141
2738
  return Array.isArray(result.messages) ? result.messages : [];
2142
2739
  }
2143
2740
  if (!opts?.gateLocked) {
@@ -2145,21 +2742,21 @@ export class V2E2EECoordinator {
2145
2742
  group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2146
2743
  _group_cursor_params: opts?.cursorParams,
2147
2744
  }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2148
- return await client._runPullSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2745
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
2149
2746
  ...(opts ?? {}),
2150
2747
  gateLocked: true,
2151
- }), true);
2748
+ }), opts?.background !== false);
2152
2749
  }
2153
2750
  const decrypted = [];
2154
2751
  const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
2155
2752
  const cursorParams = opts?.cursorParams ?? {};
2156
2753
  const ownsCursor = opts?.ownsCursor !== false;
2754
+ if (ownsCursor)
2755
+ client._delivery.rememberCommittedSeqState?.(ns);
2756
+ const namespaceGeneration = ownsCursor ? client._delivery.captureSeqNamespaceGeneration?.(ns) : undefined;
2157
2757
  if (ownsCursor)
2158
2758
  client._delivery.onPullStarted?.(ns);
2159
- let pullGateKey = pullGateKeyForClient(client, 'group.v2.pull', {
2160
- group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
2161
- _group_cursor_params: cursorParams,
2162
- }, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2759
+ const background = opts?.background !== false;
2163
2760
  let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
2164
2761
  ? afterSeq
2165
2762
  : (afterSeq || client._seqTracker.getContiguousSeq(ns));
@@ -2171,6 +2768,20 @@ export class V2E2EECoordinator {
2171
2768
  let lastAutoAckSeq = 0;
2172
2769
  let pendingAckSeq = deferredForwardAck;
2173
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
+ };
2174
2785
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
2175
2786
  let pipelineRunning = false;
2176
2787
  let pipelineError = null;
@@ -2190,14 +2801,7 @@ export class V2E2EECoordinator {
2190
2801
  const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
2191
2802
  const inlineAckGeneration = ackUpToSeq > 0
2192
2803
  ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null : null;
2193
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2194
- group_id: wireGroupId,
2195
- after_seq: producerAfterSeq,
2196
- limit,
2197
- ...cursorParams,
2198
- ...(opts?.force ? { force: true } : {}),
2199
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2200
- }, groupAid), pullGateKey);
2804
+ const result = await fetchPage(producerAfterSeq, ackUpToSeq);
2201
2805
  if (pipelineStopped)
2202
2806
  break;
2203
2807
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
@@ -2250,107 +2854,130 @@ export class V2E2EECoordinator {
2250
2854
  };
2251
2855
  try {
2252
2856
  while (pageCount < maxPages) {
2253
- let pipelinePage;
2254
- if (pipelineRunning) {
2255
- pipelinePage = await pipelineQueue.take();
2256
- if (!pipelinePage) {
2257
- pipelineRunning = false;
2258
- if (pipelineError)
2259
- throw pipelineError;
2260
- break;
2261
- }
2262
- }
2263
- pageCount += 1;
2264
- const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2265
- const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2266
- ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
2267
- : null);
2268
- let result;
2269
- if (pipelinePage) {
2270
- nextAfterSeq = pipelinePage.afterSeq;
2271
- result = pipelinePage.result;
2272
- }
2273
- else {
2274
- result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2275
- group_id: wireGroupId,
2276
- after_seq: nextAfterSeq,
2277
- limit,
2278
- ...cursorParams,
2279
- ...(opts?.force ? { force: true } : {}),
2280
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2281
- }, groupAid), pullGateKey);
2282
- }
2283
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2284
- if (ownsCursor && !opts?.force)
2285
- client._delivery.observeBackgroundPullResponse?.(ns, nextAfterSeq, result, limit);
2286
- if (ackUpToSeq > 0) {
2287
- const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
2288
- if (actualAckSeq >= ackUpToSeq) {
2289
- if (pendingInlineGeneration !== null) {
2290
- 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;
2291
2867
  }
2292
- if (pendingAckSeq <= ackUpToSeq)
2293
- pendingAckSeq = 0;
2294
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2295
- 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;
2296
2878
  }
2297
2879
  else {
2298
- client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2299
- pendingAckSeq = Math.max(pendingAckSeq, ackUpToSeq);
2300
- this.recordForwardAck(ns, ackUpToSeq);
2880
+ result = await fetchPage(nextAfterSeq, ackUpToSeq);
2301
2881
  }
2302
- }
2303
- const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2304
- if (resultGroupAid)
2305
- groupAid = resultGroupAid;
2306
- const eventGroupAid = resultGroupAid || groupAid || gid;
2307
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2308
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2309
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2310
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2311
- const hasMore = result.has_more === true;
2312
- const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2313
- const seqs = messages
2314
- .map((msg) => Number(msg.seq ?? 0))
2315
- .filter((seq) => Number.isFinite(seq) && seq > 0);
2316
- const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2317
- let pageTrackerSnapshot;
2318
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
2319
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2320
- if (!pipelineRunning && shouldContinueForwardPage({
2321
- rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
2322
- hasMoreField, hasMore, serverHead, pendingAckSeq: 0,
2323
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2324
- ? client._seqTracker.getMaxSeenSeq(ns) : pageMaxSeq,
2325
- }))
2326
- startPipeline(pageMaxSeq);
2327
- const deferredKeyFetches = new Map();
2328
- let blockedSeq = 0;
2329
- const pageDecryptedBefore = decrypted.length;
2330
- for (const msg of messages) {
2331
- const seq = Number(msg.seq ?? 0);
2332
- if (!Number.isFinite(seq) || seq <= 0)
2333
- continue;
2334
- const version = String(msg.version ?? 'v2');
2335
- if (version === 'v1') {
2336
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2337
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2338
- const payloadObj = isJsonObject(payload) ? payload : null;
2339
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2340
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2341
- && client._delivery.recallEventFromGroupMessage(msg)) {
2342
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2343
- ...msg,
2344
- group_id: gid,
2345
- group_aid: eventGroupAid,
2346
- }, 'pull');
2347
- client._markPublishedSeq(ns, seq);
2348
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2349
- 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);
2350
2900
  }
2351
- if (payloadObj) {
2352
- const payloadType = String(payloadObj.type ?? '').trim();
2353
- 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) {
2354
2981
  let v1Msg = {
2355
2982
  message_id: String(msg.message_id ?? ''),
2356
2983
  from: String(msg.from_aid ?? ''),
@@ -2361,218 +2988,196 @@ export class V2E2EECoordinator {
2361
2988
  timestamp: msg.t_server,
2362
2989
  payload,
2363
2990
  encrypted: false,
2991
+ need_render: msg.need_render !== false,
2364
2992
  };
2365
2993
  if (msg.sender_device_id !== undefined)
2366
2994
  v1Msg.sender_device_id = msg.sender_device_id;
2367
2995
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2368
2996
  attachGatewayProximity(v1Msg, msg);
2369
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2370
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2371
- }
2372
- else {
2373
- client._markPublishedSeq(ns, seq);
2374
- }
2997
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false, 'pull', undefined, applicationPage);
2375
2998
  decrypted.push(v1Msg);
2376
2999
  continue;
2377
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;
2378
3026
  }
2379
- else if (payload !== undefined && payload !== null) {
2380
- let v1Msg = {
2381
- message_id: String(msg.message_id ?? ''),
2382
- from: String(msg.from_aid ?? ''),
2383
- group_id: gid,
2384
- group_aid: eventGroupAid,
2385
- seq: msg.seq,
2386
- type: String(msg.type ?? ''),
2387
- timestamp: msg.t_server,
2388
- payload,
2389
- encrypted: false,
2390
- };
2391
- if (msg.sender_device_id !== undefined)
2392
- v1Msg.sender_device_id = msg.sender_device_id;
2393
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2394
- attachGatewayProximity(v1Msg, msg);
2395
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2396
- decrypted.push(v1Msg);
3027
+ if (version !== 'v2') {
3028
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2397
3029
  continue;
2398
3030
  }
2399
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
2400
- const deferStatus = {};
2401
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2402
- if (deferStatus.deferred) {
2403
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2404
- if (deferStatus.fromAid) {
2405
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2406
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
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;
3035
+ }
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)
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);
3055
+ }
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}`);
2407
3082
  }
2408
- }
2409
- if (plaintext) {
2410
- plaintext.group_id = gid;
2411
- plaintext.group_aid = eventGroupAid;
2412
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2413
- 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}`);
2414
3085
  }
2415
- else {
2416
- 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}`);
2417
3088
  }
2418
- decrypted.push(plaintext);
2419
3089
  }
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);
3097
+ }
3098
+ client._delivery.completePendingTailWindow?.(ns);
3099
+ committedAckSeq = ackSeq;
3100
+ }
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;
2420
3112
  }
2421
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2422
- continue;
2423
- }
2424
- if (version !== 'v2') {
2425
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2426
- continue;
2427
- }
2428
- const deferStatus = {};
2429
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2430
- if (deferStatus.deferred) {
2431
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2432
- }
2433
- if (deferStatus.deferred && deferStatus.fromAid) {
2434
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2435
- deferredKeyFetches.set(key, {
2436
- fromAid: deferStatus.fromAid,
2437
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2438
- groupId: deferStatus.groupId ?? '',
2439
- });
2440
3113
  }
2441
- if (plaintext === null)
2442
- continue;
2443
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2444
- plaintext.group_id = gid;
2445
- plaintext.group_aid = eventGroupAid;
2446
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2447
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
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);
2448
3118
  }
2449
- else {
2450
- client._markPublishedSeq(ns, seq);
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);
2451
3122
  }
2452
- decrypted.push(plaintext);
2453
- }
2454
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2455
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2456
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2457
- const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2458
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2459
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2460
- let commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2461
- commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
2462
- let ackSeq = pageContigBefore;
2463
- let contigAdvanced = false;
2464
- if (ownsCursor) {
2465
- try {
2466
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2467
- const state = client._seqTracker.getSyncState?.(ns);
2468
- 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);
2469
- await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough);
2470
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2471
- pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
2472
- client._delivery.commitPendingTailWindow?.(ns, commitTarget);
2473
- if (commitTarget > pageContigBefore) {
2474
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
2475
- if (effectiveFloor > pageContigBefore) {
2476
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2477
- }
2478
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2479
- client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2480
- }
2481
- if (seqs.length > 0) {
2482
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2483
- }
2484
- }
2485
- ackSeq = client._seqTracker.getContiguousSeq(ns);
2486
- contigAdvanced = ackSeq !== pageContigBefore;
2487
- if (contigAdvanced) {
2488
- await client._commitSeqTrackerState(ns);
2489
- this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2490
- if (pageCount === 1 && deferredServerCursor > 0)
2491
- this.clearForwardCursor(ns, ackSeq);
2492
- }
2493
- client._delivery.completePendingTailWindow?.(ns);
2494
- committedAckSeq = 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);
2495
3136
  }
2496
- catch (exc) {
2497
- if (Number(client._v2RuntimeGeneration ?? 0) === generation
2498
- && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
2499
- && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2500
- && pageTrackerSnapshot !== undefined
2501
- && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2502
- client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
2503
- }
2504
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2505
- throw exc;
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);
2506
3171
  }
3172
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
3173
+ if (!pipelineRunning && tailDelayMs > 0)
3174
+ await v2Sleep(tailDelayMs);
3175
+ nextAfterSeq = nextAfter;
2507
3176
  }
2508
- for (const fetch of deferredKeyFetches.values()) {
2509
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2510
- }
2511
- 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);
2512
- if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2513
- this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2514
- }
2515
- // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2516
- // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2517
- const knownServerCursorSeq = hasServerCursor
2518
- ? cursorCurrentSeq
2519
- : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2520
- const ackNeeded = ackSeq > 0
2521
- && ackSeq > lastAutoAckSeq
2522
- && ownsCursor
2523
- && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2524
- || (contigAdvanced && ackSeq > knownServerCursorSeq));
2525
- if (ackNeeded) {
2526
- this.recordForwardAck(ns, ackSeq);
2527
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2528
- }
2529
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2530
- if (!ownsCursor)
2531
- break;
2532
- const rawCount = messages.length;
2533
- if (blockedSeq > 0)
2534
- break;
2535
- const shouldContinue = shouldContinueForwardPage({
2536
- rawCount,
2537
- nextAfterSeq,
2538
- nextAfter,
2539
- limit,
2540
- hasMoreField,
2541
- hasMore,
2542
- serverHead,
2543
- pendingAckSeq,
2544
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2545
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2546
- });
2547
- const shouldContinueForPipeline = shouldContinueForwardPage({
2548
- rawCount,
2549
- nextAfterSeq,
2550
- nextAfter,
2551
- limit,
2552
- hasMoreField,
2553
- hasMore,
2554
- serverHead,
2555
- pendingAckSeq: 0,
2556
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2557
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2558
- });
2559
- if (!shouldContinue)
2560
- break;
2561
- if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2562
- startPipeline(nextAfter);
2563
- }
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 (!pipelineRunning && tailDelayMs > 0)
2574
- await v2Sleep(tailDelayMs);
2575
- nextAfterSeq = nextAfter;
2576
3181
  }
2577
3182
  }
2578
3183
  finally {
@@ -2583,18 +3188,18 @@ export class V2E2EECoordinator {
2583
3188
  }
2584
3189
  if (pendingAckSeq > 0) {
2585
3190
  this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2586
- await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
3191
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid, background);
2587
3192
  pendingAckSeq = 0;
2588
3193
  }
2589
3194
  client._delivery.onPullWorkSettled?.();
2590
3195
  return deduplicatePulledResults(decrypted);
2591
3196
  }
2592
- async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
3197
+ async confirmForwardGroupAck(groupId, upToSeq, groupAid, background = false) {
2593
3198
  const ns = `group:${String(groupId ?? '').trim()}`;
2594
3199
  const generation = Number(this.client._v2RuntimeGeneration ?? 0);
2595
3200
  const deliveryGeneration = this.client._delivery.captureInlineGeneration?.();
2596
3201
  try {
2597
- const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
3202
+ const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid, background);
2598
3203
  this.ensureDeliveryCurrent(ns, generation, deliveryGeneration);
2599
3204
  const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
2600
3205
  if (actualAckSeq < upToSeq) {
@@ -2613,10 +3218,10 @@ export class V2E2EECoordinator {
2613
3218
  async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
2614
3219
  return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, false);
2615
3220
  }
2616
- async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
2617
- 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);
2618
3223
  }
2619
- async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark) {
3224
+ async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark, background) {
2620
3225
  const client = this.client;
2621
3226
  const gid = typeof groupId === 'string' ? groupId.trim() : '';
2622
3227
  if (!gid)
@@ -2638,7 +3243,9 @@ export class V2E2EECoordinator {
2638
3243
  return { acked: 0 };
2639
3244
  // 与 P2P 一致,必须将 ACK 响应限定到请求开始时对应的 inline generation。
2640
3245
  const ackGeneration = inlineGeneration ?? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null;
2641
- 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));
2642
3249
  const actualAckSeq = requireServerWatermark
2643
3250
  ? (client._delivery?.resolveForwardAckSeq?.(raw, seq) ?? 0)
2644
3251
  : (client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq);
@@ -3301,6 +3908,7 @@ export class V2E2EECoordinator {
3301
3908
  }
3302
3909
  const sender = await session.getSenderIdentity();
3303
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;
3304
3912
  return envelope;
3305
3913
  }
3306
3914
  async buildV2GroupEnvelope(opts) {
@@ -3423,6 +4031,7 @@ export class V2E2EECoordinator {
3423
4031
  }
3424
4032
  const sender = await session.getSenderIdentity();
3425
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;
3426
4035
  return envelope;
3427
4036
  }
3428
4037
  handleGroupChangedSpk(data, groupId, action) {