@agentunion/fastaun-browser 0.5.17 → 0.5.19

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 (119) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/_packed_docs/CHANGELOG.md +83 -0
  3. package/_packed_docs/INDEX.md +163 -16
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +52 -20
  5. package/_packed_docs/agent.md/SCHEMA.md +266 -207
  6. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +64 -31
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +8 -6
  8. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +39 -39
  9. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +1 -1
  10. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
  11. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +20 -2
  12. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  13. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
  14. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +246 -241
  15. package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
  16. package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +69 -38
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
  19. package/_packed_docs/sdk/INDEX.md +78 -18
  20. package/dist/agent-md-schema.d.ts +3 -0
  21. package/dist/agent-md-schema.d.ts.map +1 -1
  22. package/dist/agent-md-schema.js +46 -10
  23. package/dist/agent-md-schema.js.map +1 -1
  24. package/dist/agent-md.d.ts +44 -1
  25. package/dist/agent-md.d.ts.map +1 -1
  26. package/dist/agent-md.js +518 -26
  27. package/dist/agent-md.js.map +1 -1
  28. package/dist/aid-store.d.ts +12 -1
  29. package/dist/aid-store.d.ts.map +1 -1
  30. package/dist/aid-store.js +113 -18
  31. package/dist/aid-store.js.map +1 -1
  32. package/dist/aid.d.ts +17 -1
  33. package/dist/aid.d.ts.map +1 -1
  34. package/dist/aid.js +69 -9
  35. package/dist/aid.js.map +1 -1
  36. package/dist/auth.d.ts +4 -1
  37. package/dist/auth.d.ts.map +1 -1
  38. package/dist/auth.js +343 -23
  39. package/dist/auth.js.map +1 -1
  40. package/dist/bundle.js +37513 -31501
  41. package/dist/cert-utils.d.ts +1 -1
  42. package/dist/cert-utils.d.ts.map +1 -1
  43. package/dist/cert-utils.js +24 -10
  44. package/dist/cert-utils.js.map +1 -1
  45. package/dist/client/delivery.d.ts +16 -1
  46. package/dist/client/delivery.d.ts.map +1 -1
  47. package/dist/client/delivery.js +198 -58
  48. package/dist/client/delivery.js.map +1 -1
  49. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  50. package/dist/client/rpc-pipeline.js +28 -6
  51. package/dist/client/rpc-pipeline.js.map +1 -1
  52. package/dist/client/v2-e2ee.d.ts +1 -0
  53. package/dist/client/v2-e2ee.d.ts.map +1 -1
  54. package/dist/client/v2-e2ee.js +858 -484
  55. package/dist/client/v2-e2ee.js.map +1 -1
  56. package/dist/client.d.ts +100 -1
  57. package/dist/client.d.ts.map +1 -1
  58. package/dist/client.js +1216 -24
  59. package/dist/client.js.map +1 -1
  60. package/dist/errors.d.ts.map +1 -1
  61. package/dist/errors.js +3 -2
  62. package/dist/errors.js.map +1 -1
  63. package/dist/events.d.ts +4 -0
  64. package/dist/events.d.ts.map +1 -1
  65. package/dist/events.js +41 -1
  66. package/dist/events.js.map +1 -1
  67. package/dist/facades.d.ts +84 -2
  68. package/dist/facades.d.ts.map +1 -1
  69. package/dist/facades.js +104 -4
  70. package/dist/facades.js.map +1 -1
  71. package/dist/index.d.ts +8 -3
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +3 -1
  74. package/dist/index.js.map +1 -1
  75. package/dist/keystore/index.d.ts +17 -0
  76. package/dist/keystore/index.d.ts.map +1 -1
  77. package/dist/keystore/indexeddb-shared.d.ts +10 -1
  78. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  79. package/dist/keystore/indexeddb-shared.js +111 -0
  80. package/dist/keystore/indexeddb-shared.js.map +1 -1
  81. package/dist/keystore/indexeddb-token-store.d.ts +7 -1
  82. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  83. package/dist/keystore/indexeddb-token-store.js +30 -1
  84. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  85. package/dist/org-protocol.d.ts +98 -0
  86. package/dist/org-protocol.d.ts.map +1 -0
  87. package/dist/org-protocol.js +1742 -0
  88. package/dist/org-protocol.js.map +1 -0
  89. package/dist/ownership-proof.d.ts +118 -0
  90. package/dist/ownership-proof.d.ts.map +1 -0
  91. package/dist/ownership-proof.js +283 -0
  92. package/dist/ownership-proof.js.map +1 -0
  93. package/dist/ownership-v21.d.ts +177 -0
  94. package/dist/ownership-v21.d.ts.map +1 -0
  95. package/dist/ownership-v21.js +609 -0
  96. package/dist/ownership-v21.js.map +1 -0
  97. package/dist/ownership-workflow.d.ts +146 -0
  98. package/dist/ownership-workflow.d.ts.map +1 -0
  99. package/dist/ownership-workflow.js +1066 -0
  100. package/dist/ownership-workflow.js.map +1 -0
  101. package/dist/seq-tracker.d.ts +1 -0
  102. package/dist/seq-tracker.d.ts.map +1 -1
  103. package/dist/seq-tracker.js +10 -2
  104. package/dist/seq-tracker.js.map +1 -1
  105. package/dist/tools/cross-sdk-agent.d.ts +82 -0
  106. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  107. package/dist/tools/cross-sdk-agent.js +332 -7
  108. package/dist/tools/cross-sdk-agent.js.map +1 -1
  109. package/dist/transport.d.ts +6 -0
  110. package/dist/transport.d.ts.map +1 -1
  111. package/dist/transport.js +88 -2
  112. package/dist/transport.js.map +1 -1
  113. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  114. package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
  115. package/dist/v2/e2ee/decrypt.js +9 -1
  116. package/dist/v2/e2ee/decrypt.js.map +1 -1
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/package.json +5 -5
@@ -14,6 +14,52 @@ const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
14
14
  const V2_PULL_DEFAULT_PAGE_LIMIT = 50;
15
15
  const V2_PULL_MAX_PAGE_LIMIT = 50;
16
16
  const V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS = 3_000;
17
+ const V2_PULL_PIPELINE_QUEUE_CAPACITY = 100;
18
+ class V2PullPageQueue {
19
+ capacity;
20
+ items = [];
21
+ takers = [];
22
+ putters = [];
23
+ queuedWeight = 0;
24
+ closed = false;
25
+ constructor(capacity) {
26
+ this.capacity = capacity;
27
+ }
28
+ async put(item, weight = 1) {
29
+ const itemWeight = Math.max(1, Math.trunc(weight));
30
+ while (!this.closed && this.items.length > 0 && this.queuedWeight + itemWeight > this.capacity) {
31
+ await new Promise((resolve) => this.putters.push(resolve));
32
+ }
33
+ if (this.closed)
34
+ return false;
35
+ const taker = this.takers.shift();
36
+ if (taker)
37
+ taker(item);
38
+ else {
39
+ this.items.push(item);
40
+ this.queuedWeight += itemWeight;
41
+ }
42
+ return true;
43
+ }
44
+ async take() {
45
+ const item = this.items.shift();
46
+ if (item !== undefined) {
47
+ this.queuedWeight = Math.max(0, this.queuedWeight - Math.max(1, Math.trunc(Number(item.weight ?? 1))));
48
+ this.putters.shift()?.();
49
+ return item;
50
+ }
51
+ if (this.closed)
52
+ return undefined;
53
+ return await new Promise((resolve) => this.takers.push(resolve));
54
+ }
55
+ close() {
56
+ this.closed = true;
57
+ for (const resolve of this.takers.splice(0))
58
+ resolve(undefined);
59
+ for (const resolve of this.putters.splice(0))
60
+ resolve();
61
+ }
62
+ }
17
63
  const V2_SESSION_INIT_RETRY_LIMIT = 12;
