@agentunion/fastaun 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 (95) 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 +2 -0
  21. package/dist/agent-md-schema.js +46 -9
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.d.ts +46 -1
  24. package/dist/agent-md.js +492 -30
  25. package/dist/agent-md.js.map +1 -1
  26. package/dist/aid-store.d.ts +9 -1
  27. package/dist/aid-store.js +139 -18
  28. package/dist/aid-store.js.map +1 -1
  29. package/dist/aid.d.ts +17 -1
  30. package/dist/aid.js +67 -7
  31. package/dist/aid.js.map +1 -1
  32. package/dist/auth.d.ts +6 -1
  33. package/dist/auth.js +428 -15
  34. package/dist/auth.js.map +1 -1
  35. package/dist/cert-utils.d.ts +1 -1
  36. package/dist/cert-utils.js +19 -7
  37. package/dist/cert-utils.js.map +1 -1
  38. package/dist/client/delivery.d.ts +4 -1
  39. package/dist/client/delivery.js +264 -68
  40. package/dist/client/delivery.js.map +1 -1
  41. package/dist/client/rpc-pipeline.js +29 -14
  42. package/dist/client/rpc-pipeline.js.map +1 -1
  43. package/dist/client/v2-e2ee.d.ts +8 -1
  44. package/dist/client/v2-e2ee.js +893 -529
  45. package/dist/client/v2-e2ee.js.map +1 -1
  46. package/dist/client.d.ts +87 -1
  47. package/dist/client.js +816 -35
  48. package/dist/client.js.map +1 -1
  49. package/dist/errors.js +4 -3
  50. package/dist/errors.js.map +1 -1
  51. package/dist/events.d.ts +5 -1
  52. package/dist/events.js +47 -6
  53. package/dist/events.js.map +1 -1
  54. package/dist/facades.d.ts +85 -3
  55. package/dist/facades.js +104 -4
  56. package/dist/facades.js.map +1 -1
  57. package/dist/index.d.ts +9 -4
  58. package/dist/index.js +3 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/keystore/aid-db.d.ts +5 -0
  61. package/dist/keystore/aid-db.js +48 -0
  62. package/dist/keystore/aid-db.js.map +1 -1
  63. package/dist/keystore/index.d.ts +8 -0
  64. package/dist/keystore/local-identity-store.js +10 -3
  65. package/dist/keystore/local-identity-store.js.map +1 -1
  66. package/dist/keystore/local-token-store.d.ts +6 -1
  67. package/dist/keystore/local-token-store.js +24 -1
  68. package/dist/keystore/local-token-store.js.map +1 -1
  69. package/dist/org-protocol.d.ts +98 -0
  70. package/dist/org-protocol.js +1689 -0
  71. package/dist/org-protocol.js.map +1 -0
  72. package/dist/ownership-proof.d.ts +117 -0
  73. package/dist/ownership-proof.js +436 -0
  74. package/dist/ownership-proof.js.map +1 -0
  75. package/dist/ownership-v21.d.ts +178 -0
  76. package/dist/ownership-v21.js +635 -0
  77. package/dist/ownership-v21.js.map +1 -0
  78. package/dist/ownership-workflow.d.ts +82 -0
  79. package/dist/ownership-workflow.js +872 -0
  80. package/dist/ownership-workflow.js.map +1 -0
  81. package/dist/seq-tracker.d.ts +1 -0
  82. package/dist/seq-tracker.js +10 -1
  83. package/dist/seq-tracker.js.map +1 -1
  84. package/dist/tools/cross-sdk-agent.d.ts +89 -1
  85. package/dist/tools/cross-sdk-agent.js +338 -7
  86. package/dist/tools/cross-sdk-agent.js.map +1 -1
  87. package/dist/transport.d.ts +7 -0
  88. package/dist/transport.js +108 -4
  89. package/dist/transport.js.map +1 -1
  90. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  91. package/dist/v2/e2ee/decrypt.js +9 -1
  92. package/dist/v2/e2ee/decrypt.js.map +1 -1
  93. package/dist/version.d.ts +1 -1
  94. package/dist/version.js +1 -1
  95. package/package.json +8 -8
@@ -10,6 +10,52 @@ const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
10
10
  const V2_PULL_DEFAULT_PAGE_LIMIT = 50;
11
11
  const V2_PULL_MAX_PAGE_LIMIT = 50;
12
12
  const V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS = 3_000;
13
+ const V2_PULL_PIPELINE_QUEUE_CAPACITY = 100;
14
+ class V2PullPageQueue {
15
+ capacity;
16
+ items = [];
17
+ takers = [];
18
+ putters = [];
19
+ queuedWeight = 0;
20
+ closed = false;
21
+ constructor(capacity) {
22
+ this.capacity = capacity;
23
+ }
24
+ async put(item, weight = 1) {
25
+ const itemWeight = Math.max(1, Math.trunc(weight));
26
+ while (!this.closed && this.items.length > 0 && this.queuedWeight + itemWeight > this.capacity) {
27
+ await new Promise((resolve) => this.putters.push(resolve));
28
+ }
29
+ if (this.closed)
30
+ return false;
31
+ const taker = this.takers.shift();
32
+ if (taker)
33
+ taker(item);
34
+ else {
35
+ this.items.push(item);
36
+ this.queuedWeight += itemWeight;
37
+ }
38
+ return true;
39
+ }
40
+ async take() {
41
+ const item = this.items.shift();
42
+ if (item !== undefined) {
43
+ this.queuedWeight = Math.max(0, this.queuedWeight - Math.max(1, Math.trunc(Number(item.weight ?? 1))));
44
+ this.putters.shift()?.();
45
+ return item;
46
+ }
47
+ if (this.closed)
48
+ return undefined;
49
+ return await new Promise((resolve) => this.takers.push(resolve));
50
+ }
51
+ close() {
52
+ this.closed = true;
53
+ for (const resolve of this.takers.splice(0))
54
+ resolve(undefined);
55
+ for (const resolve of this.putters.splice(0))
56
+ resolve();
57
+ }
58
+ }
13
59
  function pullGateKeyForClient(client, method, params, fallback) {
14
60
  const key = client._rpcPipeline?.pullGateKeyForCall?.(method, params);
15
61
  return typeof key === 'string' && key ? key : fallback;
@@ -779,11 +825,12 @@ export class V2E2EECoordinator {
779
825
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
780
826
  continue;
781
827
  }
782
- pending.delete(key);
783
828
  const seq = Number(entry.msg.seq ?? 0);
784
829
  if (entry.groupId) {
785
- if (client._delivery.isSelfSentGroupMessage?.(entry.msg))
830
+ if (client._delivery.isSelfSentGroupMessage?.(entry.msg)) {
831
+ pending.delete(key);
786
832
  continue;
833
+ }
787
834
  plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
788
835
  plaintext.group_id = entry.groupId;
789
836
  await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
@@ -791,6 +838,8 @@ export class V2E2EECoordinator {
791
838
  else {
792
839
  await client._publishPulledMessage('message.received', `p2p:${client._aid ?? ''}`, seq, plaintext, false, 'pending_retry');
793
840
  }
841
+ if (generationIsCurrent())
842
+ pending.delete(key);
794
843
  client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
795
844
  }
796
845
  }
@@ -1076,6 +1125,11 @@ export class V2E2EECoordinator {
1076
1125
  history_error: { code: 'decrypt_failed', message: reason },
1077
1126
  };
1078
1127
  }
1128
+ ensureDeliveryCurrent(ns, generation) {
1129
+ if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
1130
+ || this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
1131
+ throw new StateError('pull invalidated');
1132
+ }
1079
1133
  async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
1080
1134
  const client = this.client;
1081
1135
  const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
