@agentunion/fastaun 0.5.33 → 0.5.35

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 (113) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +28 -26
  3. package/_packed_docs/CHANGELOG.md +59 -2
  4. package/_packed_docs/INDEX.md +26 -12
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +17 -11
  6. package/_packed_docs/agent.md/SCHEMA.md +1 -1
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +2 -2
  8. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +58 -8
  9. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +17 -17
  10. package/_packed_docs/protocol/echo-probe-gateway.md +99 -0
  11. package/_packed_docs/protocol/echo-probe-sdk.md +91 -0
  12. package/_packed_docs/sdk/0.5.33-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +464 -316
  13. package/_packed_docs/sdk/0.5.33-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +499 -304
  14. package/_packed_docs/sdk/0.5.34-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +133 -0
  15. package/_packed_docs/sdk/0.5.34-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +107 -0
  16. package/_packed_docs/sdk/0.5.35-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +189 -0
  17. package/_packed_docs/sdk/0.5.35-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +204 -0
  18. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -13
  19. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +13 -1
  20. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +19 -9
  21. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +87 -20
  22. package/_packed_docs/sdk/09-collab-rpc-manual.md +48 -30
  23. package/_packed_docs/sdk/09-custody-api-manual.md +3 -3
  24. package/_packed_docs/sdk/09-group-rpc-manual.md +60 -13
  25. package/_packed_docs/sdk/09-message-rpc-manual.md +22 -0
  26. package/_packed_docs/sdk/09-meta-rpc-manual.md +2 -1
  27. package/_packed_docs/sdk/09-payload-reference.md +9 -8
  28. package/_packed_docs/sdk/09-proxy-rpc-manual.md +20 -14
  29. package/_packed_docs/sdk/09-storage-rpc-manual.md +1698 -1659
  30. package/_packed_docs/sdk/09-stream-rpc-manual.md +33 -20
  31. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  32. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +125 -30
  33. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/274/200/345/217/221/347/247/273/346/244/215/346/214/207/345/215/227.md +109 -33
  34. package/_packed_docs/sdk/INDEX.md +30 -18
  35. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +1 -1
  36. 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 +5 -5
  37. 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 +5 -5
  38. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +9 -9
  39. package/_packed_docs/sdk/README.md +11 -6
  40. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/345/256/236/346/226/275/350/256/241/345/210/222.md +400 -0
  41. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/346/226/271/346/241/210.md +485 -0
  42. package/dist/auth.js +39 -45
  43. package/dist/auth.js.map +1 -1
  44. package/dist/client/control-messages.js +2 -2
  45. package/dist/client/control-messages.js.map +1 -1
  46. package/dist/client/delivery.d.ts +12 -1
  47. package/dist/client/delivery.js +281 -46
  48. package/dist/client/delivery.js.map +1 -1
  49. package/dist/client/lifecycle.js +13 -1
  50. package/dist/client/lifecycle.js.map +1 -1
  51. package/dist/client/rpc-budget.d.ts +21 -0
  52. package/dist/client/rpc-budget.js +82 -0
  53. package/dist/client/rpc-budget.js.map +1 -0
  54. package/dist/client/rpc-pipeline.d.ts +5 -0
  55. package/dist/client/rpc-pipeline.js +227 -63
  56. package/dist/client/rpc-pipeline.js.map +1 -1
  57. package/dist/client/sync-channels.d.ts +90 -0
  58. package/dist/client/sync-channels.js +508 -0
  59. package/dist/client/sync-channels.js.map +1 -0
  60. package/dist/client/v2-e2ee.d.ts +5 -1
  61. package/dist/client/v2-e2ee.js +420 -303
  62. package/dist/client/v2-e2ee.js.map +1 -1
  63. package/dist/client.d.ts +17 -0
  64. package/dist/client.js +373 -50
  65. package/dist/client.js.map +1 -1
  66. package/dist/events.d.ts +1 -0
  67. package/dist/events.js +5 -0
  68. package/dist/events.js.map +1 -1
  69. package/dist/index.d.ts +3 -1
  70. package/dist/index.js +1 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/mail-client.d.ts +101 -0
  73. package/dist/mail-client.js +369 -0
  74. package/dist/mail-client.js.map +1 -0
  75. package/dist/pubsub/binding.js +23 -3
  76. package/dist/pubsub/binding.js.map +1 -1
  77. package/dist/pubsub/channel.d.ts +15 -3
  78. package/dist/pubsub/channel.js +114 -7
  79. package/dist/pubsub/channel.js.map +1 -1
  80. package/dist/pubsub/client.d.ts +17 -2
  81. package/dist/pubsub/client.js +205 -40
  82. package/dist/pubsub/client.js.map +1 -1
  83. package/dist/pubsub/credentials.d.ts +3 -0
  84. package/dist/pubsub/credentials.js +57 -20
  85. package/dist/pubsub/credentials.js.map +1 -1
  86. package/dist/pubsub/index.d.ts +5 -2
  87. package/dist/pubsub/index.js +70 -54
  88. package/dist/pubsub/index.js.map +1 -1
  89. package/dist/pubsub/service.d.ts +9 -0
  90. package/dist/pubsub/service.js +266 -20
  91. package/dist/pubsub/service.js.map +1 -1
  92. package/dist/pubsub/types.d.ts +13 -1
  93. package/dist/storage/lowlevel.d.ts +3 -1
  94. package/dist/storage/lowlevel.js +13 -0
  95. package/dist/storage/lowlevel.js.map +1 -1
  96. package/dist/storage/types.d.ts +10 -0
  97. package/dist/storage/types.js.map +1 -1
  98. package/dist/storage/vfs.d.ts +3 -1
  99. package/dist/storage/vfs.js +2 -2
  100. package/dist/storage/vfs.js.map +1 -1
  101. package/dist/tools/cross-sdk-agent.d.ts +55 -5
  102. package/dist/tools/cross-sdk-agent.js +88 -21
  103. package/dist/tools/cross-sdk-agent.js.map +1 -1
  104. package/dist/transport.d.ts +16 -0
  105. package/dist/transport.js +129 -7
  106. package/dist/transport.js.map +1 -1
  107. package/dist/udp-standby.d.ts +89 -0
  108. package/dist/udp-standby.js +487 -0
  109. package/dist/udp-standby.js.map +1 -0
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +5 -1
  113. package/scripts/patch-imapflow-external.mjs +69 -0