18
64
  const V2_SESSION_INIT_RETRY_BASE_MS = 1000;
19
65
  const V2_SESSION_INIT_RETRY_MAX_MS = 5000;
@@ -954,11 +1000,12 @@ export class V2E2EECoordinator {
954
1000
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
955
1001
  continue;
956
1002
  }
957
- client._v2SenderIKPending.delete(key);
958
1003
  const seq = Number(entry.msg.seq ?? 0);
959
1004
  if (entry.groupId) {
960
- if (client._delivery.isSelfSentGroupMessage?.(entry.msg))
1005
+ if (client._delivery.isSelfSentGroupMessage?.(entry.msg)) {
1006
+ client._v2SenderIKPending.delete(key);
961
1007
  continue;
1008
+ }
962
1009
  plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
963
1010
  plaintext.group_id = entry.groupId;
964
1011
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
@@ -966,6 +1013,7 @@ export class V2E2EECoordinator {
966
1013
  else {
967
1014
  await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
968
1015
  }
1016
+ client._v2SenderIKPending.delete(key);
969
1017
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
970
1018
  }
971
1019
  }
@@ -1064,6 +1112,12 @@ export class V2E2EECoordinator {
1064
1112
  history_error: { code: 'decrypt_failed', message: reason },
1065
1113
  };
1066
1114
  }
1115
+ ensureDeliveryCurrent(ns, generation, inlineGeneration) {
1116
+ if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
1117
+ || (inlineGeneration !== undefined && !this.client._delivery.isInlineGenerationCurrent(inlineGeneration))
1118
+ || this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
1119
+ throw new StateError('pull invalidated');
1120
+ }
1067
1121
  async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
1068
1122
  const client = this.client;
1069
1123
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
@@ -1080,6 +1134,11 @@ export class V2E2EECoordinator {
1080
1134
  to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
1081
1135
  timestamp: msg.t_server, payload: payload, encrypted: false,
1082
1136
  };
1137
+ if (msg.sender_device_id !== undefined)
1138
+ plaintext.sender_device_id = msg.sender_device_id;
1139
+ const direction = String(msg.direction ?? '').trim();
1140
+ if (direction)
1141
+ plaintext.direction = direction;
1083
1142
  attachGatewayProximity(plaintext, msg);
1084
1143
  }