@@ -1097,6 +1151,11 @@ export class V2E2EECoordinator {
1097
1151
  payload: payload,
1098
1152
  encrypted: false,
1099
1153
  };
1154
+ if (msg.sender_device_id !== undefined)
1155
+ plaintext.sender_device_id = msg.sender_device_id;
1156
+ const direction = String(msg.direction ?? '').trim();
1157
+ if (direction)
1158
+ plaintext.direction = direction;
1100
1159
  attachGatewayProximity(plaintext, msg);
1101
1160
  }
1102
1161
  else if (isJsonObject(payload)
@@ -1114,6 +1173,9 @@ export class V2E2EECoordinator {
1114
1173
  client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
1115
1174
  return null;
1116
1175
  }
1176
+ if (publish && client._delivery.isSelfSentP2PMessage?.(plaintext)) {
1177
+ return plaintext;
1178
+ }
1117
1179
  if (publish) {
1118
1180
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1119
1181
  await client._publishAppEvent(event.event, event.payload, source);
@@ -1166,6 +1228,8 @@ export class V2E2EECoordinator {
1166
1228
  payload: payload,
1167
1229
  encrypted: false,
1168
1230
  };
1231
+ if (msg.sender_device_id !== undefined)
1232
+ plaintext.sender_device_id = msg.sender_device_id;
1169
1233
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1170
1234
  attachGatewayProximity(plaintext, msg);
1171
1235
  }
@@ -1194,6 +1258,7 @@ export class V2E2EECoordinator {
1194
1258
  }
1195
1259
  async pullV2TailInternal(params, deferPublish = false) {
1196
1260
  const client = this.client;
1261
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1197
1262
  if (params.window_mode !== 'tail')
1198
1263
  throw new ValidationError('window_mode must equal tail');
1199
1264
  if ('ack_up_to_seq' in params || params.force === true)
@@ -1208,6 +1273,14 @@ export class V2E2EECoordinator {
1208
1273
  }, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
1209
1274
  const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
1210
1275
  const page = validateTailPage(result, afterSeq, limit);
1276
+ const decoded = [];
1277
+ for (const msg of page.messages) {
1278
+ this.ensureDeliveryCurrent(ns, generation);
1279
+ const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1280
+ if (plaintext)
1281
+ decoded.push(plaintext);
1282
+ }
1283
+ this.ensureDeliveryCurrent(ns, generation);
1211
1284
  if (ns) {
1212
1285
  client._seqTracker.commitTailWindow(ns, {
1213
1286
  windowStartSeq: page.windowStart,
@@ -1217,12 +1290,7 @@ export class V2E2EECoordinator {
1217
1290
  });
1218
1291
  client._persistSeq(ns);
1219
1292
  await client._saveSeqTrackerState?.();
1220
- }
1221
- const decoded = [];
1222
- for (const msg of page.messages) {
1223
- const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
1224
- if (plaintext)
1225
- decoded.push(plaintext);
1293
+ this.ensureDeliveryCurrent(ns, generation);
1226
1294
  }
1227
1295
  const ack = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1228
1296
  if (ns)
@@ -1252,6 +1320,7 @@ export class V2E2EECoordinator {
1252
1320
  }
1253
1321
  async pullV2Impl(afterSeq = 0, limit = 50, opts) {
1254
1322
  const client = this.client;
1323
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1255
1324
  await client._ensureV2SessionReady('message.pull');
1256
1325
  const ns = client._aid ? `p2p:${client._aid}` : '';
1257
1326
  if (ns)
@@ -1290,288 +1359,434 @@ export class V2E2EECoordinator {
1290
1359
  let lastAutoAckSeq = 0;
1291
1360
  let pendingAckSeq = deferredForwardAck;
1292
1361
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
1293
- while (pageCount < maxPages) {
1294
- pageCount += 1;
1295
- const requestedVolatileCursor = volatileCursor;
1296
- const ackUpToSeq = pendingAckSeq;
1297
- const inlineAckGeneration = ackUpToSeq > 0
1298
- ? client._delivery.captureP2PInlineAckGeneration()
1299
- : null;
1300
- client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
1301
- let result;
1302
- try {
1303
- result = await client._callRawV2Rpc('message.v2.pull', {
1304
- after_seq: nextAfterSeq,
1305
- limit,
1306
- ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1307
- ...(opts?.force ? { force: true } : {}),
1308
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1309
- }, pullGateKey);
1310
- }
1311
- catch (exc) {
1312
- if (!volatileCursor || !String(exc instanceof Error ? exc.message : exc).toLowerCase().includes('volatile cursor epoch mismatch')) {
1313
- throw exc;
1362
+ let pipelineRunning = false;
1363
+ let pipelineError = null;
1364
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1365
+ let pipelineProducer = null;
1366
+ let pipelineStopped = false;
1367
+ const pipelineResume = { current: null };
1368
+ const startPipeline = (startAfterSeq, startVolatileCursor) => {
1369
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
1370
+ return;
1371
+ pipelineRunning = true;
1372
+ pipelineProducer = (async () => {
1373
+ let producerAfterSeq = startAfterSeq;
1374
+ let producerVolatileCursor = startVolatileCursor;
1375
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
1376
+ if (pipelineStopped)
1377
+ break;
1378
+ const requestedVolatileCursor = producerVolatileCursor;
1379
+ const result = await client._callRawV2Rpc('message.v2.pull', {
1380
+ after_seq: producerAfterSeq,
1381
+ limit,
1382
+ ...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
1383
+ ...(opts?.force ? { force: true } : {}),
1384
+ }, pullGateKey);
1385
+ if (pipelineStopped)
1386
+ break;
1387
+ const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1388
+ const completed = new Promise((resolve) => { pipelineResume.current = resolve; });
1389
+ const enqueued = await pipelineQueue.put({
1390
+ result,
1391
+ afterSeq: producerAfterSeq,
1392
+ ackUpToSeq: 0,
1393
+ inlineAckGeneration: null,
1394
+ requestedVolatileCursor,
1395
+ weight: rawPageMessages.length,
1396
+ }, rawPageMessages.length);
1397
+ if (!enqueued || pipelineStopped)
1398
+ break;
1399
+ await completed;
1400
+ if (pipelineStopped)
1401
+ break;
1402
+ const volatileMessages = client._delivery.volatileMessages?.(rawPageMessages) ?? [];
1403
+ if (volatileMessages.length > 0)
1404
+ break;
1405
+ const messages = deduplicateForwardPageMessages(rawPageMessages, producerAfterSeq);
1406
+ const seqs = messages
1407
+ .map((msg) => Number(msg.seq ?? 0))
1408
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1409
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
1410
+ const cursor = isJsonObject(result.cursor)
1411
+ ? result.cursor : null;
1412
+ const shouldContinue = shouldContinueForwardPage({
1413
+ rawCount: messages.length,
1414
+ nextAfterSeq: producerAfterSeq,
1415
+ nextAfter,
1416
+ limit,
1417
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
1418
+ hasMore: result.has_more === true,
1419
+ serverHead: safeNonNegativeSeq(cursor?.latest_seq) ?? 0,
1420
+ observedUpper: Math.max(nextAfter, Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1421
+ pendingAckSeq: 0,
1422
+ });
1423
+ if (!shouldContinue)
1424
+ break;
1425
+ producerAfterSeq = nextAfter;
1426
+ producerVolatileCursor = '';
1314
1427
  }
1315
- client._delivery.resetVolatilePullCursor?.();
1316
- volatileCursor = '';
1317
- pageCount -= 1;
1318
- continue;
1319
- }
1320
- if (ackUpToSeq > 0) {
1321
- const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
1322
- if (actualAckSeq >= ackUpToSeq) {
1323
- if (inlineAckGeneration !== null) {
1324
- client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1428
+ })().catch((error) => {
1429
+ pipelineError = error;
1430
+ }).finally(() => {
1431
+ pipelineQueue.close();
1432
+ });
1433
+ };
1434
+ const stopPipeline = async () => {
1435
+ pipelineStopped = true;
1436
+ pipelineResume.current?.();
1437
+ pipelineResume.current = null;
1438
+ pipelineQueue.close();
1439
+ if (pipelineProducer) {
1440
+ const producer = pipelineProducer;
1441
+ pipelineProducer = null;
1442
+ await producer;
1443
+ }
1444
+ pipelineRunning = false;
1445
+ };
1446
+ try {
1447
+ while (pageCount < maxPages) {
1448
+ let pipelinePage;
1449
+ if (pipelineRunning) {
1450
+ pipelinePage = await pipelineQueue.take();
1451
+ if (!pipelinePage) {
1452
+ pipelineRunning = false;
1453
+ if (pipelineError)
1454
+ throw pipelineError;
1455
+ break;
1325
1456
  }
1326
- pendingAckSeq = 0;
1327
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1328
- this.clearForwardAck(ns, ackUpToSeq);
1329
1457
  }
1330
- else {
1331
- client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1332
- await this.confirmForwardP2PAck(ackUpToSeq);
1333
- pendingAckSeq = 0;
1334
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1458
+ pageCount += 1;
1459
+ const requestedVolatileCursor = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1460
+ const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1461
+ const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1462
+ ? client._delivery.captureP2PInlineAckGeneration()
1463
+ : null);
1464
+ client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
1465
+ let result;
1466
+ if (pipelinePage) {
1467
+ nextAfterSeq = pipelinePage.afterSeq;
1468
+ result = pipelinePage.result;
1335
1469
  }
1336
- }
1337
- const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1338
- const volatileMessages = client._delivery.volatileMessages?.(pageMessages) ?? [];
1339
- if (volatileMessages.length > 0) {
1340
- decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1341
- volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
1342
- }
1343
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1344
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1345
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1346
- const hasMore = result.has_more === true;
1347
- const volatileHasMore = result.volatile_has_more === true;
1348
- const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1349
- 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 ?? '')}`);
1350
- for (const msg of messages) {
1351
- client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
1352
- }
1353
- const seqs = messages
1354
- .map((msg) => Number(msg.seq ?? 0))
1355
- .filter((seq) => Number.isFinite(seq) && seq > 0);
1356
- const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1357
- const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1358
- ? client._seqTracker.snapshotNamespace(ns)
1359
- : null;
1360
- let pageMaxSeq = nextAfterSeq;
1361
- if (seqs.length > 0) {
1362
- pageMaxSeq = Math.max(...seqs);
1363
- }
1364
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1365
- const deferredKeyFetches = new Map();
1366
- let blockedSeq = 0;
1367
- const pageDecryptedBefore = decrypted.length;
1368
- for (const msg of messages) {
1369
- const seq = Number(msg.seq ?? 0);
1370
- if (!Number.isFinite(seq) || seq <= 0)
1371
- continue;
1372
- const version = String(msg.version ?? 'v2');
1373
- if (version === 'v1') {
1374
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1375
- const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1376
- const payloadType = isJsonObject(legacyPayload)
1377
- ? String(legacyPayload.type ?? '').trim()
1378
- : '';
1379
- if (legacyPayload !== undefined && legacyPayload !== null
1380
- && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1381
- const v1Msg = {
1382
- message_id: String(msg.message_id ?? ''),
1383
- from: String(msg.from_aid ?? ''),
1384
- to: String(legacy.to ?? client._aid ?? ''),
1385
- seq: msg.seq,
1386
- type: String(msg.type ?? ''),
1387
- timestamp: msg.t_server,
1388
- payload: legacyPayload,
1389
- encrypted: false,
1390
- };
1391
- attachGatewayProximity(v1Msg, msg);
1392
- const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1393
- if (ns) {
1394
- await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1395
- }
1396
- else {
1397
- await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1398
- }
1399
- decrypted.push(v1Msg);
1400
- client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
1470
+ else {
1471
+ try {
1472
+ result = await client._callRawV2Rpc('message.v2.pull', {
1473
+ after_seq: nextAfterSeq,
1474
+ limit,
1475
+ ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1476
+ ...(opts?.force ? { force: true } : {}),
1477
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1478
+ }, pullGateKey);
1401
1479
  }
1402
- else if (isJsonObject(legacyPayload)
1403
- && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1404
- const deferStatus = {};
1405
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1406
- if (deferStatus.deferred) {
1407
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1408
- if (deferStatus.fromAid) {
1409
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1410
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1411
- }
1480
+ catch (exc) {
1481
+ if (!volatileCursor || !String(exc instanceof Error ? exc.message : exc).toLowerCase().includes('volatile cursor epoch mismatch')) {
1482
+ throw exc;
1412
1483
  }
1413
- if (plaintext) {
1414
- if (ns)
1415
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1416
- else
1417
- await client._publishAppEvent('message.received', plaintext, 'pull');
1418
- decrypted.push(plaintext);
1484
+ client._delivery.resetVolatilePullCursor?.();
1485
+ volatileCursor = '';
1486
+ pageCount -= 1;
1487
+ continue;
1488
+ }
1489
+ }
1490
+ if (ackUpToSeq > 0) {
1491
+ const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
1492
+ if (actualAckSeq >= ackUpToSeq) {
1493
+ if (inlineAckGeneration !== null) {
1494
+ client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
1419
1495
  }
1496
+ pendingAckSeq = 0;
1497
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1498
+ this.clearForwardAck(ns, ackUpToSeq);
1420
1499
  }
1421
1500
  else {
1422
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1501
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1502
+ await this.confirmForwardP2PAck(ackUpToSeq);
1503
+ pendingAckSeq = 0;
1504
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1423
1505
  }
1424
- continue;
1425
1506
  }
1426
- if (version !== 'v2') {
1427
- client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1428
- continue;
1429
- }
1430
- const spkId = String(msg.spk_id ?? '');
1431
- if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1432
- client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1507
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1508
+ const volatileMessages = client._delivery.volatileMessages?.(pageMessages) ?? [];
1509
+ if (volatileMessages.length > 0) {
1510
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1511
+ volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
1433
1512
  }
1434
- const deferStatus = {};
1435
- const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1436
- if (deferStatus.deferred) {
1437
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1513
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1514
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1515
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1516
+ const hasMore = result.has_more === true;
1517
+ const volatileHasMore = result.volatile_has_more === true;
1518
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1519
+ 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 ?? '')}`);
1520
+ for (const msg of messages) {
1521
+ client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
1438
1522
  }
1439
- if (deferStatus.deferred && deferStatus.fromAid) {
1440
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1441
- deferredKeyFetches.set(key, {
1442
- fromAid: deferStatus.fromAid,
1443
- senderDeviceId: deferStatus.senderDeviceId ?? '',
1444
- groupId: deferStatus.groupId ?? '',
1445
- });
1523
+ const seqs = messages
1524
+ .map((msg) => Number(msg.seq ?? 0))
1525
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1526
+ const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1527
+ let pageTrackerSnapshot = null;
1528
+ let trackerMutated = false;
1529
+ let pageMaxSeq = nextAfterSeq;
1530
+ if (seqs.length > 0) {
1531
+ pageMaxSeq = Math.max(...seqs);
1446
1532
  }
1447
- if (plaintext === null) {
1448
- client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1449
- continue;
1533
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1534
+ const deferredKeyFetches = new Map();
1535
+ let blockedSeq = 0;
1536
+ const pageDecryptedBefore = decrypted.length;
1537
+ for (const msg of messages) {
1538
+ const seq = Number(msg.seq ?? 0);
1539
+ if (!Number.isFinite(seq) || seq <= 0)
1540
+ continue;
1541
+ const version = String(msg.version ?? 'v2');
1542
+ if (version === 'v1') {
1543
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1544
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1545
+ const payloadType = isJsonObject(legacyPayload)
1546
+ ? String(legacyPayload.type ?? '').trim()
1547
+ : '';
1548
+ if (legacyPayload !== undefined && legacyPayload !== null
1549
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1550
+ const v1Msg = {
1551
+ message_id: String(msg.message_id ?? ''),
1552
+ from: String(msg.from_aid ?? ''),
1553
+ to: String(legacy.to ?? client._aid ?? ''),
1554
+ seq: msg.seq,
1555
+ type: String(msg.type ?? ''),
1556
+ timestamp: msg.t_server,
1557
+ payload: legacyPayload,
1558
+ encrypted: false,
1559
+ };
1560
+ const direction = String(msg.direction ?? '').trim();
1561
+ if (direction)
1562
+ v1Msg.direction = direction;
1563
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1564
+ v1Msg.direction = 'inbound';
1565
+ }
1566
+ if (msg.sender_device_id !== undefined)
1567
+ v1Msg.sender_device_id = msg.sender_device_id;
1568
+ attachGatewayProximity(v1Msg, msg);
1569
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1570
+ if (ns) {
1571
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1572
+ }
1573
+ else {
1574
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1575
+ }
1576
+ decrypted.push(v1Msg);
1577
+ client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
1578
+ }
1579
+ else if (isJsonObject(legacyPayload)
1580
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1581
+ const deferStatus = {};
1582
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1583
+ if (deferStatus.deferred) {
1584
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1585
+ if (deferStatus.fromAid) {
1586
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1587
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1588
+ }
1589
+ }
1590
+ if (plaintext) {
1591
+ if (ns)
1592
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1593
+ else
1594
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1595
+ decrypted.push(plaintext);
1596
+ }
1597
+ }
1598
+ else {
1599
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1600
+ }
1601
+ continue;
1602
+ }
1603
+ if (version !== 'v2') {
1604
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1605
+ continue;
1606
+ }
1607
+ const spkId = String(msg.spk_id ?? '');
1608
+ if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
1609
+ client._v2Session.trackOldSPKMaxSeq(spkId, seq);
1610
+ }
1611
+ const deferStatus = {};
1612
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1613
+ if (deferStatus.deferred) {
1614
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1615
+ }
1616
+ if (deferStatus.deferred && deferStatus.fromAid) {
1617
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1618
+ deferredKeyFetches.set(key, {
1619
+ fromAid: deferStatus.fromAid,
1620
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1621
+ groupId: deferStatus.groupId ?? '',
1622
+ });
1623
+ }
1624
+ if (plaintext === null) {
1625
+ client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
1626
+ continue;
1627
+ }
1628
+ if (ns) {
1629
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1630
+ }
1631
+ else {
1632
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1633
+ }
1634
+ decrypted.push(plaintext);
1635
+ client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1450
1636
  }
1637
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1638
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1639
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1640
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1451
1641
  if (ns) {
1452
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1453
- }
1454
- else {
1455
- await client._publishAppEvent('message.received', plaintext, 'pull');
1456
- }
1457
- decrypted.push(plaintext);
1458
- client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
1459
- }
1460
- const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1461
- const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1462
- const serverAckSeq = parsedServerAckSeq ?? 0;
1463
- const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1464
- if (ns) {
1465
- const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1466
- let ackSeq = pageContigBefore;
1467
- let contigAdvanced = false;
1468
- try {
1469
- if (commitTarget > pageContigBefore) {
1470
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
1471
- if (retentionFloor > pageContigBefore) {
1472
- client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1642
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1643
+ let ackSeq = pageContigBefore;
1644
+ let contigAdvanced = false;
1645
+ try {
1646
+ this.ensureDeliveryCurrent(ns, generation);
1647
+ const window = client._seqTracker.getSyncState?.(ns);
1648
+ const deliveryTarget = window && commitTarget >= window.tail - 1
1649
+ ? Math.max(commitTarget, window.head) : commitTarget;
1650
+ if (deliveryTarget > pageContigBefore) {
1651
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
1652
+ }
1653
+ this.ensureDeliveryCurrent(ns, generation);
1654
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
1655
+ if (commitTarget > pageContigBefore) {
1656
+ trackerMutated = true;
1657
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1658
+ if (retentionFloor > pageContigBefore) {
1659
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1660
+ }
1661
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1662
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1663
+ }
1664
+ if (seqs.length > 0) {
1665
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1666
+ }
1473
1667
  }
1474
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1475
- client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1668
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1669
+ contigAdvanced = ackSeq !== pageContigBefore;
1670
+ if (contigAdvanced) {
1671
+ await client._commitSeqTrackerState(ns);
1672
+ this.ensureDeliveryCurrent(ns, generation);
1673
+ if (pageCount === 1 && deferredServerCursor > 0)
1674
+ this.clearForwardCursor(ns, ackSeq);
1476
1675
  }
1477
- if (seqs.length > 0) {
1478
- client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1676
+ }
1677
+ catch (exc) {
1678
+ if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
1679
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
1680
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1681
+ (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
1682
+ .call(client._seqTracker, ns, pageTrackerSnapshot);
1479
1683
  }
1684
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1685
+ throw exc;
1480
1686
  }
1481
- ackSeq = client._seqTracker.getContiguousSeq(ns);
1482
- contigAdvanced = ackSeq !== pageContigBefore;
1483
- if (contigAdvanced) {
1484
- await client._commitSeqTrackerState(ns);
1485
- if (pageCount === 1 && deferredServerCursor > 0)
1486
- this.clearForwardCursor(ns, ackSeq);
1687
+ for (const fetch of deferredKeyFetches.values()) {
1688
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1487
1689
  }
1488
- }
1489
- catch (exc) {
1490
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1491
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1690
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1691
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1692
+ }
1693
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1694
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1695
+ const knownServerAckSeq = hasServerAckSeq
1696
+ ? serverAckSeq
1697
+ : (pageCount === 1 ? deferredServerCursor : 0);
1698
+ const ackNeeded = ackSeq > 0
1699
+ && ackSeq > lastAutoAckSeq
1700
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1701
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1702
+ if (ackNeeded) {
1703
+ this.recordForwardAck(ns, ackSeq);
1704
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1705
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1706
+ if (canContinuePage && !pipelineRunning) {
1707
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1708
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1709
+ client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
1710
+ }
1711
+ else {
1712
+ client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1713
+ await this.confirmForwardP2PAck(ackSeq);
1714
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1715
+ }
1492
1716
  }
1493
- client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1494
- throw exc;
1495
- }
1496
- if (contigAdvanced)
1497
- await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
1498
- for (const fetch of deferredKeyFetches.values()) {
1499
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1500
- }
1501
- if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1502
- this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1503
1717
  }
1504
- // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1505
- // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1506
- const knownServerAckSeq = hasServerAckSeq
1507
- ? serverAckSeq
1508
- : (pageCount === 1 ? deferredServerCursor : 0);
1509
- const ackNeeded = ackSeq > 0
1510
- && ackSeq > lastAutoAckSeq
1511
- && ((hasServerAckSeq && ackSeq > serverAckSeq)
1512
- || (contigAdvanced && ackSeq > knownServerAckSeq));
1513
- if (ackNeeded) {
1514
- this.recordForwardAck(ns, ackSeq);
1515
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1516
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1517
- if (canContinuePage) {
1518
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1519
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1520
- client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
1718
+ if (!ns) {
1719
+ for (const fetch of deferredKeyFetches.values()) {
1720
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1521
1721
  }
1522
- else {
1523
- client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
1524
- await this.confirmForwardP2PAck(ackSeq);
1525
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1722
+ }
1723
+ if (ns)
1724
+ client._delivery.syncProgress(ns, {
1725
+ rawCount: messages.length,
1726
+ pulledCount: decrypted.length - pageDecryptedBefore,
1727
+ remaining: result.remaining,
1728
+ hasMore: hasMoreField ? hasMore : undefined,
1729
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
1730
+ targetSeq: serverHead,
1731
+ });
1732
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1733
+ const rawCount = messages.length;
1734
+ if (blockedSeq > 0)
1735
+ break;
1736
+ const shouldContinuePersistent = shouldContinueForwardPage({
1737
+ rawCount,
1738
+ nextAfterSeq,
1739
+ nextAfter,
1740
+ limit,
1741
+ hasMoreField,
1742
+ hasMore,
1743
+ serverHead,
1744
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1745
+ pendingAckSeq,
1746
+ });
1747
+ const shouldContinueForPipeline = shouldContinueForwardPage({
1748
+ rawCount,
1749
+ nextAfterSeq,
1750
+ nextAfter,
1751
+ limit,
1752
+ hasMoreField,
1753
+ hasMore,
1754
+ serverHead,
1755
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1756
+ pendingAckSeq: 0,
1757
+ });
1758
+ const shouldContinueVolatile = (hasMore || volatileHasMore)
1759
+ && volatileMessages.length > 0
1760
+ && Boolean(volatileCursor)
1761
+ && volatileCursor !== requestedVolatileCursor;
1762
+ if (!shouldContinuePersistent && !shouldContinueVolatile)
1763
+ break;
1764
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1765
+ if (pendingAckSeq > 0) {
1766
+ await this.confirmForwardP2PAck(pendingAckSeq);
1767
+ pendingAckSeq = 0;
1526
1768
  }
1769
+ startPipeline(nextAfter, volatileCursor);
1527
1770
  }
1528
- }
1529
- if (!ns) {
1530
- for (const fetch of deferredKeyFetches.values()) {
1531
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1771
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1772
+ const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1773
+ after_seq: nextAfter, force: opts?.force === true, limit,
1774
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1775
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1776
+ pullGateKey = nextKey;
1777
+ }
1778
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1779
+ if (tailDelayMs > 0)
1780
+ await v2Sleep(tailDelayMs);
1781
+ nextAfterSeq = nextAfter;
1782
+ if (pipelinePage) {
1783
+ pipelineResume.current?.();
1784
+ pipelineResume.current = null;
1532
1785
  }
1533
1786
  }
1534
- if (ns)
1535
- client._delivery.syncProgress(ns, {
1536
- rawCount: messages.length,
1537
- pulledCount: decrypted.length - pageDecryptedBefore,
1538
- remaining: result.remaining,
1539
- hasMore: hasMoreField ? hasMore : undefined,
1540
- currentSeq: client._seqTracker.getContiguousSeq(ns),
1541
- targetSeq: serverHead,
1542
- });
1543
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1544
- const rawCount = messages.length;
1545
- if (blockedSeq > 0)
1546
- break;
1547
- const shouldContinuePersistent = shouldContinueForwardPage({
1548
- rawCount,
1549
- nextAfterSeq,
1550
- nextAfter,
1551
- limit,
1552
- hasMoreField,
1553
- hasMore,
1554
- serverHead,
1555
- observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1556
- pendingAckSeq,
1557
- });
1558
- const shouldContinueVolatile = (hasMore || volatileHasMore)
1559
- && volatileMessages.length > 0
1560
- && Boolean(volatileCursor)
1561
- && volatileCursor !== requestedVolatileCursor;
1562
- if (!shouldContinuePersistent && !shouldContinueVolatile)
1563
- break;
1564
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1565
- const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1566
- after_seq: nextAfter, force: opts?.force === true, limit,
1567
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1568
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1569
- pullGateKey = nextKey;
1570
- }
1571
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1572
- if (tailDelayMs > 0)
1573
- await v2Sleep(tailDelayMs);
1574
- nextAfterSeq = nextAfter;
1787
+ }
1788
+ finally {
1789
+ await stopPipeline();
1575
1790
  }
1576
1791
  if (pageCount >= maxPages) {
1577
1792
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
@@ -1948,6 +2163,7 @@ export class V2E2EECoordinator {
1948
2163
  }
1949
2164
  async pullGroupV2TailInternal(params, deferPublish = false) {
1950
2165
  const client = this.client;
2166
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
1951
2167
  if (params.window_mode !== 'tail')
1952
2168
  throw new ValidationError('window_mode must equal tail');
1953
2169
  if ('ack_up_to_seq' in params || params.force === true)
@@ -1987,6 +2203,15 @@ export class V2E2EECoordinator {
1987
2203
  if (resultAid)
1988
2204
  groupAid = resultAid;
1989
2205
  const page = validateTailPage(result, afterSeq, limit);
2206
+ const decoded = [];
2207
+ for (const msg of page.messages) {
2208
+ this.ensureDeliveryCurrent(ns, generation);
2209
+ client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2210
+ const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2211
+ if (plaintext)
2212
+ decoded.push(plaintext);
2213
+ }
2214
+ this.ensureDeliveryCurrent(ns, generation);
1990
2215
  client._seqTracker.commitTailWindow(ns, {
1991
2216
  windowStartSeq: page.windowStart,
1992
2217
  headSeq: page.head,
@@ -1995,13 +2220,7 @@ export class V2E2EECoordinator {
1995
2220
  });
1996
2221
  client._persistSeq(ns);
1997
2222
  await client._saveSeqTrackerState?.();
1998
- const decoded = [];
1999
- for (const msg of page.messages) {
2000
- client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
2001
- const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
2002
- if (plaintext)
2003
- decoded.push(plaintext);
2004
- }
2223
+ this.ensureDeliveryCurrent(ns, generation);
2005
2224
  const ack = client._seqTracker.getContiguousSeq(ns);
2006
2225
  if (ownsCursor)
2007
2226
  client._delivery.syncProgress(ns, {
@@ -2049,6 +2268,7 @@ export class V2E2EECoordinator {
2049
2268
  }
2050
2269
  async pullGroupV2Impl(groupId, afterSeq = 0, limit = 50, opts) {
2051
2270
  const client = this.client;
2271
+ const generation = Number(client._v2RuntimeGeneration ?? 0);
2052
2272
  await client._ensureV2SessionReady('group.pull');
2053
2273
  const gid = String(groupId ?? '').trim();
2054
2274
  if (!gid)
@@ -2109,91 +2329,214 @@ export class V2E2EECoordinator {
2109
2329
  let lastAutoAckSeq = 0;
2110
2330
  let pendingAckSeq = deferredForwardAck;
2111
2331
  const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
2112
- while (pageCount < maxPages) {
2113
- pageCount += 1;
2114
- const ackUpToSeq = ownsCursor ? pendingAckSeq : 0;
2115
- const inlineAckGeneration = ackUpToSeq > 0
2116
- ? client._delivery.captureGroupInlineAckGeneration()
2117
- : null;
2118
- client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
2119
- const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2120
- group_id: wireGroupId,
2121
- after_seq: nextAfterSeq,
2122
- limit,
2123
- ...cursorParams,
2124
- ...(opts?.force ? { force: true } : {}),
2125
- ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2126
- }, groupAid), pullGateKey);
2127
- if (ackUpToSeq > 0) {
2128
- const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
2129
- if (actualAckSeq >= ackUpToSeq) {
2130
- if (inlineAckGeneration !== null) {
2131
- client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
2332
+ let pipelineRunning = false;
2333
+ let pipelineError = null;
2334
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2335
+ let pipelineProducer = null;
2336
+ let pipelineStopped = false;
2337
+ const pipelineResume = { current: null };
2338
+ const startPipeline = (startAfterSeq) => {
2339
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
2340
+ return;
2341
+ pipelineRunning = true;
2342
+ pipelineProducer = (async () => {
2343
+ let producerAfterSeq = startAfterSeq;
2344
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
2345
+ if (pipelineStopped)
2346
+ break;
2347
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2348
+ group_id: wireGroupId,
2349
+ after_seq: producerAfterSeq,
2350
+ limit,
2351
+ ...cursorParams,
2352
+ ...(opts?.force ? { force: true } : {}),
2353
+ }, groupAid), pullGateKey);
2354
+ if (pipelineStopped)
2355
+ break;
2356
+ const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
2357
+ const completed = new Promise((resolve) => { pipelineResume.current = resolve; });
2358
+ const enqueued = await pipelineQueue.put({
2359
+ result,
2360
+ afterSeq: producerAfterSeq,
2361
+ ackUpToSeq: 0,
2362
+ inlineAckGeneration: null,
2363
+ requestedVolatileCursor: '',
2364
+ weight: pageMessages.length,
2365
+ }, pageMessages.length);
2366
+ if (!enqueued || pipelineStopped)
2367
+ break;
2368
+ await completed;
2369
+ if (pipelineStopped)
2370
+ break;
2371
+ const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
2372
+ const seqs = messages
2373
+ .map((msg) => Number(msg.seq ?? 0))
2374
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2375
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
2376
+ const cursor = isJsonObject(result.cursor)
2377
+ ? result.cursor : null;
2378
+ const shouldContinue = shouldContinueForwardPage({
2379
+ rawCount: messages.length,
2380
+ nextAfterSeq: producerAfterSeq,
2381
+ nextAfter,
2382
+ limit,
2383
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
2384
+ hasMore: result.has_more === true,
2385
+ serverHead: Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0),
2386
+ observedUpper: Math.max(nextAfter, client._seqTracker.getMaxSeenSeq(ns)),
2387
+ pendingAckSeq: 0,
2388
+ });
2389
+ if (!shouldContinue)
2390
+ break;
2391
+ producerAfterSeq = nextAfter;
2392
+ }
2393
+ })().catch((error) => {
2394
+ pipelineError = error;
2395
+ }).finally(() => {
2396
+ pipelineQueue.close();
2397
+ });
2398
+ };
2399
+ const stopPipeline = async () => {
2400
+ pipelineStopped = true;
2401
+ pipelineResume.current?.();
2402
+ pipelineResume.current = null;
2403
+ pipelineQueue.close();
2404
+ if (pipelineProducer) {
2405
+ const producer = pipelineProducer;
2406
+ pipelineProducer = null;
2407
+ await producer;
2408
+ }
2409
+ pipelineRunning = false;
2410
+ };
2411
+ try {
2412
+ while (pageCount < maxPages) {
2413
+ let pipelinePage;
2414
+ if (pipelineRunning) {
2415
+ pipelinePage = await pipelineQueue.take();
2416
+ if (!pipelinePage) {
2417
+ pipelineRunning = false;
2418
+ if (pipelineError)
2419
+ throw pipelineError;
2420
+ break;
2132
2421
  }
2133
- pendingAckSeq = 0;
2134
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2135
- this.clearForwardAck(ns, ackUpToSeq);
2422
+ }
2423
+ pageCount += 1;
2424
+ const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2425
+ const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2426
+ ? client._delivery.captureGroupInlineAckGeneration()
2427
+ : null);
2428
+ client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
2429
+ let result;
2430
+ if (pipelinePage) {
2431
+ nextAfterSeq = pipelinePage.afterSeq;
2432
+ result = pipelinePage.result;
2136
2433
  }
2137
2434
  else {
2138
- client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2139
- await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2140
- pendingAckSeq = 0;
2141
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2435
+ result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2436
+ group_id: wireGroupId,
2437
+ after_seq: nextAfterSeq,
2438
+ limit,
2439
+ ...cursorParams,
2440
+ ...(opts?.force ? { force: true } : {}),
2441
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2442
+ }, groupAid), pullGateKey);
2142
2443
  }
2143
- }
2144
- const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2145
- if (resultGroupAid)
2146
- groupAid = resultGroupAid;
2147
- const eventGroupAid = resultGroupAid || groupAid || gid;
2148
- const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
2149
- const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2150
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2151
- const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2152
- const hasMore = result.has_more === true;
2153
- const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2154
- client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, cursor_current=${String(cursor?.current_seq ?? '')}`);
2155
- for (const msg of messages) {
2156
- client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
2157
- }
2158
- const seqs = messages
2159
- .map((msg) => Number(msg.seq ?? 0))
2160
- .filter((seq) => Number.isFinite(seq) && seq > 0);
2161
- const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2162
- const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
2163
- ? client._seqTracker.snapshotNamespace(ns)
2164
- : null;
2165
- let pageMaxSeq = nextAfterSeq;
2166
- if (seqs.length > 0) {
2167
- pageMaxSeq = Math.max(...seqs);
2168
- }
2169
- const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2170
- const deferredKeyFetches = new Map();
2171
- let blockedSeq = 0;
2172
- const pageDecryptedBefore = decrypted.length;
2173
- for (const msg of messages) {
2174
- const seq = Number(msg.seq ?? 0);
2175
- if (!Number.isFinite(seq) || seq <= 0)
2176
- continue;
2177
- const version = String(msg.version ?? 'v2');
2178
- if (version === 'v1') {
2179
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2180
- const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2181
- const payloadObj = isJsonObject(payload) ? payload : null;
2182
- const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2183
- // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2184
- if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2185
- && client._delivery.recallEventFromGroupMessage(msg)) {
2186
- await client._delivery.publishGroupRecallTombstone(gid, seq, {
2187
- ...msg,
2188
- group_id: gid,
2189
- group_aid: eventGroupAid,
2190
- }, 'pull');
2191
- client._markPublishedSeq(ns, seq);
2192
- client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2193
- continue;
2444
+ if (ackUpToSeq > 0) {
2445
+ const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
2446
+ if (actualAckSeq >= ackUpToSeq) {
2447
+ if (inlineAckGeneration !== null) {
2448
+ client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
2449
+ }
2450
+ pendingAckSeq = 0;
2451
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2452
+ this.clearForwardAck(ns, ackUpToSeq);
2194
2453
  }
2195
- if (payloadObj) {
2196
- if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2454
+ else {
2455
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2456
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2457
+ pendingAckSeq = 0;
2458
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2459
+ }
2460
+ }
2461
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2462
+ if (resultGroupAid)
2463
+ groupAid = resultGroupAid;
2464
+ const eventGroupAid = resultGroupAid || groupAid || gid;
2465
+ const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
2466
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2467
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2468
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2469
+ const hasMore = result.has_more === true;
2470
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2471
+ client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, cursor_current=${String(cursor?.current_seq ?? '')}`);
2472
+ for (const msg of messages) {
2473
+ client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
2474
+ }
2475
+ const seqs = messages
2476
+ .map((msg) => Number(msg.seq ?? 0))
2477
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2478
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2479
+ let pageTrackerSnapshot = null;
2480
+ let trackerMutated = false;
2481
+ let pageMaxSeq = nextAfterSeq;
2482
+ if (seqs.length > 0) {
2483
+ pageMaxSeq = Math.max(...seqs);
2484
+ }
2485
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2486
+ const deferredKeyFetches = new Map();
2487
+ let blockedSeq = 0;
2488
+ const pageDecryptedBefore = decrypted.length;
2489
+ for (const msg of messages) {
2490
+ const seq = Number(msg.seq ?? 0);
2491
+ if (!Number.isFinite(seq) || seq <= 0)
2492
+ continue;
2493
+ const version = String(msg.version ?? 'v2');
2494
+ if (version === 'v1') {
2495
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2496
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2497
+ const payloadObj = isJsonObject(payload) ? payload : null;
2498
+ const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
2499
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2500
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2501
+ && client._delivery.recallEventFromGroupMessage(msg)) {
2502
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
2503
+ ...msg,
2504
+ group_id: gid,
2505
+ group_aid: eventGroupAid,
2506
+ }, 'pull');
2507
+ client._markPublishedSeq(ns, seq);
2508
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2509
+ continue;
2510
+ }
2511
+ if (payloadObj) {
2512
+ if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
2513
+ let v1Msg = {
2514
+ message_id: String(msg.message_id ?? ''),
2515
+ from: String(msg.from_aid ?? ''),
2516
+ group_id: gid,
2517
+ group_aid: eventGroupAid,
2518
+ seq: msg.seq,
2519
+ type: String(msg.type ?? ''),
2520
+ timestamp: msg.t_server,
2521
+ payload,
2522
+ encrypted: false,
2523
+ };
2524
+ if (msg.sender_device_id !== undefined)
2525
+ v1Msg.sender_device_id = msg.sender_device_id;
2526
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2527
+ attachGatewayProximity(v1Msg, msg);
2528
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2529
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2530
+ }
2531
+ else {
2532
+ client._markPublishedSeq(ns, seq);
2533
+ }
2534
+ decrypted.push(v1Msg);
2535
+ client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2536
+ continue;
2537
+ }
2538
+ }
2539
+ else if (payload !== undefined && payload !== null) {
2197
2540
  let v1Msg = {
2198
2541
  message_id: String(msg.message_id ?? ''),
2199
2542
  from: String(msg.from_aid ?? ''),
@@ -2205,209 +2548,224 @@ export class V2E2EECoordinator {
2205
2548
  payload,
2206
2549
  encrypted: false,
2207
2550
  };
2551
+ if (msg.sender_device_id !== undefined)
2552
+ v1Msg.sender_device_id = msg.sender_device_id;
2208
2553
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2209
2554
  attachGatewayProximity(v1Msg, msg);
2210
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2211
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2212
- }
2213
- else {
2214
- client._markPublishedSeq(ns, seq);
2215
- }
2555
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2216
2556
  decrypted.push(v1Msg);
2217
2557
  client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2218
2558
  continue;
2219
2559
  }
2560
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2561
+ const deferStatus = {};
2562
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2563
+ if (deferStatus.deferred) {
2564
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2565
+ if (deferStatus.fromAid) {
2566
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2567
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2568
+ }
2569
+ }
2570
+ if (plaintext) {
2571
+ plaintext.group_id = gid;
2572
+ plaintext.group_aid = eventGroupAid;
2573
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2574
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2575
+ }
2576
+ else {
2577
+ client._markPublishedSeq(ns, seq);
2578
+ }
2579
+ decrypted.push(plaintext);
2580
+ }
2581
+ }
2582
+ else {
2583
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2584
+ }
2585
+ continue;
2220
2586
  }
2221
- else if (payload !== undefined && payload !== null) {
2222
- let v1Msg = {
2223
- message_id: String(msg.message_id ?? ''),
2224
- from: String(msg.from_aid ?? ''),
2225
- group_id: gid,
2226
- group_aid: eventGroupAid,
2227
- seq: msg.seq,
2228
- type: String(msg.type ?? ''),
2229
- timestamp: msg.t_server,
2230
- payload,
2231
- encrypted: false,
2232
- };
2233
- v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2234
- attachGatewayProximity(v1Msg, msg);
2235
- await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2236
- decrypted.push(v1Msg);
2237
- client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
2587
+ if (version !== 'v2') {
2588
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2238
2589
  continue;
2239
2590
  }
2240
- if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
2241
- const deferStatus = {};
2242
- const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2243
- if (deferStatus.deferred) {
2244
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2245
- if (deferStatus.fromAid) {
2246
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2247
- deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2248
- }
2591
+ const deferStatus = {};
2592
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2593
+ if (deferStatus.deferred) {
2594
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2595
+ }
2596
+ if (deferStatus.deferred && deferStatus.fromAid) {
2597
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2598
+ deferredKeyFetches.set(key, {
2599
+ fromAid: deferStatus.fromAid,
2600
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2601
+ groupId: deferStatus.groupId ?? '',
2602
+ });
2603
+ }
2604
+ if (plaintext === null) {
2605
+ client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
2606
+ continue;
2607
+ }
2608
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2609
+ plaintext.group_id = gid;
2610
+ plaintext.group_aid = eventGroupAid;
2611
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2612
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2613
+ }
2614
+ else {
2615
+ client._markPublishedSeq(ns, seq);
2616
+ }
2617
+ decrypted.push(plaintext);
2618
+ client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2619
+ }
2620
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2621
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2622
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2623
+ const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2624
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2625
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2626
+ const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2627
+ let ackSeq = pageContigBefore;
2628
+ let contigAdvanced = false;
2629
+ if (ownsCursor) {
2630
+ try {
2631
+ this.ensureDeliveryCurrent(ns, generation);
2632
+ const window = client._seqTracker.getSyncState?.(ns);
2633
+ const deliveryTarget = window && commitTarget >= window.tail - 1
2634
+ ? Math.max(commitTarget, window.head) : commitTarget;
2635
+ if (deliveryTarget > pageContigBefore) {
2636
+ await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
2249
2637
  }
2250
- if (plaintext) {
2251
- plaintext.group_id = gid;
2252
- plaintext.group_aid = eventGroupAid;
2253
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2254
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2638
+ this.ensureDeliveryCurrent(ns, generation);
2639
+ pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
2640
+ if (commitTarget > pageContigBefore) {
2641
+ trackerMutated = true;
2642
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
2643
+ if (effectiveFloor > pageContigBefore) {
2644
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2255
2645
  }
2256
- else {
2257
- client._markPublishedSeq(ns, seq);
2646
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2647
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2258
2648
  }
2259
- decrypted.push(plaintext);
2649
+ if (seqs.length > 0) {
2650
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2651
+ }
2652
+ }
2653
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
2654
+ contigAdvanced = ackSeq !== pageContigBefore;
2655
+ if (contigAdvanced) {
2656
+ await client._commitSeqTrackerState(ns);
2657
+ this.ensureDeliveryCurrent(ns, generation);
2658
+ if (pageCount === 1 && deferredServerCursor > 0)
2659
+ this.clearForwardCursor(ns, ackSeq);
2260
2660
  }
2261
2661
  }
2262
- else {
2263
- client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2662
+ catch (exc) {
2663
+ if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
2664
+ && client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
2665
+ && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2666
+ (client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
2667
+ .call(client._seqTracker, ns, pageTrackerSnapshot);
2668
+ }
2669
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2670
+ throw exc;
2264
2671
  }
2265
- continue;
2266
2672
  }
2267
- if (version !== 'v2') {
2268
- client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2269
- continue;
2270
- }
2271
- const deferStatus = {};
2272
- let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2273
- if (deferStatus.deferred) {
2274
- blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2275
- }
2276
- if (deferStatus.deferred && deferStatus.fromAid) {
2277
- const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2278
- deferredKeyFetches.set(key, {
2279
- fromAid: deferStatus.fromAid,
2280
- senderDeviceId: deferStatus.senderDeviceId ?? '',
2281
- groupId: deferStatus.groupId ?? '',
2282
- });
2283
- }
2284
- if (plaintext === null) {
2285
- client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
2286
- continue;
2287
- }
2288
- plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2289
- plaintext.group_id = gid;
2290
- plaintext.group_aid = eventGroupAid;
2291
- if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2292
- await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2673
+ for (const fetch of deferredKeyFetches.values()) {
2674
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2293
2675
  }
2294
- else {
2295
- client._markPublishedSeq(ns, seq);
2676
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2677
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2296
2678
  }
2297
- decrypted.push(plaintext);
2298
- client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
2299
- }
2300
- const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2301
- const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2302
- const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2303
- const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
2304
- const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2305
- const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2306
- const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2307
- let ackSeq = pageContigBefore;
2308
- let contigAdvanced = false;
2309
- if (ownsCursor) {
2310
- try {
2311
- if (commitTarget > pageContigBefore) {
2312
- client._seqTracker.forceContiguousSeq(ns, commitTarget);
2313
- if (effectiveFloor > pageContigBefore) {
2314
- client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2315
- }
2316
- if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2317
- client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2318
- }
2319
- if (seqs.length > 0) {
2320
- client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2321
- }
2679
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2680
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2681
+ const knownServerCursorSeq = hasServerCursor
2682
+ ? cursorCurrentSeq
2683
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2684
+ const ackNeeded = ackSeq > 0
2685
+ && ackSeq > lastAutoAckSeq
2686
+ && ownsCursor
2687
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2688
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
2689
+ if (ackNeeded) {
2690
+ this.recordForwardAck(ns, ackSeq);
2691
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2692
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2693
+ if (canContinuePage && !pipelineRunning) {
2694
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2695
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2696
+ client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
2322
2697
  }
2323
- ackSeq = client._seqTracker.getContiguousSeq(ns);
2324
- contigAdvanced = ackSeq !== pageContigBefore;
2325
- if (contigAdvanced) {
2326
- await client._commitSeqTrackerState(ns);
2327
- if (pageCount === 1 && deferredServerCursor > 0)
2328
- this.clearForwardCursor(ns, ackSeq);
2698
+ else {
2699
+ client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
2700
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2701
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2329
2702
  }
2330
2703
  }
2331
- catch (exc) {
2332
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2333
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2704
+ client._delivery.syncProgress(ns, {
2705
+ rawCount: messages.length,
2706
+ pulledCount: decrypted.length - pageDecryptedBefore,
2707
+ remaining: result.remaining,
2708
+ hasMore: hasMoreField ? hasMore : undefined,
2709
+ currentSeq: client._seqTracker.getContiguousSeq(ns),
2710
+ targetSeq: serverHead,
2711
+ });
2712
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2713
+ if (!ownsCursor)
2714
+ break;
2715
+ const rawCount = messages.length;
2716
+ if (blockedSeq > 0)
2717
+ break;
2718
+ const shouldContinue = shouldContinueForwardPage({
2719
+ rawCount,
2720
+ nextAfterSeq,
2721
+ nextAfter,
2722
+ limit,
2723
+ hasMoreField,
2724
+ hasMore,
2725
+ serverHead,
2726
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
2727
+ pendingAckSeq,
2728
+ });
2729
+ const shouldContinueForPipeline = shouldContinueForwardPage({
2730
+ rawCount,
2731
+ nextAfterSeq,
2732
+ nextAfter,
2733
+ limit,
2734
+ hasMoreField,
2735
+ hasMore,
2736
+ serverHead,
2737
+ observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
2738
+ pendingAckSeq: 0,
2739
+ });
2740
+ if (!shouldContinue)
2741
+ break;
2742
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2743
+ if (pendingAckSeq > 0) {
2744
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2745
+ pendingAckSeq = 0;
2334
2746
  }
2335
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2336
- throw exc;
2747
+ startPipeline(nextAfter);
2337
2748
  }
2338
- if (contigAdvanced)
2339
- await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
2340
- }
2341
- for (const fetch of deferredKeyFetches.values()) {
2342
- this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2343
- }
2344
- if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2345
- this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2346
- }
2347
- // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2348
- // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2349
- const knownServerCursorSeq = hasServerCursor
2350
- ? cursorCurrentSeq
2351
- : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2352
- const ackNeeded = ackSeq > 0
2353
- && ackSeq > lastAutoAckSeq
2354
- && ownsCursor
2355
- && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2356
- || (contigAdvanced && ackSeq > knownServerCursorSeq));
2357
- if (ackNeeded) {
2358
- this.recordForwardAck(ns, ackSeq);
2359
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2360
- const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2361
- if (canContinuePage) {
2362
- pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2363
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2364
- client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
2749
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2750
+ const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2751
+ group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2752
+ _group_cursor_params: cursorParams,
2753
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2754
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2755
+ pullGateKey = nextKey;
2365
2756
  }
2366
- else {
2367
- client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
2368
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2369
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2757
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2758
+ if (tailDelayMs > 0)
2759
+ await v2Sleep(tailDelayMs);
2760
+ nextAfterSeq = nextAfter;
2761
+ if (pipelinePage) {
2762
+ pipelineResume.current?.();
2763
+ pipelineResume.current = null;
2370
2764
  }
2371
2765
  }
2372
- client._delivery.syncProgress(ns, {
2373
- rawCount: messages.length,
2374
- pulledCount: decrypted.length - pageDecryptedBefore,
2375
- remaining: result.remaining,
2376
- hasMore: hasMoreField ? hasMore : undefined,
2377
- currentSeq: client._seqTracker.getContiguousSeq(ns),
2378
- targetSeq: serverHead,
2379
- });
2380
- const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2381
- if (!ownsCursor)
2382
- break;
2383
- const rawCount = messages.length;
2384
- if (blockedSeq > 0)
2385
- break;
2386
- const shouldContinue = shouldContinueForwardPage({
2387
- rawCount,
2388
- nextAfterSeq,
2389
- nextAfter,
2390
- limit,
2391
- hasMoreField,
2392
- hasMore,
2393
- serverHead,
2394
- observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
2395
- pendingAckSeq,
2396
- });
2397
- if (!shouldContinue)
2398
- break;
2399
- if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2400
- const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2401
- group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2402
- _group_cursor_params: cursorParams,
2403
- }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2404
- await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2405
- pullGateKey = nextKey;
2406
- }
2407
- const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2408
- if (tailDelayMs > 0)
2409
- await v2Sleep(tailDelayMs);
2410
- nextAfterSeq = nextAfter;
2766
+ }
2767
+ finally {
2768
+ await stopPipeline();
2411
2769
  }
2412
2770
  if (pageCount >= maxPages) {
2413
2771
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
@@ -2945,10 +3303,16 @@ export class V2E2EECoordinator {
2945
3303
  encrypted: true,
2946
3304
  e2ee,
2947
3305
  };
3306
+ if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
3307
+ result.from_aid = fromAid;
2948
3308
  const senderDeviceId = String(aad.from_device ?? '').trim();
2949
3309
  if (senderDeviceId)
2950
3310
  result.sender_device_id = senderDeviceId;
2951
- result.direction = fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound';
3311
+ const direction = String(msg.direction ?? '').trim();
3312
+ if (direction)
3313
+ result.direction = direction;
3314
+ else if (fromAid && fromAid !== client._aid)
3315
+ result.direction = 'inbound';
2952
3316
  if (msg.t_server !== undefined)
2953
3317
  result.t_server = msg.t_server;
2954
3318
  if (msg.device_id !== undefined)
@@ -2990,10 +3354,10 @@ export class V2E2EECoordinator {
2990
3354
  }
2991
3355
  return await this.publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
2992
3356
  }
2993
- async decryptV2PushMessage(data) {
3357
+ async decryptV2PushMessage(data, deferStatus) {
2994
3358
  if (!isJsonObject(data))
2995
3359
  return null;
2996
- return await this.client._decryptV2Message(data, false, false, false, false, undefined, false, 'inline_push');
3360
+ return await this.client._decryptV2Message(data, false, true, false, false, deferStatus, false, 'inline_push');
2997
3361
  }
2998
3362
  handleGroupChangedSpk(data, groupId, action) {
2999
3363
  if (!this.client._v2Session || !groupId || !MEMBERSHIP_ACTIONS.has(action))