@@ -14,6 +14,11 @@ const GROUP_RECALL_SEEN_LIMIT = 10_000;
14
14
  const VOLATILE_SEEN_LIMIT = 10_000;
15
15
  const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
16
16
  const BACKGROUND_PULL_RETRY_DELAY_MS = 250;
17
+ function isOversizedPullError(error) {
18
+ const failure = error;
19
+ return [failure?.data?.code, failure?.data?.reason, failure?.stringCode].includes('OVERSIZED_MESSAGE')
20
+ || String(failure?.message ?? '').includes('OVERSIZED_MESSAGE');
21
+ }
17
22
  function isTerminalGroupPullError(error) {
18
23
  if (!error || typeof error !== 'object')
19
24
  return false;
@@ -99,9 +104,73 @@ function deduplicatePlainForwardPageMessages(value, afterSeq) {
99
104
  }
100
105
  export class MessageDeliveryEngine {
101
106
  runtime;
107
+ forwardAckJobs = new Map();
108
+ forwardAckTasks = new Set();
109
+ scheduleForwardAck(ns, seq, send) {
110
+ const client = this.runtime.client;
111
+ if (client._closing || seq <= 0)
112
+ return;
113
+ const scope = () => [client._aid, client._deviceId, client._slotId, this.captureInlineAckGeneration(),
114
+ client._v2RuntimeGeneration, client._transport, client._transport?._connectionGeneration,
115
+ client._transport?._workerGeneration, this.captureSeqNamespaceGeneration(ns)];
116
+ const current = scope();
117
+ const previous = this.forwardAckJobs.get(ns);
118
+ if (previous && previous.scope.every((value, i) => value === current[i])) {
119
+ if (seq > previous.seq) {
120
+ previous.seq = seq;
121
+ previous.send = send;
122
+ }
123
+ return;
124
+ }
125
+ const job = { seq, scope: current, send };
126
+ this.forwardAckJobs.set(ns, job);
127
+ const isCurrent = () => this.forwardAckJobs.get(ns) === job && job.scope.every((value, i) => value === scope()[i]);
128
+ const task = (async () => {
129
+ let failures = 0;
130
+ while (isCurrent()) {
131
+ const pending = job.seq;
132
+ try {
133
+ await job.send(pending);
134
+ failures = 0;
135
+ if (job.seq <= pending) {
136
+ if (this.forwardAckJobs.get(ns) === job)
137
+ this.forwardAckJobs.delete(ns);
138
+ return;
139
+ }
140
+ }
141
+ catch {
142
+ if (!isCurrent() || client._closing || client.state !== ConnectionState.READY)
143
+ return;
144
+ if (job.seq <= pending)
145
+ await new Promise(resolve => setTimeout(resolve, Math.min(100 * 2 ** Math.min(failures++, 5), 3000)));
146
+ }
147
+ }
148
+ })().finally(() => {
149
+ if (this.forwardAckJobs.get(ns) === job)
150
+ this.forwardAckJobs.delete(ns);
151
+ this.forwardAckTasks.delete(task);
152
+ });
153
+ this.forwardAckTasks.add(task);
154
+ }
155
+ drainForwardAcks() {
156
+ if (!this.forwardAckTasks.size)
157
+ return;
158
+ return this.waitForwardAcks();
159
+ }
160
+ async waitForwardAcks() {
161
+ let timer;
162
+ try {
163
+ await Promise.race([Promise.allSettled([...this.forwardAckTasks]), new Promise(resolve => { timer = setTimeout(resolve, 1000); })]);
164
+ }
165
+ finally {
166
+ if (timer)
167
+ clearTimeout(timer);
168
+ }
169
+ }
102
170
  realtimeTailHeads = null;
103
171
  realtimeSyncing = null;
104
172
  pendingGroupPullUpper = null;
173
+ pendingV2GroupPulls = new Set();
105
174
  onlineUnreadHintTargets = null;
106
175
  onlineUnreadHintTasks = null;
107
176
  backgroundPullNeeds = new Map();
@@ -135,6 +204,9 @@ export class MessageDeliveryEngine {
135
204
  seqTrackerNamespaceGenerations = new Map();
136
205
  peerRecoveryTasks = new Map();
137
206
  peerRecoveryNeeds = new Map();
207
+ peerVolatileNeeds = new Set();
208
+ peerVolatileTask = null;
209
+ oversizedPulls = new Set();
138
210
  peerForwardNeeds = new Map();
139
211
  peerPushVersions = new Map();
140
212
  peerCatalogVersion = null;
@@ -163,6 +235,7 @@ export class MessageDeliveryEngine {
163
235
  this.automaticPullFailures.clear();
164
236
  this.terminalGroupPulls.clear();
165
237
  this.groupPullVersions.clear();
238
+ this.oversizedPulls.clear();
166
239
  }
167
240
  groupPullNamespace(namespace) {
168
241
  let groupId = namespace.startsWith('group_event:') ? namespace.slice('group_event:'.length)
@@ -267,6 +340,17 @@ export class MessageDeliveryEngine {
267
340
  catch (error) {
268
341
  if (!groupCurrent())
269
342
  throw new Error('pull invalidated');
343
+ if (!signal.aborted && generation === this.captureInlineAckGeneration() && isOversizedPullError(error)) {
344
+ this.oversizedPulls.add(namespace);
345
+ this.automaticPullFailures.delete(namespace);
346
+ this.backgroundPullNeeds.delete(namespace);
347
+ const peer = this.peerRecoveryNeeds.get(this.peerSender(namespace));
348
+ if (peer)
349
+ peer.paused = true;
350
+ if (this.groupPullNamespace(namespace))
351
+ this.stopAutomaticGroupPull(namespace, error);
352
+ throw error;
353
+ }
270
354
  if (!signal.aborted && generation === this.captureInlineAckGeneration()
271
355
  && this.groupPullNamespace(namespace) && isTerminalGroupPullError(error)) {
272
356
  this.stopAutomaticGroupPull(namespace, error);
@@ -471,6 +555,8 @@ export class MessageDeliveryEngine {
471
555
  this.peerPullConfirmedThrough.clear();
472
556
  this.peerServerConfirmedAcks.clear();
473
557
  this.peerRecoveryTasks.clear();
558
+ this.peerVolatileNeeds.clear();
559
+ this.peerVolatileTask = null;
474
560
  this.settledDeliveries.clear();
475
561
  this.deliveryInflight.clear();
476
562
  this.peerCatalogTask = null;
@@ -791,6 +877,8 @@ export class MessageDeliveryEngine {
791
877
  }
792
878
  refreshPendingPeers() {
793
879
  this.ensurePeerContext();
880
+ if (this.oversizedPulls.has('message.peers'))
881
+ return;
794
882
  this.peerCatalogVersion = {};
795
883
  if (!this.hasP2pApplicationSubscription())
796
884
  return;
@@ -818,15 +906,57 @@ export class MessageDeliveryEngine {
818
906
  }
819
907
  })();
820
908
  this.peerCatalogTask = task.then(() => ({}), () => ({}));
821
- client._safeAsync(task.catch(() => {
909
+ client._safeAsync(task.catch((error) => {
822
910
  if (generation !== this.captureInlineAckGeneration())
823
911
  return;
912
+ if (isOversizedPullError(error))
913
+ return;
824
914
  const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
825
915
  this.refreshPendingPeers(); }, BACKGROUND_PULL_RETRY_DELAY_MS);
826
916
  timer.unref?.();
827
917
  }).finally(() => { if (generation === this.captureInlineAckGeneration())
828
918
  this.peerCatalogTask = null; }));
829
919
  }
920
+ schedulePeerVolatileRecovery(sender) {
921
+ const client = this.runtime.client;
922
+ const namespace = `${this.peerNamespace(sender)}|volatile`;
923
+ if (this.oversizedPulls.has(namespace))
924
+ return;
925
+ this.peerVolatileNeeds.add(sender);
926
+ if (this.peerVolatileTask)
927
+ return;
928
+ const generation = this.captureInlineAckGeneration(), context = this.ensurePeerContext();
929
+ const current = () => generation === this.captureInlineAckGeneration() && context === this.ensurePeerContext();
930
+ const task = (async () => {
931
+ while (current() && this.peerVolatileNeeds.size && this.hasP2pApplicationSubscription()
932
+ && client._sessionOptions?.background_sync !== false && !client._closing) {
933
+ const peer = this.peerVolatileNeeds.values().next().value;
934
+ this.peerVolatileNeeds.delete(peer);
935
+ try {
936
+ const result = await client._v2E2EE.pullPeerV2Volatile(peer, { background: true, retainMessages: false, maxPages: 1 });
937
+ if (!current())
938
+ return;
939
+ if (!result.complete)
940
+ this.peerVolatileNeeds.add(peer);
941
+ }
942
+ catch (error) {
943
+ if (!current())
944
+ return;
945
+ if (isOversizedPullError(error)) {
946
+ this.oversizedPulls.add(`${this.peerNamespace(peer)}|volatile`);
947
+ client._dispatcher.enqueue('sync.error', { namespace: `${this.peerNamespace(peer)}|volatile`, method: 'message.v2.pull', code: 'OVERSIZED_MESSAGE', error: String(error) });
948
+ }
949
+ else
950
+ client._safeAsync(Promise.reject(error));
951
+ }
952
+ }
953
+ })();
954
+ this.peerVolatileTask = task;
955
+ client._safeAsync(task.finally(() => {
956
+ if (current() && this.peerVolatileTask === task)
957
+ this.peerVolatileTask = null;
958
+ }));
959
+ }
830
960
  schedulePeerRecovery(sender, head = 0, volatile = false) {
831
961
  const client = this.runtime.client;
832
962
  if (!sender || client._closing)
@@ -855,6 +985,11 @@ export class MessageDeliveryEngine {
855
985
  this.peerForwardNeeds.set(namespace, {});
856
986
  return;
857
987
  }
988
+ if (volatile && client._syncChannelCoordinator?.needsSplit?.()
989
+ && (this.peerRecoveryTasks.has(sender) || this.oversizedPulls.has(namespace)))
990
+ this.schedulePeerVolatileRecovery(sender);
991
+ if (this.oversizedPulls.has(namespace))
992
+ return;
858
993
  const need = this.peerRecoveryNeeds.get(sender) ?? { head: 0, volatile: false, pending: false };
859
994
  need.paused = false;
860
995
  need.head = Math.max(need.head, head);
@@ -953,9 +1088,11 @@ export class MessageDeliveryEngine {
953
1088
  }
954
1089
  })();
955
1090
  this.peerRecoveryTasks.set(sender, task);
956
- client._safeAsync(task.catch(() => {
1091
+ client._safeAsync(task.catch((error) => {
957
1092
  if (generation !== this.captureInlineAckGeneration())
958
1093
  return;
1094
+ if (isOversizedPullError(error) || need.paused)
1095
+ return;
959
1096
  const timer = setTimeout(() => {
960
1097
  if (generation === this.captureInlineAckGeneration() && this.peerRecoveryNeeds.get(sender) === need
961
1098
  && !need.paused && !this.peerRecoveryTasks.has(sender))
@@ -1033,7 +1170,7 @@ export class MessageDeliveryEngine {
1033
1170
  this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
1034
1171
  }
1035
1172
  }
1036
- async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
1173
+ async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '', background = false) {
1037
1174
  if (!isJsonObject(result)) {
1038
1175
  throw new Error(`${method} response must be an object`);
1039
1176
  }
@@ -1177,7 +1314,10 @@ export class MessageDeliveryEngine {
1177
1314
  throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
1178
1315
  }
1179
1316
  this.ensurePullOperationCurrent(ns);
1180
- await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
1317
+ if (background)
1318
+ this.scheduleForwardAck(ns, pendingAckSeq, seq => this.confirmPlainForwardAck(ns, ackMethod, seq, groupId));
1319
+ else
1320
+ await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
1181
1321
  }
1182
1322
  this.syncProgress(ns, {
1183
1323
  rawCount: messages.length,
@@ -1385,6 +1525,7 @@ export class MessageDeliveryEngine {
1385
1525
  this.pendingGroupInlineAckNamespaces = null;
1386
1526
  this.realtimeSyncing = null;
1387
1527
  this.pendingGroupPullUpper = null;
1528
+ this.pendingV2GroupPulls.clear();
1388
1529
  this.onlineUnreadHintTargets = null;
1389
1530
  this.onlineUnreadHintTasks = null;
1390
1531
  this.realtimeTailHeads = null;
@@ -1399,6 +1540,8 @@ export class MessageDeliveryEngine {
1399
1540
  this.peerPullConfirmedThrough.clear();
1400
1541
  this.peerServerConfirmedAcks.clear();
1401
1542
  this.peerRecoveryTasks.clear();
1543
+ this.peerVolatileNeeds.clear();
1544
+ this.peerVolatileTask = null;
1402
1545
  }
1403
1546
  deliveryChangedNamespace(event, payload) {
1404
1547
  if (event === 'group.message_created' && isJsonObject(payload)) {
@@ -1669,8 +1812,31 @@ export class MessageDeliveryEngine {
1669
1812
  pendingPullUpper(ns) {
1670
1813
  return this.pendingGroupPullUpper?.get(ns) ?? 0;
1671
1814
  }
1815
+ pendingHistoryNamespaces() {
1816
+ const namespaces = new Set(), client = this.runtime.client, now = Date.now();
1817
+ for (const [sender, need] of this.peerRecoveryNeeds) {
1818
+ const ns = this.peerNamespace(sender);
1819
+ if (need.pending && !need.paused && !this.peerRecoveryTasks.has(sender) && !this.oversizedPulls.has(ns)
1820
+ && (this.automaticPullFailures.get(ns)?.retryAt ?? 0) <= now)
1821
+ namespaces.add(ns);
1822
+ }
1823
+ for (const [ns, need] of this.backgroundPullNeeds) {
1824
+ if (need.messageClassRequest || need.retryAt > now || !(need.probe || need.hasMore)
1825
+ || this.automaticGroupPullStopped(ns) || this.oversizedPulls.has(ns) || this.backgroundPullTasks.has(ns)
1826
+ || this.realtimeSyncing?.has(ns) || this.onlineUnreadHintTasks?.has(ns))
1827
+ continue;
1828
+ const eventScope = ns.startsWith('group_event:');
1829
+ if ((!eventScope && client._rpcPipeline?.hasPullActivity?.(ns, false))
1830
+ || (eventScope && ['pulling', 'processing', 'pulled'].includes(client._rpcPipeline?.getPullLifecycle?.(ns))))
1831
+ continue;
1832
+ if (ns.startsWith('group:') || eventScope || ns === `p2p:${client._aid}`)
1833
+ namespaces.add(ns);
1834
+ }
1835
+ return [...namespaces];
1836
+ }
1672
1837
  hasAnyPendingPull() {
1673
1838
  return this.peerCatalogDirty || [...this.peerRecoveryNeeds.values()].some((need) => !need.paused) || this.peerRecoveryTasks.size > 0 || Boolean(this.peerCatalogTask)
1839
+ || Boolean(this.peerVolatileTask) || this.peerVolatileNeeds.size > 0
1674
1840
  || [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
1675
1841
  || [...this.backgroundPullNeeds.values()].some((need) => need.retryAt !== Infinity)
1676
1842
  || Boolean(this.onlineUnreadHintTasks?.size)
@@ -1696,7 +1862,7 @@ export class MessageDeliveryEngine {
1696
1862
  return;
1697
1863
  this.ensurePeerContext();
1698
1864
  const ns = `message_class/${params.message_class}/${group ? params.volatile ? 'volatile_group' : 'group' : 'peer'}/${group ? params.group_id ?? params.group_aid : params.sender_aid}`;
1699
- if (this.automaticGroupPullStopped(ns))
1865
+ if (this.automaticGroupPullStopped(ns) || this.oversizedPulls.has(ns))
1700
1866
  return;
1701
1867
  this.recordBackgroundPullNeed(ns, 0, true, true);
1702
1868
  this.backgroundPullNeeds.get(ns).messageClassRequest = { ...params };
@@ -1740,7 +1906,7 @@ export class MessageDeliveryEngine {
1740
1906
  }
1741
1907
  }
1742
1908
  recordBackgroundPullNeed(ns, throughSeq = 0, probe = false, restart = false) {
1743
- if (this.automaticGroupPullStopped(ns))
1909
+ if (this.automaticGroupPullStopped(ns) || this.oversizedPulls.has(ns))
1744
1910
  return;
1745
1911
  const client = this.runtime.client;
1746
1912
  if (client._sessionOptions?.background_sync === false || !client._aid
@@ -1759,7 +1925,7 @@ export class MessageDeliveryEngine {
1759
1925
  need.probe ||= probe;
1760
1926
  this.backgroundPullNeeds.set(ns, need);
1761
1927
  }
1762
- observeBackgroundPullResponse(ns, afterSeq, result, limit = 50) {
1928
+ observeBackgroundPullResponse(ns, afterSeq, result, limit = _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize) {
1763
1929
  if (!isJsonObject(result))
1764
1930
  return;
1765
1931
  const response = result;
@@ -1785,7 +1951,7 @@ export class MessageDeliveryEngine {
1785
1951
  finishBackgroundPull(ns, generation, afterSeq, failed, trigger) {
1786
1952
  if (generation !== this.captureInlineAckGeneration())
1787
1953
  return;
1788
- if (this.automaticGroupPullStopped(ns))
1954
+ if (this.automaticGroupPullStopped(ns) || this.oversizedPulls.has(ns))
1789
1955
  return;
1790
1956
  const need = this.backgroundPullNeeds.get(ns);
1791
1957
  if (need && trigger && need.trigger !== trigger) {
@@ -1812,7 +1978,7 @@ export class MessageDeliveryEngine {
1812
1978
  this.requestBackgroundPullRecovery();
1813
1979
  }
1814
1980
  requestBackgroundPullRecovery() {
1815
- if (this.backgroundPullCheckQueued || (this.backgroundPullNeeds.size === 0 && !this.peerCatalogDirty && this.peerRecoveryNeeds.size === 0))
1981
+ if (this.backgroundPullCheckQueued || (this.backgroundPullNeeds.size === 0 && !this.peerCatalogDirty && this.peerRecoveryNeeds.size === 0 && this.peerVolatileNeeds.size === 0))
1816
1982
  return;
1817
1983
  this.backgroundPullCheckQueued = true;
1818
1984
  const generation = this.captureInlineAckGeneration();
@@ -1824,6 +1990,8 @@ export class MessageDeliveryEngine {
1824
1990
  if (client._sessionOptions?.background_sync === false || client.state !== ConnectionState.READY || client._closing)
1825
1991
  return;
1826
1992
  if (client._p2pPeerPullEnabled) {
1993
+ if (this.peerVolatileNeeds.size && !this.peerVolatileTask)
1994
+ this.schedulePeerVolatileRecovery(this.peerVolatileNeeds.values().next().value);
1827
1995
  if (this.peerCatalogDirty && !this.peerCatalogTask)
1828
1996
  this.refreshPendingPeers();
1829
1997
  for (const [sender, need] of this.peerRecoveryNeeds) {
@@ -1837,6 +2005,11 @@ export class MessageDeliveryEngine {
1837
2005
  this.backgroundPullNeeds.delete(ns);
1838
2006
  continue;
1839
2007
  }
2008
+ if (this.pendingV2GroupPulls.has(ns)) {
2009
+ if (!client._v2Session)
2010
+ continue;
2011
+ this.pendingV2GroupPulls.delete(ns);
2012
+ }
1840
2013
  if (ns.startsWith('p2p:') && !this.hasP2pApplicationSubscription())
1841
2014
  continue;
1842
2015
  if (need.messageClassRequest && need.messageClassRequest.message_class !== 'sdk_control') {
@@ -2262,6 +2435,8 @@ export class MessageDeliveryEngine {
2262
2435
  return applicationResult({ ...result, message_class: 'sdk_control' });
2263
2436
  if (!APP_SEND_ENVELOPE_METHODS.has(method) || !isJsonObject(result))
2264
2437
  return result;
2438
+ if (result.status === 'echo_return')
2439
+ return result;
2265
2440
  let out = { ...result };
2266
2441
  const serverEnvelope = method === 'group.send' && isJsonObject(out.envelope)
2267
2442
  ? { ...out.envelope }
@@ -2586,8 +2761,10 @@ export class MessageDeliveryEngine {
2586
2761
  return this.runApplicationPage(namespace, source, async () => { await this.publishAppEvent(event, payload, source, deliveryContext); });
2587
2762
  }
2588
2763
  }
2589
- if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
2590
- client._maybeAppendEchoTraceReceive(payload);
2764
+ if (source !== 'echo_return' && (event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
2765
+ if (client._maybeAppendEchoTraceReceive(payload)) {
2766
+ return client._returnReceivedEcho(payload);
2767
+ }
2591
2768
  }
2592
2769
  this.logAppMessagePublish(event, payload, source);
2593
2770
  if (isJsonObject(payload)) {
@@ -2829,7 +3006,14 @@ export class MessageDeliveryEngine {
2829
3006
  }
2830
3007
  async onRawMessageReceived(data) {
2831
3008
  const client = this.runtime.client;
2832
- if (client._p2pPeerPullEnabled) {
3009
+ if (isJsonObject(data) && !client._echoStarts.has(data))
3010
+ client._echoStarts.set(data, performance.now());
3011
+ const message = isJsonObject(data) ? data : undefined;
3012
+ const payload = message?.payload;
3013
+ const plainEcho = message && !message.encrypted && !message.encrypt && isJsonObject(payload)
3014
+ && !String(payload.type ?? '').trim().toLowerCase().startsWith('e2ee') && !payload.ciphertext
3015
+ && !client._isEncryptedPushMessage(message) && client._isEchoPayload(payload);
3016
+ if (client._p2pPeerPullEnabled && !plainEcho) {
2833
3017
  await this.onPeerPush(data);
2834
3018
  return;
2835
3019
  }
@@ -2898,6 +3082,7 @@ export class MessageDeliveryEngine {
2898
3082
  return;
2899
3083
  }
2900
3084
  const msg = { ...data };
3085
+ client._echoStarts.set(msg, client._echoStarts.get(data) ?? performance.now());
2901
3086
  if (!this.messageTargetsCurrentInstance(msg)) {
2902
3087
  client._clientLog.debug(`P2P push filtered by instance: message_id=${String(msg.message_id ?? '')}, seq=${String(msg.seq ?? '')}, target_device=${String(msg.device_id ?? '')}, target_slot=${String(msg.slot_id ?? '')}, local_device=${client._deviceId}, local_slot=${client._slotId}`);
2903
3088
  return;
@@ -2954,6 +3139,8 @@ export class MessageDeliveryEngine {
2954
3139
  }
2955
3140
  async onRawGroupMessageCreated(data) {
2956
3141
  const client = this.runtime.client;
3142
+ if (isJsonObject(data) && !client._echoStarts.has(data))
3143
+ client._echoStarts.set(data, performance.now());
2957
3144
  const tStart = Date.now();
2958
3145
  if (isJsonObject(data)) {
2959
3146
  client._logMessageDebug('server-push', '_raw.group.message_created', 'group.message_created', data);
@@ -2994,6 +3181,7 @@ export class MessageDeliveryEngine {
2994
3181
  }
2995
3182
  const msg = { ...data };
2996
3183
  const groupId = String(msg.group_id ?? '');
3184
+ client._echoStarts.set(msg, client._echoStarts.get(data) ?? performance.now());
2997
3185
  const seq = msg.seq;
2998
3186
  const payload = msg.payload;
2999
3187
  if (groupId) {
@@ -3068,12 +3256,15 @@ export class MessageDeliveryEngine {
3068
3256
  const afterSeq = client._seqTracker.getContiguousSeq(ns);
3069
3257
  client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
3070
3258
  const operation = async () => {
3071
- const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
3259
+ let count;
3260
+ const messages = await client._pullGroupV2(groupId, afterSeq, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, {
3261
+ gateLocked: true, background: true, retainMessages: false, onMessageCount: (value) => { count = value; },
3262
+ });
3072
3263
  this.prunePushedSeqs(ns);
3073
- return messages.length;
3264
+ return count ?? messages.length;
3074
3265
  };
3075
3266
  if (client._rpcPipeline) {
3076
- const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
3267
+ const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize });
3077
3268
  await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, true));
3078
3269
  }
3079
3270
  else {
@@ -3107,9 +3298,9 @@ export class MessageDeliveryEngine {
3107
3298
  try {
3108
3299
  // Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
3109
3300
  // 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
3110
- const operation = () => client._pullV2(afterSeq, 50, { gateLocked: true });
3301
+ const operation = () => client._pullV2(afterSeq, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, { gateLocked: true, background: true });
3111
3302
  const messages = await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline
3112
- ? client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), attempt, true)
3303
+ ? client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize }), attempt, true)
3113
3304
  : client._withBackgroundRpc(attempt));
3114
3305
  filled = messages.length;
3115
3306
  this.prunePushedSeqs(ns);
@@ -3149,11 +3340,14 @@ export class MessageDeliveryEngine {
3149
3340
  let filled = 0;
3150
3341
  let failed = false;
3151
3342
  try {
3152
- const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
3343
+ let count;
3344
+ const operation = () => client._pullGroupV2(groupId, afterSeq, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, {
3345
+ gateLocked: true, background: true, retainMessages: false, onMessageCount: (value) => { count = value; },
3346
+ });
3153
3347
  const messages = await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline
3154
- ? client._rpcPipeline.runPullNamespaceSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), attempt, true)
3348
+ ? client._rpcPipeline.runPullNamespaceSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize }), attempt, true)
3155
3349
  : client._withBackgroundRpc(attempt));
3156
- filled = messages.length;
3350
+ filled = count ?? messages.length;
3157
3351
  this.prunePushedSeqs(ns);
3158
3352
  client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
3159
3353
  }
@@ -3263,7 +3457,7 @@ export class MessageDeliveryEngine {
3263
3457
  return;
3264
3458
  if (generation !== this.captureInlineAckGeneration())
3265
3459
  return;
3266
- this.observeBackgroundPullResponse(ns, nextAfterSeq, result);
3460
+ this.observeBackgroundPullResponse(ns, nextAfterSeq, result, request.limit);
3267
3461
  const events = result.events;
3268
3462
  if (!Array.isArray(events))
3269
3463
  return;
@@ -3355,12 +3549,15 @@ export class MessageDeliveryEngine {
3355
3549
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
3356
3550
  const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
3357
3551
  try {
3358
- const ackPromise = client._transport.call('group.ack_events', {
3552
+ const ackParams = {
3359
3553
  group_id: groupId,
3360
3554
  event_seq: ackSeq,
3361
3555
  device_id: client._deviceId,
3362
3556
  slot_id: client._slotId,
3363
- }, undefined, undefined, true);
3557
+ };
3558
+ const ackPromise = !singlePage && client._rpcPipeline
3559
+ ? client._rpcPipeline.rawCall('group.ack_events', ackParams, { signed: false, background: true })
3560
+ : client._transport.call('group.ack_events', ackParams, undefined, undefined, true);
3364
3561
  if (singlePage)
3365
3562
  await ackPromise;
3366
3563
  else
@@ -3716,7 +3913,7 @@ export class MessageDeliveryEngine {
3716
3913
  return requestedSeq;
3717
3914
  return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
3718
3915
  }
3719
- async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
3916
+ async runP2PRealtimeSync(ns, pushSeq, pageLimit = _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, forceTail = false) {
3720
3917
  const client = this.runtime.client;
3721
3918
  const before = this.syncState(ns);
3722
3919
  const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
@@ -3745,12 +3942,12 @@ export class MessageDeliveryEngine {
3745
3942
  const tailResult = await client._v2E2EE.pullV2TailInternal({
3746
3943
  window_mode: 'tail',
3747
3944
  after_seq: after,
3748
- limit: pageLimit,
3945
+ limit: Math.min(pageLimit, 50),
3749
3946
  }, true);
3750
3947
  tailHead = this.syncState(ns).head;
3751
3948
  return tailResult;
3752
3949
  }
3753
- const opts = { gateLocked: true };
3950
+ const opts = { gateLocked: true, background };
3754
3951
  if (request.max_pages !== undefined)
3755
3952
  opts.maxPages = request.max_pages;
3756
3953
  const messages = await client._pullV2(after, pageLimit, opts);
@@ -3762,7 +3959,7 @@ export class MessageDeliveryEngine {
3762
3959
  return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullSerialized(key, attempt, background));
3763
3960
  };
3764
3961
  if (order.length === 1 && order[0] === 'tail') {
3765
- result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
3962
+ result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: Math.min(pageLimit, 50) }, false);
3766
3963
  }
3767
3964
  const state = this.syncState(ns);
3768
3965
  const needsForward = state.ack < state.tail - 1;
@@ -3793,7 +3990,7 @@ export class MessageDeliveryEngine {
3793
3990
  }
3794
3991
  return result;
3795
3992
  }
3796
- async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
3993
+ async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, forceTail = false) {
3797
3994
  const client = this.runtime.client;
3798
3995
  const before = this.syncState(ns);
3799
3996
  const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
@@ -3828,11 +4025,13 @@ export class MessageDeliveryEngine {
3828
4025
  tailHead = this.syncState(ns).head;
3829
4026
  return tailResult;
3830
4027
  }
3831
- const opts = { gateLocked: true, background };
4028
+ let count;
4029
+ const opts = { gateLocked: true, background, retainMessages: !background,
4030
+ onMessageCount: (value) => { count = value; } };
3832
4031
  if (request.max_pages !== undefined)
3833
4032
  opts.maxPages = request.max_pages;
3834
4033
  const messages = await client._pullGroupV2(groupId, after, pageLimit, opts);
3835
- return { messages, raw_count: messages.length };
4034
+ return { messages, raw_count: count ?? messages.length };
3836
4035
  };
3837
4036
  if (!client._rpcPipeline)
3838
4037
  return await this.runAutomaticPull(ns, invoke);
@@ -3874,7 +4073,7 @@ export class MessageDeliveryEngine {
3874
4073
  const client = this.runtime.client;
3875
4074
  const request = { after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
3876
4075
  const invoke = async () => {
3877
- const messages = await client._pullV2(Number(request.after_seq), pageLimit, { gateLocked: true, maxPages: 1 });
4076
+ const messages = await client._pullV2(Number(request.after_seq), pageLimit, { gateLocked: true, maxPages: 1, background });
3878
4077
  return { messages, raw_count: messages.length };
3879
4078
  };
3880
4079
  if (!client._rpcPipeline)
@@ -3888,8 +4087,11 @@ export class MessageDeliveryEngine {
3888
4087
  const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
3889
4088
  const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
3890
4089
  const invoke = async () => {
3891
- const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages, background });
3892
- return { messages, raw_count: messages.length };
4090
+ let count;
4091
+ const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, {
4092
+ gateLocked: true, maxPages, background, retainMessages: !background, onMessageCount: (value) => { count = value; },
4093
+ });
4094
+ return { messages, raw_count: count ?? messages.length };
3893
4095
  };
3894
4096
  if (!client._rpcPipeline)
3895
4097
  return await this.runAutomaticPull(ns, invoke);
@@ -4097,7 +4299,7 @@ export class MessageDeliveryEngine {
4097
4299
  if (!this.beginRealtimeSync(ns))
4098
4300
  return;
4099
4301
  try {
4100
- await this.runP2PRealtimeSync(ns, this.syncState(ns).head, 50, true);
4302
+ await this.runP2PRealtimeSync(ns, this.syncState(ns).head, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, true);
4101
4303
  }
4102
4304
  catch (exc) {
4103
4305
  client._clientLog.warn(`V2 P2P invalid-seq Tail failed: ${formatDeliveryError(exc)}`);
@@ -4112,7 +4314,7 @@ export class MessageDeliveryEngine {
4112
4314
  if (!this.beginRealtimeSync(ns))
4113
4315
  return;
4114
4316
  try {
4115
- await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, 50, true);
4317
+ await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, true);
4116
4318
  }
4117
4319
  catch (exc) {
4118
4320
  client._clientLog.warn(`V2 group invalid-seq Tail failed: group=${groupId} ${formatDeliveryError(exc)}`);
@@ -4692,7 +4894,7 @@ export class MessageDeliveryEngine {
4692
4894
  }
4693
4895
  let pullFailed = false;
4694
4896
  try {
4695
- await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
4897
+ await this.runP2PRealtimeSync(ns, pushSeq, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, false);
4696
4898
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
4697
4899
  return;
4698
4900
  // Tail 期间新到达的 Push 已由 realtime sync 在同一周期内补至多一页 Forward。
@@ -4729,7 +4931,7 @@ export class MessageDeliveryEngine {
4729
4931
  const coordinator = client._v2E2EE;
4730
4932
  const pendingAckSeq = Number(coordinator.pendingForwardAck?.(ns, committedAck) ?? 0);
4731
4933
  if (pendingAckSeq > 0) {
4732
- client._safeAsync(coordinator.confirmForwardGroupAck(groupId, pendingAckSeq, String(data.group_aid ?? '').trim() || undefined, true));
4934
+ this.scheduleForwardAck(ns, pendingAckSeq, seq => coordinator.confirmForwardGroupAck(groupId, seq, String(data.group_aid ?? '').trim() || undefined, true));
4733
4935
  }
4734
4936
  else {
4735
4937
  const pendingNs = this.resolvePendingGroupInlineAckNamespace(ns);
@@ -4755,7 +4957,7 @@ export class MessageDeliveryEngine {
4755
4957
  while (true) {
4756
4958
  const ackBefore = this.syncState(ns).ack;
4757
4959
  attemptAfterSeq = ackBefore;
4758
- await this.runGroupForwardRecovery(groupId, ns, 50, true, targets.get(ns) ?? 0);
4960
+ await this.runGroupForwardRecovery(groupId, ns, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, true, targets.get(ns) ?? 0);
4759
4961
  const ackAfter = this.syncState(ns).ack;
4760
4962
  if (ackAfter >= (targets.get(ns) ?? 0) || ackAfter <= ackBefore)
4761
4963
  return;
@@ -4790,8 +4992,6 @@ export class MessageDeliveryEngine {
4790
4992
  const groupId = typeof d.group_id === 'string' ? d.group_id.trim() : '';
4791
4993
  const rawSeq = d.seq;
4792
4994
  const inlinePresent = hasNonEmptyInlineMessage(d.inline_message);
4793
- if (!client._v2Session && !(expectedVersion === 'v1' && inlinePresent))
4794
- return;
4795
4995
  const seq = this.realtimeHeadSeq(rawSeq, inlinePresent);
4796
4996
  if (!groupId) {
4797
4997
  client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: group=${groupId || '<empty>'}, seq=${String(d.seq ?? '')}`);
@@ -4804,6 +5004,15 @@ export class MessageDeliveryEngine {
4804
5004
  return;
4805
5005
  }
4806
5006
  const eventKind = String(d.kind ?? '').trim();
5007
+ if (!client._v2Session && !(expectedVersion === 'v1' && inlinePresent)) {
5008
+ if (client._sessionOptions?.background_sync !== false) {
5009
+ const ns = `group:${groupId}`;
5010
+ this.recordBackgroundPullNeed(ns, seq, eventKind === 'group.online_unread_hint', true);
5011
+ if (this.backgroundPullNeeds.has(ns))
5012
+ this.pendingV2GroupPulls.add(ns);
5013
+ }
5014
+ return;
5015
+ }
4807
5016
  if (eventKind === 'group.online_unread_hint') {
4808
5017
  if (client._sessionOptions.background_sync === false)
4809
5018
  return;
@@ -4852,7 +5061,7 @@ export class MessageDeliveryEngine {
4852
5061
  return;
4853
5062
  }
4854
5063
  let pullFailed = false;
4855
- void this.runGroupRealtimeSync(groupId, ns, seq, 50, false).then(async () => {
5064
+ void this.runGroupRealtimeSync(groupId, ns, seq, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, false).then(async () => {
4856
5065
  if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
4857
5066
  return;
4858
5067
  // Tail 期间新到达的 Push 已由 realtime sync 在同一周期内补至多一页 Forward。
@@ -4938,18 +5147,44 @@ export class MessageDeliveryEngine {
4938
5147
  this.refreshPendingPeers();
4939
5148
  return;
4940
5149
  }
4941
- if (!client._aid || !client._v2Session)
5150
+ if (!client._aid || !client._v2Session || client._sessionOptions?.background_sync === false)
4942
5151
  return;
4943
5152
  const ns = `p2p:${client._aid}`;
4944
5153
  const generation = this.captureInlineAckGeneration();
4945
- const afterSeq = this.syncState(ns).ack;
4946
- this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq), true, true);
5154
+ const state = this.syncState(ns);
5155
+ const afterSeq = state.ack;
5156
+ const hintSeq = positiveSafeSequenceHint(data.seq);
5157
+ if (hintSeq > 0 && afterSeq >= Math.max(hintSeq, state.head, Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0))
5158
+ && afterSeq >= state.tail - 1 && !client._pendingOrderedMsgs.get(ns)?.size) {
5159
+ const coordinator = client._v2E2EE;
5160
+ const pendingAckSeq = Number(coordinator.pendingForwardAck?.(ns, afterSeq) ?? 0);
5161
+ if (pendingAckSeq > 0) {
5162
+ this.scheduleForwardAck(ns, pendingAckSeq, seq => coordinator.confirmForwardP2PAck(seq));
5163
+ }
5164
+ else {
5165
+ const pending = this.pendingP2PInlineAcks?.get(ns);
5166
+ if (pending)
5167
+ this.retryPendingP2PInlineAck(ns, pending.seq, 'covered online hint', afterSeq);
5168
+ }
5169
+ return;
5170
+ }
5171
+ this.recordBackgroundPullNeed(ns, hintSeq, true, true);
4947
5172
  const pullTrigger = this.backgroundPullNeeds.get(ns)?.trigger;
5173
+ const tasks = this.onlineUnreadHintTasks ?? new Map();
5174
+ this.onlineUnreadHintTasks = tasks;
5175
+ if (tasks.has(ns))
5176
+ return;
4948
5177
  let failed = false;
4949
- client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
5178
+ const task = this.runP2PForwardRecovery(ns, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, true).then(() => undefined).catch((exc) => {
4950
5179
  failed = true;
4951
5180
  client._clientLog.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
4952
- }).finally(() => this.finishBackgroundPull(ns, generation, afterSeq, failed, pullTrigger)));
5181
+ }).finally(() => {
5182
+ if (tasks.get(ns) === task)
5183
+ tasks.delete(ns);
5184
+ this.finishBackgroundPull(ns, generation, afterSeq, failed, pullTrigger);
5185
+ });
5186
+ tasks.set(ns, task);
5187
+ client._safeAsync(task);
4953
5188
  }
4954
5189
  async saveSeqTrackerState() {
4955
5190
  const client = this.runtime.client;