1085
1144
  else if (isJsonObject(payload)
@@ -1099,6 +1158,9 @@ export class V2E2EECoordinator {
1099
1158
  client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
1100
1159
  return null;
1101
1160
  }
1161
+ if (publish && client._delivery.isSelfSentP2PMessage?.(plaintext)) {
1162
+ return plaintext;
1163
+ }
1102
1164
  if (publish) {
1103
1165
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1104
1166
  if (orderedPublish) {
@@ -1155,6 +1217,8 @@ export class V2E2EECoordinator {
1155
1217
  group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
1156
1218
  timestamp: msg.t_server, payload: payload, encrypted: false,
1157
1219
  };
1220
+ if (msg.sender_device_id !== undefined)
1221
+ plaintext.sender_device_id = msg.sender_device_id;
1158
1222
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1159
1223
  attachGatewayProximity(plaintext, msg);
1160
1224
  }
@@ -1201,6 +1265,8 @@ export class V2E2EECoordinator {
1201
1265
  }
1202
1266
  async pullV2TailInternal(params, deferPublish = false) {
1203
1267
  const client = this.client;
1268
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1269
+ const inlineGeneration = client._delivery.captureInlineGeneration?.();
1204
1270
  if (params.window_mode !== 'tail')
1205
1271
  throw new ValidationError('window_mode must equal tail');
1206
1272
  if ('ack_up_to_seq' in params || params.force === true)
@@ -1215,19 +1281,27 @@ export class V2E2EECoordinator {
1215
1281
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1216
1282
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
1217
1283
  const page = validateTailPage(result, afterSeq, limit);
1218
- if (ns) {
1219
- client._seqTracker.commitTailWindow(ns, {
1220
- windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1221
- });
1222
- client._persistSeq(ns);
1223
- await client._saveSeqTrackerState?.();
1224
- }
1225
1284
  const decoded = [];
1226
1285
  for (const msg of page.messages) {
1286
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1227
1287
  const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1228
1288
  if (plaintext)
1229
1289
  decoded.push(plaintext);
1230
1290
  }
1291
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1292
+ if (ns) {
1293
+ const proof = {
1294
+ windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1295
+ };
1296
+ if (deferPublish)
1297
+ client._delivery.deferTailWindow(ns, proof);
1298
+ else {
1299
+ client._seqTracker.commitTailWindow(ns, proof);
1300
+ client._persistSeq(ns);
1301
+ await client._saveSeqTrackerState?.();
1302
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1303
+ }
1304
+ }
1231
1305
  const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1232
1306
  if (ns)
1233
1307
  client._delivery.onPullPage?.(ns, decoded.length, result.has_more === true, page.head, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, page.messages.length, ack, page.head);
@@ -1264,6 +1338,8 @@ export class V2E2EECoordinator {
1264
1338
  }
1265
1339
  async pullV2Impl(afterSeq = 0, limit = 50, opts) {
1266
1340
  const client = this.client;
1341
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1342
+ const inlineGeneration = client._delivery.captureInlineGeneration?.();
1267
1343
  await client._ensureV2SessionReady('message.pull');
1268
1344
  const ns = client._aid ? `p2p:${client._aid}` : '';
1269
1345
  let volatileCursor = String(opts?.afterVolatileCursor ?? client._delivery.volatilePullCursor() ?? '').trim();
@@ -1304,264 +1380,410 @@ export class V2E2EECoordinator {
1304
1380
  let lastAutoAckSeq = 0;
1305
1381
  let pendingAckSeq = deferredForwardAck;
1306
1382
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1307
- while (pageCount < maxPages) {
1308
- pageCount += 1;
1309
- const volatileCursorBeforePage = volatileCursor;
1310
- const ackUpToSeq = pendingAckSeq;
1311
- const pendingInlineGeneration = ackUpToSeq > 0
1312
- ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1313
- : null;
1314
- let result;
1315
- try {
1316
- result = await client._callRawV2Rpc('message.v2.pull', {
1317
- after_seq: nextAfterSeq,
1318
- limit,
1319
- ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1320
- ...(opts?.force ? { force: true } : {}),
1321
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1322
- }, pullGateKey);
1323
- }
1324
- catch (exc) {
1325
- const text = exc instanceof Error ? exc.message : String(exc);
1326
- if (!volatileCursor || !text.toLowerCase().includes('volatile cursor epoch mismatch'))
1327
- throw exc;
1328
- client._delivery.resetVolatilePullCursor();
1329
- volatileCursor = '';
1330
- pageCount -= 1;
1331
- continue;
1332
- }
1333
- if (ackUpToSeq > 0) {
1334
- const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1335
- if (actualAckSeq >= ackUpToSeq) {
1336
- if (pendingInlineGeneration !== null) {
1337
- client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1383
+ let pipelineRunning = false;
1384
+ let pipelineError = null;
1385
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1386
+ let pipelineProducer = null;
1387
+ let pipelineStopped = false;
1388
+ let pipelineResume = null;
1389
+ const startPipeline = (startAfterSeq, startVolatileCursor) => {
1390
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
1391
+ return;
1392
+ pipelineRunning = true;
1393
+ pipelineProducer = (async () => {
1394
+ let producerAfterSeq = startAfterSeq;
1395
+ let producerVolatileCursor = startVolatileCursor;
1396
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
1397
+ if (pipelineStopped)
1398
+ break;
1399
+ const requestedVolatileCursor = producerVolatileCursor;
1400
+ const result = await client._callRawV2Rpc('message.v2.pull', {
1401
+ after_seq: producerAfterSeq,
1402
+ limit,
1403
+ ...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
1404
+ ...(opts?.force ? { force: true } : {}),
1405
+ }, pullGateKey);
1406
+ if (pipelineStopped)
1407
+ break;
1408
+ const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1409
+ const completed = new Promise((resolve) => { pipelineResume = resolve; });
1410
+ const enqueued = await pipelineQueue.put({
1411
+ result,
1412
+ afterSeq: producerAfterSeq,
1413
+ ackUpToSeq: 0,
1414
+ inlineAckGeneration: null,
1415
+ requestedVolatileCursor,
1416
+ weight: rawPageMessages.length,
1417
+ }, rawPageMessages.length);
1418
+ if (!enqueued || pipelineStopped)
1419
+ break;
1420
+ await completed;
1421
+ if (pipelineStopped)
1422
+ break;
1423
+ const volatileMessages = client._delivery.volatileMessages(rawPageMessages);
1424
+ if (volatileMessages.length > 0)
1425
+ break;
1426
+ const messages = deduplicateForwardPageMessages(rawPageMessages, producerAfterSeq);
1427
+ const seqs = messages
1428
+ .map((msg) => Number(msg.seq ?? 0))
1429
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1430
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
1431
+ const cursor = isJsonObject(result.cursor)
1432
+ ? result.cursor : null;
1433
+ const shouldContinue = shouldContinueForwardPage({
1434
+ rawCount: messages.length,
1435
+ nextAfterSeq: producerAfterSeq,
1436
+ nextAfter,
1437
+ limit,
1438
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
1439
+ hasMore: result.has_more === true,
1440
+ serverHead: safeNonNegativeSeq(cursor?.latest_seq) ?? 0,
1441
+ observedUpper: Math.max(nextAfter, Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1442
+ pendingAckSeq: 0,
1443
+ });
1444
+ if (!shouldContinue)
1445
+ break;
1446
+ producerAfterSeq = nextAfter;
1447
+ producerVolatileCursor = '';
1448
+ }
1449
+ })().catch((error) => {
1450
+ pipelineError = error;
1451
+ }).finally(() => {
1452
+ pipelineQueue.close();
1453
+ });
1454
+ };
1455
+ const stopPipeline = async () => {
1456
+ pipelineStopped = true;
1457
+ pipelineResume?.();
1458
+ pipelineResume = null;
1459
+ pipelineQueue.close();
1460
+ if (pipelineProducer) {
1461
+ const producer = pipelineProducer;
1462
+ pipelineProducer = null;
1463
+ await producer;
1464
+ }
1465
+ pipelineRunning = false;
1466
+ };
1467
+ try {
1468
+ while (pageCount < maxPages) {
1469
+ let pipelinePage;
1470
+ if (pipelineRunning) {
1471
+ pipelinePage = await pipelineQueue.take();
1472
+ if (!pipelinePage) {
1473
+ pipelineRunning = false;
1474
+ if (pipelineError)
1475
+ throw pipelineError;
1476
+ break;
1338
1477
  }
1339
- pendingAckSeq = 0;
1340
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1341
- this.clearForwardAck(ns, ackUpToSeq);
1478
+ }
1479
+ pageCount += 1;
1480
+ const volatileCursorBeforePage = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1481
+ const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1482
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1483
+ ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1484
+ : null);
1485
+ let result;
1486
+ if (pipelinePage) {
1487
+ nextAfterSeq = pipelinePage.afterSeq;
1488
+ result = pipelinePage.result;
1342
1489
  }
1343
1490
  else {
1344
- client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1345
- await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1346
- pendingAckSeq = 0;
1347
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1491
+ try {
1492
+ result = await client._callRawV2Rpc('message.v2.pull', {
1493
+ after_seq: nextAfterSeq,
1494
+ limit,
1495
+ ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1496
+ ...(opts?.force ? { force: true } : {}),
1497
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1498
+ }, pullGateKey);
1499
+ }
1500
+ catch (exc) {
1501
+ const text = exc instanceof Error ? exc.message : String(exc);
1502
+ if (!volatileCursor || !text.toLowerCase().includes('volatile cursor epoch mismatch'))
1503
+ throw exc;
1504
+ client._delivery.resetVolatilePullCursor();
1505
+ volatileCursor = '';
1506
+ pageCount -= 1;
1507
+ continue;
1508
+ }
1348
1509
  }
1349
- }
1350
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1351
- const volatileMessages = client._delivery.volatileMessages(pageMessages);
1352
- if (volatileMessages.length > 0) {
1353
- decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1354
- volatileCursor = client._delivery.volatilePullCursor();
1355
- }
1356
- const volatileAdvanced = Boolean(volatileCursor) && volatileCursor !== volatileCursorBeforePage;
1357
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1358
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1359
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1360
- const hasMore = result.has_more === true;
1361
- const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1362
- 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 ?? '')}`);
1363
- const seqs = messages
1364
- .map((msg) => Number(msg.seq ?? 0))
1365
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1366
- const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1367
- const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1368
- ? client._seqTracker.snapshotNamespace(ns)
1369
- : null;
1370
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1371
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1372
- const deferredKeyFetches = new Map();
1373
- let blockedSeq = 0;
1374
- const pageDecryptedBefore = decrypted.length;
1375
- for (const msg of messages) {
1376
- const seq = Number(msg.seq ?? 0);
1377
- if (!Number.isFinite(seq) || seq <= 0)
1378
- continue;
1379
- const version = String(msg.version ?? 'v2');
1380
- if (version === 'v1') {
1381
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1382
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1383
- const payloadType = isJsonObject(legacyPayload)
1384
- ? String(legacyPayload.type ?? '').trim()
1385
- : '';
1386
- if (legacyPayload !== undefined && legacyPayload !== null
1387
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1388
- const v1Msg = {
1389
- message_id: String(msg.message_id ?? ''),
1390
- from: String(msg.from_aid ?? ''),
1391
- to: String(legacy.to ?? client._aid ?? ''),
1392
- seq: msg.seq,
1393
- type: String(msg.type ?? ''),
1394
- timestamp: msg.t_server,
1395
- payload: legacyPayload,
1396
- encrypted: false,
1397
- };
1398
- attachGatewayProximity(v1Msg, msg);
1399
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1400
- if (ns)
1401
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1402
- else
1403
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1404
- decrypted.push(v1Msg);
1510
+ if (ackUpToSeq > 0) {
1511
+ const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1512
+ if (actualAckSeq >= ackUpToSeq) {
1513
+ if (pendingInlineGeneration !== null) {
1514
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1515
+ }
1516
+ pendingAckSeq = 0;
1517
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1518
+ this.clearForwardAck(ns, ackUpToSeq);
1519
+ }
1520
+ else {
1521
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1522
+ await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1523
+ pendingAckSeq = 0;
1524
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1405
1525
  }
1406
- else if (isJsonObject(legacyPayload)
1407
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1408
- const deferStatus = {};
1409
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1410
- if (deferStatus.deferred) {
1411
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1412
- if (deferStatus.fromAid) {
1413
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1414
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1526
+ }
1527
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1528
+ const volatileMessages = client._delivery.volatileMessages(pageMessages);
1529
+ if (volatileMessages.length > 0) {
1530
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1531
+ volatileCursor = client._delivery.volatilePullCursor();
1532
+ }
1533
+ const volatileAdvanced = Boolean(volatileCursor) && volatileCursor !== volatileCursorBeforePage;
1534
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1535
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1536
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1537
+ const hasMore = result.has_more === true;
1538
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1539
+ client._clientLog.debug(`message.v2.pull page response: page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, server_ack_seq=${String(result.server_ack_seq ?? '')}`);
1540
+ const seqs = messages
1541
+ .map((msg) => Number(msg.seq ?? 0))
1542
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1543
+ const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1544
+ let pageTrackerSnapshot;
1545
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1546
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1547
+ const deferredKeyFetches = new Map();
1548
+ let blockedSeq = 0;
1549
+ const pageDecryptedBefore = decrypted.length;
1550
+ for (const msg of messages) {
1551
+ const seq = Number(msg.seq ?? 0);
1552
+ if (!Number.isFinite(seq) || seq <= 0)
1553
+ continue;
1554
+ const version = String(msg.version ?? 'v2');
1555
+ if (version === 'v1') {
1556
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1557
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1558
+ const payloadType = isJsonObject(legacyPayload)
1559
+ ? String(legacyPayload.type ?? '').trim()
1560
+ : '';
1561
+ if (legacyPayload !== undefined && legacyPayload !== null
1562
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1563
+ const v1Msg = {
1564
+ message_id: String(msg.message_id ?? ''),
1565
+ from: String(msg.from_aid ?? ''),
1566
+ to: String(legacy.to ?? client._aid ?? ''),
1567
+ seq: msg.seq,
1568
+ type: String(msg.type ?? ''),
1569
+ timestamp: msg.t_server,
1570
+ payload: legacyPayload,
1571
+ encrypted: false,
1572
+ };
1573
+ const direction = String(msg.direction ?? '').trim();
1574
+ if (direction)
1575
+ v1Msg.direction = direction;
1576
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1577
+ v1Msg.direction = 'inbound';
1415
1578
  }
1416
- }
1417
- if (plaintext) {
1579
+ if (msg.sender_device_id !== undefined)
1580
+ v1Msg.sender_device_id = msg.sender_device_id;
1581
+ attachGatewayProximity(v1Msg, msg);
1582
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1418
1583
  if (ns)
1419
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1584
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1420
1585
  else
1421
- await client._publishAppEvent('message.received', plaintext, 'pull');
1422
- decrypted.push(plaintext);
1586
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1587
+ decrypted.push(v1Msg);
1588
+ }
1589
+ else if (isJsonObject(legacyPayload)
1590
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1591
+ const deferStatus = {};
1592
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1593
+ if (deferStatus.deferred) {
1594
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1595
+ if (deferStatus.fromAid) {
1596
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1597
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1598
+ }
1599
+ }
1600
+ if (plaintext) {
1601
+ if (ns)
1602
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1603
+ else
1604
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1605
+ decrypted.push(plaintext);
1606
+ }
1423
1607
  }
1608
+ else {
1609
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1610
+ }
1611
+ continue;
1612
+ }
1613
+ if (version !== 'v2') {
1614
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1615
+ continue;
1616
+ }
1617
+ const msgSpkId = String(msg.spk_id ?? '');
1618
+ if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1619
+ client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1620
+ }
1621
+ const deferStatus = {};
1622
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1623
+ if (deferStatus.deferred) {
1624
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1625
+ }
1626
+ if (deferStatus.deferred && deferStatus.fromAid) {
1627
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1628
+ deferredKeyFetches.set(key, {
1629
+ fromAid: deferStatus.fromAid,
1630
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1631
+ groupId: deferStatus.groupId ?? '',
1632
+ });
1633
+ }
1634
+ if (plaintext === null)
1635
+ continue;
1636
+ if (ns) {
1637
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1638
+ decrypted.push(plaintext);
1424
1639
  }
1425
1640
  else {
1426
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1641
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1642
+ decrypted.push(plaintext);
1427
1643
  }
1428
- continue;
1429
- }
1430
- if (version !== 'v2') {
1431
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1432
- continue;
1433
- }
1434
- const msgSpkId = String(msg.spk_id ?? '');
1435
- if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1436
- client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1437
- }
1438
- const deferStatus = {};
1439
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1440
- if (deferStatus.deferred) {
1441
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1442
- }
1443
- if (deferStatus.deferred && deferStatus.fromAid) {
1444
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1445
- deferredKeyFetches.set(key, {
1446
- fromAid: deferStatus.fromAid,
1447
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1448
- groupId: deferStatus.groupId ?? '',
1449
- });
1450
1644
  }
1451
- if (plaintext === null)
1452
- continue;
1645
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1646
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1647
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1648
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1453
1649
  if (ns) {
1454
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1455
- decrypted.push(plaintext);
1456
- }
1457
- else {
1458
- await client._publishAppEvent('message.received', plaintext, 'pull');
1459
- decrypted.push(plaintext);
1460
- }
1461
- }
1462
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1463
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1464
- const serverAckSeq = parsedServerAckSeq ?? 0;
1465
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1466
- if (ns) {
1467
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1468
- let ackSeq = pageContigBefore;
1469
- let contigAdvanced = false;
1470
- try {
1471
- if (commitTarget > pageContigBefore) {
1472
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1473
- if (retentionFloor > pageContigBefore) {
1474
- client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1650
+ let commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1651
+ commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
1652
+ let ackSeq = pageContigBefore;
1653
+ let contigAdvanced = false;
1654
+ try {
1655
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1656
+ const state = client._seqTracker.getSyncState?.(ns);
1657
+ const deliveryThrough = client._delivery.pendingTailDeliveryThrough?.(ns, state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget) ?? (state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget);
1658
+ await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough);
1659
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1660
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
1661
+ client._delivery.commitPendingTailWindow?.(ns, commitTarget);
1662
+ if (commitTarget > pageContigBefore) {
1663
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1664
+ if (retentionFloor > pageContigBefore) {
1665
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1666
+ }
1667
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1668
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1669
+ }
1670
+ if (seqs.length > 0) {
1671
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1672
+ }
1475
1673
  }
1476
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1477
- client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1674
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1675
+ contigAdvanced = ackSeq !== pageContigBefore;
1676
+ if (contigAdvanced) {
1677
+ await client._commitSeqTrackerState(ns);
1678
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1679
+ if (pageCount === 1 && deferredServerCursor > 0)
1680
+ this.clearForwardCursor(ns, ackSeq);
1478
1681
  }
1479
- if (seqs.length > 0) {
1480
- client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1682
+ client._delivery.completePendingTailWindow?.(ns);
1683
+ }
1684
+ catch (exc) {
1685
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
1686
+ && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
1687
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
1688
+ && pageTrackerSnapshot !== undefined
1689
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1690
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
1481
1691
  }
1692
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1693
+ throw exc;
1482
1694
  }
1483
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1484
- contigAdvanced = ackSeq !== pageContigBefore;
1485
- if (contigAdvanced) {
1486
- await client._commitSeqTrackerState(ns);
1487
- if (pageCount === 1 && deferredServerCursor > 0)
1488
- this.clearForwardCursor(ns, ackSeq);
1695
+ for (const fetch of deferredKeyFetches.values()) {
1696
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1489
1697
  }
1490
- }
1491
- catch (exc) {
1492
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1493
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1698
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1699
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1700
+ }
1701
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1702
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1703
+ const knownServerAckSeq = hasServerAckSeq
1704
+ ? serverAckSeq
1705
+ : (pageCount === 1 ? deferredServerCursor : 0);
1706
+ const ackNeeded = ackSeq > 0
1707
+ && ackSeq > lastAutoAckSeq
1708
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1709
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1710
+ if (ackNeeded) {
1711
+ this.recordForwardAck(ns, ackSeq);
1712
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1713
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1714
+ if (canContinuePage && !pipelineRunning) {
1715
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1716
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1717
+ }
1718
+ else {
1719
+ await this.confirmForwardP2PAck(ackSeq);
1720
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1721
+ }
1494
1722
  }
1495
- client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1496
- throw exc;
1497
1723
  }
1498
- if (contigAdvanced)
1499
- await client._drainOrderedMessages(ns, undefined, false, false);
1500
- for (const fetch of deferredKeyFetches.values()) {
1501
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
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;
1502
1746
  }
1503
- if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1504
- this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1747
+ const shouldContinueForPipeline = shouldContinueForwardPage({
1748
+ rawCount,
1749
+ nextAfterSeq,
1750
+ nextAfter,
1751
+ limit,
1752
+ hasMoreField,
1753
+ hasMore,
1754
+ serverHead,
1755
+ pendingAckSeq: 0,
1756
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1757
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1758
+ });
1759
+ if (!shouldContinue)
1760
+ break;
1761
+ if (pipelinePage) {
1762
+ pipelineResume?.();
1763
+ pipelineResume = null;
1505
1764
  }
1506
- // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1507
- // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1508
- const knownServerAckSeq = hasServerAckSeq
1509
- ? serverAckSeq
1510
- : (pageCount === 1 ? deferredServerCursor : 0);
1511
- const ackNeeded = ackSeq > 0
1512
- && ackSeq > lastAutoAckSeq
1513
- && ((hasServerAckSeq && ackSeq > serverAckSeq)
1514
- || (contigAdvanced && ackSeq > knownServerAckSeq));
1515
- if (ackNeeded) {
1516
- this.recordForwardAck(ns, ackSeq);
1517
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1518
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1519
- if (canContinuePage) {
1520
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1521
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1522
- }
1523
- else {
1524
- await this.confirmForwardP2PAck(ackSeq);
1525
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1765
+ if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1766
+ if (pendingAckSeq > 0) {
1767
+ await this.confirmForwardP2PAck(pendingAckSeq);
1768
+ pendingAckSeq = 0;
1526
1769
  }
1770
+ startPipeline(nextAfter, volatileCursor);
1771
+ }
1772
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1773
+ const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1774
+ after_seq: nextAfter, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1775
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1776
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1777
+ pullGateKey = nextKey;
1527
1778
  }
1779
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1780
+ if (tailDelayMs > 0)
1781
+ await v2Sleep(tailDelayMs);
1782
+ nextAfterSeq = nextAfter;
1528
1783
  }
1529
- if (ns)
1530
- 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);
1531
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1532
- const rawCount = messages.length;
1533
- if (blockedSeq > 0)
1534
- break;
1535
- let shouldContinue = shouldContinueForwardPage({
1536
- rawCount,
1537
- nextAfterSeq,
1538
- nextAfter,
1539
- limit,
1540
- hasMoreField,
1541
- hasMore,
1542
- serverHead,
1543
- pendingAckSeq,
1544
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1545
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1546
- });
1547
- const volatileHasMoreField = Object.prototype.hasOwnProperty.call(result, 'volatile_has_more');
1548
- const volatileHasMore = result.volatile_has_more === true;
1549
- if (!shouldContinue && volatileAdvanced && (volatileHasMore || hasMore || (!volatileHasMoreField && limit > 0 && pageMessages.length >= limit))) {
1550
- shouldContinue = true;
1551
- }
1552
- if (!shouldContinue)
1553
- break;
1554
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1555
- const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1556
- after_seq: nextAfter, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1557
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1558
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1559
- pullGateKey = nextKey;
1560
- }
1561
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1562
- if (tailDelayMs > 0)
1563
- await v2Sleep(tailDelayMs);
1564
- nextAfterSeq = nextAfter;
1784
+ }
1785
+ finally {
1786
+ await stopPipeline();
1565
1787
  }
1566
1788
  if (pageCount >= maxPages) {
1567
1789
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
@@ -1784,6 +2006,8 @@ export class V2E2EECoordinator {
1784
2006
  }
1785
2007
  async pullGroupV2TailInternal(params, deferPublish = false) {
1786
2008
  const client = this.client;
2009
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
2010
+ const inlineGeneration = client._delivery.captureInlineGeneration?.();
1787
2011
  if (params.window_mode !== 'tail')
1788
2012
  throw new ValidationError('window_mode must equal tail');
1789
2013
  if ('ack_up_to_seq' in params || params.force === true)
@@ -1819,18 +2043,26 @@ export class V2E2EECoordinator {
1819
2043
  if (resultAid)
1820
2044
  groupAid = resultAid;
1821
2045
  const page = validateTailPage(result, afterSeq, limit);
1822
- client._seqTracker.commitTailWindow(ns, {
1823
- windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
1824
- });
1825
- client._persistSeq(ns);
1826
- await client._saveSeqTrackerState?.();
1827
2046
  const decoded = [];
1828
2047
  for (const msg of page.messages) {
2048
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
1829
2049
  client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
1830
2050
  const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
1831
2051
  if (plaintext)
1832
2052
  decoded.push(plaintext);
1833
2053
  }
2054
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2055
+ const proof = {
2056
+ windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
2057
+ };
2058
+ if (deferPublish)
2059
+ client._delivery.deferTailWindow(ns, proof);
2060
+ else {
2061
+ client._seqTracker.commitTailWindow(ns, proof);
2062
+ client._persistSeq(ns);
2063
+ await client._saveSeqTrackerState?.();
2064
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2065
+ }
1834
2066
  const ack = client._seqTracker.getContiguousSeq(ns);
1835
2067
  if (ownsCursor)
1836
2068
  client._delivery.onPullPage?.(ns, decoded.length, result.has_more === true, page.head, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, page.messages.length, ack, page.head);
@@ -1875,6 +2107,8 @@ export class V2E2EECoordinator {
1875
2107
  }
1876
2108
  async pullGroupV2Impl(groupId, afterSeq = 0, limit = 50, opts) {
1877
2109
  const client = this.client;
2110
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
2111
+ const inlineGeneration = client._delivery.captureInlineGeneration?.();
1878
2112
  await client._ensureV2SessionReady('group.pull');
1879
2113
  const gid = String(groupId ?? '').trim();
1880
2114
  if (!gid)
@@ -1931,83 +2165,202 @@ export class V2E2EECoordinator {
1931
2165
  let lastAutoAckSeq = 0;
1932
2166
  let pendingAckSeq = deferredForwardAck;
1933
2167
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1934
- while (pageCount < maxPages) {
1935
- pageCount += 1;
1936
- const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
1937
- const pendingInlineGeneration = ackUpToSeq > 0
1938
- ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
1939
- : null;
1940
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
1941
- group_id: wireGroupId,
1942
- after_seq: nextAfterSeq,
1943
- limit,
1944
- ...cursorParams,
1945
- ...(opts?.force ? { force: true } : {}),
1946
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1947
- }, groupAid), pullGateKey);
1948
- if (ackUpToSeq > 0) {
1949
- const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
1950
- if (actualAckSeq >= ackUpToSeq) {
1951
- if (pendingInlineGeneration !== null) {
1952
- client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2168
+ let pipelineRunning = false;
2169
+ let pipelineError = null;
2170
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2171
+ let pipelineProducer = null;
2172
+ let pipelineStopped = false;
2173
+ let pipelineResume = null;
2174
+ const startPipeline = (startAfterSeq) => {
2175
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
2176
+ return;
2177
+ pipelineRunning = true;
2178
+ pipelineProducer = (async () => {
2179
+ let producerAfterSeq = startAfterSeq;
2180
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
2181
+ if (pipelineStopped)
2182
+ break;
2183
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2184
+ group_id: wireGroupId,
2185
+ after_seq: producerAfterSeq,
2186
+ limit,
2187
+ ...cursorParams,
2188
+ ...(opts?.force ? { force: true } : {}),
2189
+ }, groupAid), pullGateKey);
2190
+ if (pipelineStopped)
2191
+ break;
2192
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2193
+ const completed = new Promise((resolve) => { pipelineResume = resolve; });
2194
+ const enqueued = await pipelineQueue.put({
2195
+ result,
2196
+ afterSeq: producerAfterSeq,
2197
+ ackUpToSeq: 0,
2198
+ inlineAckGeneration: null,
2199
+ requestedVolatileCursor: '',
2200
+ weight: pageMessages.length,
2201
+ }, pageMessages.length);
2202
+ if (!enqueued || pipelineStopped)
2203
+ break;
2204
+ await completed;
2205
+ if (pipelineStopped)
2206
+ break;
2207
+ const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
2208
+ const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
2209
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
2210
+ const cursor = isJsonObject(result.cursor)
2211
+ ? result.cursor : null;
2212
+ const shouldContinue = shouldContinueForwardPage({
2213
+ rawCount: messages.length,
2214
+ nextAfterSeq: producerAfterSeq,
2215
+ nextAfter,
2216
+ limit,
2217
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
2218
+ hasMore: result.has_more === true,
2219
+ serverHead: Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0),
2220
+ observedUpper: Math.max(nextAfter, typeof client._seqTracker.getMaxSeenSeq === 'function' ? client._seqTracker.getMaxSeenSeq(ns) : 0),
2221
+ pendingAckSeq: 0,
2222
+ });
2223
+ if (!shouldContinue)
2224
+ break;
2225
+ producerAfterSeq = nextAfter;
2226
+ }
2227
+ })().catch((error) => {
2228
+ pipelineError = error;
2229
+ }).finally(() => {
2230
+ pipelineQueue.close();
2231
+ });
2232
+ };
2233
+ const stopPipeline = async () => {
2234
+ pipelineStopped = true;
2235
+ pipelineResume?.();
2236
+ pipelineResume = null;
2237
+ pipelineQueue.close();
2238
+ if (pipelineProducer) {
2239
+ const producer = pipelineProducer;
2240
+ pipelineProducer = null;
2241
+ await producer;
2242
+ }
2243
+ pipelineRunning = false;
2244
+ };
2245
+ try {
2246
+ while (pageCount < maxPages) {
2247
+ let pipelinePage;
2248
+ if (pipelineRunning) {
2249
+ pipelinePage = await pipelineQueue.take();
2250
+ if (!pipelinePage) {
2251
+ pipelineRunning = false;
2252
+ if (pipelineError)
2253
+ throw pipelineError;
2254
+ break;
1953
2255
  }
1954
- pendingAckSeq = 0;
1955
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1956
- this.clearForwardAck(ns, ackUpToSeq);
2256
+ }
2257
+ pageCount += 1;
2258
+ const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2259
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2260
+ ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
2261
+ : null);
2262
+ let result;
2263
+ if (pipelinePage) {
2264
+ nextAfterSeq = pipelinePage.afterSeq;
2265
+ result = pipelinePage.result;
1957
2266
  }
1958
2267
  else {
1959
- client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1960
- await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
1961
- pendingAckSeq = 0;
1962
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2268
+ result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2269
+ group_id: wireGroupId,
2270
+ after_seq: nextAfterSeq,
2271
+ limit,
2272
+ ...cursorParams,
2273
+ ...(opts?.force ? { force: true } : {}),
2274
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2275
+ }, groupAid), pullGateKey);
1963
2276
  }
1964
- }
1965
- const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
1966
- if (resultGroupAid)
1967
- groupAid = resultGroupAid;
1968
- const eventGroupAid = resultGroupAid || groupAid || gid;
1969
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1970
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1971
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1972
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1973
- const hasMore = result.has_more === true;
1974
- const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
1975
- const seqs = messages
1976
- .map((msg) => Number(msg.seq ?? 0))
1977
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1978
- const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
1979
- const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
1980
- ? client._seqTracker.snapshotNamespace(ns)
1981
- : null;
1982
- const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1983
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1984
- const deferredKeyFetches = new Map();
1985
- let blockedSeq = 0;
1986
- const pageDecryptedBefore = decrypted.length;
1987
- for (const msg of messages) {
1988
- const seq = Number(msg.seq ?? 0);
1989
- if (!Number.isFinite(seq) || seq <= 0)
1990
- continue;
1991
- const version = String(msg.version ?? 'v2');
1992
- if (version === 'v1') {
1993
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1994
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
1995
- const payloadObj = isJsonObject(payload) ? payload : null;
1996
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
1997
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
1998
- && client._delivery.recallEventFromGroupMessage(msg)) {
1999
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2000
- ...msg,
2001
- group_id: gid,
2002
- group_aid: eventGroupAid,
2003
- }, 'pull');
2004
- client._markPublishedSeq(ns, seq);
2005
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2006
- continue;
2277
+ if (ackUpToSeq > 0) {
2278
+ const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
2279
+ if (actualAckSeq >= ackUpToSeq) {
2280
+ if (pendingInlineGeneration !== null) {
2281
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2282
+ }
2283
+ pendingAckSeq = 0;
2284
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2285
+ this.clearForwardAck(ns, ackUpToSeq);
2007
2286
  }
2008
- if (payloadObj) {
2009
- const payloadType = String(payloadObj.type ?? '').trim();
2010
- if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2287
+ else {
2288
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2289
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2290
+ pendingAckSeq = 0;
2291
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2292
+ }
2293
+ }
2294
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2295
+ if (resultGroupAid)
2296
+ groupAid = resultGroupAid;
2297
+ const eventGroupAid = resultGroupAid || groupAid || gid;
2298
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2299
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2300
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2301
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2302
+ const hasMore = result.has_more === true;
2303
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2304
+ const seqs = messages
2305
+ .map((msg) => Number(msg.seq ?? 0))
2306
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2307
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2308
+ let pageTrackerSnapshot;
2309
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
2310
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2311
+ const deferredKeyFetches = new Map();
2312
+ let blockedSeq = 0;
2313
+ const pageDecryptedBefore = decrypted.length;
2314
+ for (const msg of messages) {
2315
+ const seq = Number(msg.seq ?? 0);
2316
+ if (!Number.isFinite(seq) || seq <= 0)
2317
+ continue;
2318
+ const version = String(msg.version ?? 'v2');
2319
+ if (version === 'v1') {
2320
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2321
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2322
+ const payloadObj = isJsonObject(payload) ? payload : null;
2323
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2324
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2325
+ && client._delivery.recallEventFromGroupMessage(msg)) {
2326
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
2327
+ ...msg,
2328
+ group_id: gid,
2329
+ group_aid: eventGroupAid,
2330
+ }, 'pull');
2331
+ client._markPublishedSeq(ns, seq);
2332
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2333
+ continue;
2334
+ }
2335
+ if (payloadObj) {
2336
+ const payloadType = String(payloadObj.type ?? '').trim();
2337
+ if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2338
+ let v1Msg = {
2339
+ message_id: String(msg.message_id ?? ''),
2340
+ from: String(msg.from_aid ?? ''),
2341
+ group_id: gid,
2342
+ group_aid: eventGroupAid,
2343
+ seq: msg.seq,
2344
+ type: String(msg.type ?? ''),
2345
+ timestamp: msg.t_server,
2346
+ payload,
2347
+ encrypted: false,
2348
+ };
2349
+ if (msg.sender_device_id !== undefined)
2350
+ v1Msg.sender_device_id = msg.sender_device_id;
2351
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2352
+ attachGatewayProximity(v1Msg, msg);
2353
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2354
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2355
+ }
2356
+ else {
2357
+ client._markPublishedSeq(ns, seq);
2358
+ }
2359
+ decrypted.push(v1Msg);
2360
+ continue;
2361
+ }
2362
+ }
2363
+ else if (payload !== undefined && payload !== null) {
2011
2364
  let v1Msg = {
2012
2365
  message_id: String(msg.message_id ?? ''),
2013
2366
  from: String(msg.from_aid ?? ''),
@@ -2019,194 +2372,211 @@ export class V2E2EECoordinator {
2019
2372
  payload,
2020
2373
  encrypted: false,
2021
2374
  };
2375
+ if (msg.sender_device_id !== undefined)
2376
+ v1Msg.sender_device_id = msg.sender_device_id;
2022
2377
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2023
2378
  attachGatewayProximity(v1Msg, msg);
2024
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2025
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2026
- }
2027
- else {
2028
- client._markPublishedSeq(ns, seq);
2029
- }
2379
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2030
2380
  decrypted.push(v1Msg);
2031
2381
  continue;
2032
2382
  }
2383
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
2384
+ const deferStatus = {};
2385
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2386
+ if (deferStatus.deferred) {
2387
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2388
+ if (deferStatus.fromAid) {
2389
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2390
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2391
+ }
2392
+ }
2393
+ if (plaintext) {
2394
+ plaintext.group_id = gid;
2395
+ plaintext.group_aid = eventGroupAid;
2396
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2397
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2398
+ }
2399
+ else {
2400
+ client._markPublishedSeq(ns, seq);
2401
+ }
2402
+ decrypted.push(plaintext);
2403
+ }
2404
+ }
2405
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2406
+ continue;
2033
2407
  }
2034
- else if (payload !== undefined && payload !== null) {
2035
- let v1Msg = {
2036
- message_id: String(msg.message_id ?? ''),
2037
- from: String(msg.from_aid ?? ''),
2038
- group_id: gid,
2039
- group_aid: eventGroupAid,
2040
- seq: msg.seq,
2041
- type: String(msg.type ?? ''),
2042
- timestamp: msg.t_server,
2043
- payload,
2044
- encrypted: false,
2045
- };
2046
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2047
- attachGatewayProximity(v1Msg, msg);
2048
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2049
- decrypted.push(v1Msg);
2408
+ if (version !== 'v2') {
2409
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2050
2410
  continue;
2051
2411
  }
2052
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
2053
- const deferStatus = {};
2054
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2055
- if (deferStatus.deferred) {
2056
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2057
- if (deferStatus.fromAid) {
2058
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2059
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2412
+ const deferStatus = {};
2413
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2414
+ if (deferStatus.deferred) {
2415
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2416
+ }
2417
+ if (deferStatus.deferred && deferStatus.fromAid) {
2418
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2419
+ deferredKeyFetches.set(key, {
2420
+ fromAid: deferStatus.fromAid,
2421
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2422
+ groupId: deferStatus.groupId ?? '',
2423
+ });
2424
+ }
2425
+ if (plaintext === null)
2426
+ continue;
2427
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2428
+ plaintext.group_id = gid;
2429
+ plaintext.group_aid = eventGroupAid;
2430
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2431
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2432
+ }
2433
+ else {
2434
+ client._markPublishedSeq(ns, seq);
2435
+ }
2436
+ decrypted.push(plaintext);
2437
+ }
2438
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2439
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2440
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2441
+ const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2442
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2443
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2444
+ let commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2445
+ commitTarget = client._delivery.pendingTailDeliveryThrough?.(ns, commitTarget) ?? commitTarget;
2446
+ let ackSeq = pageContigBefore;
2447
+ let contigAdvanced = false;
2448
+ if (ownsCursor) {
2449
+ try {
2450
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2451
+ const state = client._seqTracker.getSyncState?.(ns);
2452
+ const deliveryThrough = client._delivery.pendingTailDeliveryThrough?.(ns, state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget) ?? (state && commitTarget >= state.tail - 1 ? Math.max(commitTarget, state.head) : commitTarget);
2453
+ await client._drainOrderedMessages(ns, undefined, false, false, deliveryThrough);
2454
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2455
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns);
2456
+ client._delivery.commitPendingTailWindow?.(ns, commitTarget);
2457
+ if (commitTarget > pageContigBefore) {
2458
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
2459
+ if (effectiveFloor > pageContigBefore) {
2460
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2060
2461
  }
2061
- }
2062
- if (plaintext) {
2063
- plaintext.group_id = gid;
2064
- plaintext.group_aid = eventGroupAid;
2065
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2066
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2462
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2463
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2067
2464
  }
2068
- else {
2069
- client._markPublishedSeq(ns, seq);
2465
+ if (seqs.length > 0) {
2466
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2070
2467
  }
2071
- decrypted.push(plaintext);
2072
2468
  }
2469
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
2470
+ contigAdvanced = ackSeq !== pageContigBefore;
2471
+ if (contigAdvanced) {
2472
+ await client._commitSeqTrackerState(ns);
2473
+ this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
2474
+ if (pageCount === 1 && deferredServerCursor > 0)
2475
+ this.clearForwardCursor(ns, ackSeq);
2476
+ }
2477
+ client._delivery.completePendingTailWindow?.(ns);
2478
+ }
2479
+ catch (exc) {
2480
+ if (Number(client._v2RuntimeGeneration ?? 0) === generation
2481
+ && (inlineGeneration === undefined || client._delivery.isInlineGenerationCurrent(inlineGeneration))
2482
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2483
+ && pageTrackerSnapshot !== undefined
2484
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2485
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
2486
+ }
2487
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2488
+ throw exc;
2073
2489
  }
2074
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2075
- continue;
2076
- }
2077
- if (version !== 'v2') {
2078
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2079
- continue;
2080
- }
2081
- const deferStatus = {};
2082
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2083
- if (deferStatus.deferred) {
2084
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2085
- }
2086
- if (deferStatus.deferred && deferStatus.fromAid) {
2087
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2088
- deferredKeyFetches.set(key, {
2089
- fromAid: deferStatus.fromAid,
2090
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2091
- groupId: deferStatus.groupId ?? '',
2092
- });
2093
2490
  }
2094
- if (plaintext === null)
2095
- continue;
2096
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2097
- plaintext.group_id = gid;
2098
- plaintext.group_aid = eventGroupAid;
2099
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2100
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2491
+ for (const fetch of deferredKeyFetches.values()) {
2492
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2101
2493
  }
2102
- else {
2103
- client._markPublishedSeq(ns, seq);
2494
+ client._delivery.onPullPage?.(ns, decrypted.length - pageDecryptedBefore, hasMore, pageMaxSeq, typeof result.remaining === 'number' && Number.isSafeInteger(result.remaining) && result.remaining >= 0 ? result.remaining : null, messages.length, client._seqTracker.getContiguousSeq(ns), pageMaxSeq);
2495
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2496
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2104
2497
  }
2105
- decrypted.push(plaintext);
2106
- }
2107
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2108
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2109
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2110
- const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2111
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2112
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2113
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2114
- let ackSeq = pageContigBefore;
2115
- let contigAdvanced = false;
2116
- if (ownsCursor) {
2117
- try {
2118
- if (commitTarget > pageContigBefore) {
2119
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
2120
- if (effectiveFloor > pageContigBefore) {
2121
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2122
- }
2123
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2124
- client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2125
- }
2126
- if (seqs.length > 0) {
2127
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2128
- }
2129
- }
2130
- ackSeq = client._seqTracker.getContiguousSeq(ns);
2131
- contigAdvanced = ackSeq !== pageContigBefore;
2132
- if (contigAdvanced) {
2133
- await client._commitSeqTrackerState(ns);
2134
- if (pageCount === 1 && deferredServerCursor > 0)
2135
- this.clearForwardCursor(ns, ackSeq);
2498
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2499
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2500
+ const knownServerCursorSeq = hasServerCursor
2501
+ ? cursorCurrentSeq
2502
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2503
+ const ackNeeded = ackSeq > 0
2504
+ && ackSeq > lastAutoAckSeq
2505
+ && ownsCursor
2506
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2507
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
2508
+ if (ackNeeded) {
2509
+ this.recordForwardAck(ns, ackSeq);
2510
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2511
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2512
+ if (canContinuePage && !pipelineRunning) {
2513
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2514
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2136
2515
  }
2137
- }
2138
- catch (exc) {
2139
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2140
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2516
+ else {
2517
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2518
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2141
2519
  }
2142
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2143
- throw exc;
2144
2520
  }
2145
- if (contigAdvanced)
2146
- await client._drainOrderedMessages(ns, undefined, false, false);
2147
- }
2148
- for (const fetch of deferredKeyFetches.values()) {
2149
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2150
- }
2151
- 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);
2152
- if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2153
- this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2154
- }
2155
- // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2156
- // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2157
- const knownServerCursorSeq = hasServerCursor
2158
- ? cursorCurrentSeq
2159
- : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2160
- const ackNeeded = ackSeq > 0
2161
- && ackSeq > lastAutoAckSeq
2162
- && ownsCursor
2163
- && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2164
- || (contigAdvanced && ackSeq > knownServerCursorSeq));
2165
- if (ackNeeded) {
2166
- this.recordForwardAck(ns, ackSeq);
2167
2521
  const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2168
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2169
- if (canContinuePage) {
2170
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2171
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2522
+ if (!ownsCursor)
2523
+ break;
2524
+ const rawCount = messages.length;
2525
+ if (blockedSeq > 0)
2526
+ break;
2527
+ const shouldContinue = shouldContinueForwardPage({
2528
+ rawCount,
2529
+ nextAfterSeq,
2530
+ nextAfter,
2531
+ limit,
2532
+ hasMoreField,
2533
+ hasMore,
2534
+ serverHead,
2535
+ pendingAckSeq,
2536
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2537
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2538
+ });
2539
+ const shouldContinueForPipeline = shouldContinueForwardPage({
2540
+ rawCount,
2541
+ nextAfterSeq,
2542
+ nextAfter,
2543
+ limit,
2544
+ hasMoreField,
2545
+ hasMore,
2546
+ serverHead,
2547
+ pendingAckSeq: 0,
2548
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2549
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2550
+ });
2551
+ if (!shouldContinue)
2552
+ break;
2553
+ if (pipelinePage) {
2554
+ pipelineResume?.();
2555
+ pipelineResume = null;
2172
2556
  }
2173
- else {
2174
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2175
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2557
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2558
+ if (pendingAckSeq > 0) {
2559
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2560
+ pendingAckSeq = 0;
2561
+ }
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;
2176
2571
  }
2572
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2573
+ if (tailDelayMs > 0)
2574
+ await v2Sleep(tailDelayMs);
2575
+ nextAfterSeq = nextAfter;
2177
2576
  }
2178
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2179
- if (!ownsCursor)
2180
- break;
2181
- const rawCount = messages.length;
2182
- if (blockedSeq > 0)
2183
- break;
2184
- const shouldContinue = shouldContinueForwardPage({
2185
- rawCount,
2186
- nextAfterSeq,
2187
- nextAfter,
2188
- limit,
2189
- hasMoreField,
2190
- hasMore,
2191
- serverHead,
2192
- pendingAckSeq,
2193
- observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2194
- ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2195
- });
2196
- if (!shouldContinue)
2197
- break;
2198
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2199
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2200
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2201
- _group_cursor_params: cursorParams,
2202
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2203
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2204
- pullGateKey = nextKey;
2205
- }
2206
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2207
- if (tailDelayMs > 0)
2208
- await v2Sleep(tailDelayMs);
2209
- nextAfterSeq = nextAfter;
2577
+ }
2578
+ finally {
2579
+ await stopPipeline();
2210
2580
  }
2211
2581
  if (pageCount >= maxPages) {
2212
2582
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
@@ -2729,7 +3099,11 @@ export class V2E2EECoordinator {
2729
3099
  const senderDeviceId = String(aad.from_device ?? '').trim();
2730
3100
  if (senderDeviceId)
2731
3101
  result.sender_device_id = senderDeviceId;
2732
- result.direction = fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound';
3102
+ const direction = String(msg.direction ?? '').trim();
3103
+ if (direction)
3104
+ result.direction = direction;
3105
+ else if (fromAid && fromAid !== client._aid)
3106
+ result.direction = 'inbound';
2733
3107
  if (msg.t_server !== undefined)
2734
3108
  result.t_server = msg.t_server;
2735
3109
  if (msg.device_id !== undefined)