@agentunion/fastaun-browser 0.5.17 → 0.5.18

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 (60) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/_packed_docs/CHANGELOG.md +41 -0
  3. package/_packed_docs/INDEX.md +28 -3
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -5
  5. package/_packed_docs/agent.md/SCHEMA.md +37 -18
  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 +23 -17
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -2
  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 +95 -6
  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 +19 -4
  10. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +25 -2
  11. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +42 -4
  12. package/_packed_docs/sdk/09-storage-rpc-manual.md +25 -4
  13. package/_packed_docs/sdk/INDEX.md +3 -2
  14. package/dist/agent-md-schema.d.ts.map +1 -1
  15. package/dist/agent-md-schema.js +2 -0
  16. package/dist/agent-md-schema.js.map +1 -1
  17. package/dist/agent-md.d.ts +1 -1
  18. package/dist/agent-md.d.ts.map +1 -1
  19. package/dist/agent-md.js +6 -3
  20. package/dist/agent-md.js.map +1 -1
  21. package/dist/aid-store.d.ts +45 -0
  22. package/dist/aid-store.d.ts.map +1 -1
  23. package/dist/aid-store.js +231 -3
  24. package/dist/aid-store.js.map +1 -1
  25. package/dist/aid.d.ts +17 -1
  26. package/dist/aid.d.ts.map +1 -1
  27. package/dist/aid.js +49 -2
  28. package/dist/aid.js.map +1 -1
  29. package/dist/bundle.js +1315 -628
  30. package/dist/cert-utils.d.ts.map +1 -1
  31. package/dist/cert-utils.js +5 -3
  32. package/dist/cert-utils.js.map +1 -1
  33. package/dist/client/delivery.d.ts +1 -0
  34. package/dist/client/delivery.d.ts.map +1 -1
  35. package/dist/client/delivery.js +51 -7
  36. package/dist/client/delivery.js.map +1 -1
  37. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  38. package/dist/client/rpc-pipeline.js +9 -1
  39. package/dist/client/rpc-pipeline.js.map +1 -1
  40. package/dist/client/v2-e2ee.d.ts.map +1 -1
  41. package/dist/client/v2-e2ee.js +771 -471
  42. package/dist/client/v2-e2ee.js.map +1 -1
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +13 -2
  45. package/dist/client.js.map +1 -1
  46. package/dist/facades.d.ts +2 -0
  47. package/dist/facades.d.ts.map +1 -1
  48. package/dist/facades.js +6 -0
  49. package/dist/facades.js.map +1 -1
  50. package/dist/index.d.ts +3 -2
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/ownership-proof.d.ts +30 -0
  55. package/dist/ownership-proof.d.ts.map +1 -0
  56. package/dist/ownership-proof.js +97 -0
  57. package/dist/ownership-proof.js.map +1 -0
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. 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;
@@ -1080,6 +1126,11 @@ export class V2E2EECoordinator {
1080
1126
  to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
1081
1127
  timestamp: msg.t_server, payload: payload, encrypted: false,
1082
1128
  };
1129
+ if (msg.sender_device_id !== undefined)
1130
+ plaintext.sender_device_id = msg.sender_device_id;
1131
+ const direction = String(msg.direction ?? '').trim();
1132
+ if (direction)
1133
+ plaintext.direction = direction;
1083
1134
  attachGatewayProximity(plaintext, msg);
1084
1135
  }
1085
1136
  else if (isJsonObject(payload)
@@ -1099,6 +1150,9 @@ export class V2E2EECoordinator {
1099
1150
  client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
1100
1151
  return null;
1101
1152
  }
1153
+ if (publish && client._delivery.isSelfSentP2PMessage?.(plaintext)) {
1154
+ return plaintext;
1155
+ }
1102
1156
  if (publish) {
1103
1157
  const event = client._delivery.p2pAppEventForMessage(plaintext);
1104
1158
  if (orderedPublish) {
@@ -1155,6 +1209,8 @@ export class V2E2EECoordinator {
1155
1209
  group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
1156
1210
  timestamp: msg.t_server, payload: payload, encrypted: false,
1157
1211
  };
1212
+ if (msg.sender_device_id !== undefined)
1213
+ plaintext.sender_device_id = msg.sender_device_id;
1158
1214
  plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
1159
1215
  attachGatewayProximity(plaintext, msg);
1160
1216
  }
@@ -1304,264 +1360,389 @@ export class V2E2EECoordinator {
1304
1360
  let lastAutoAckSeq = 0;
1305
1361
  let pendingAckSeq = deferredForwardAck;
1306
1362
  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);
1363
+ let pipelineRunning = false;
1364
+ let pipelineError = null;
1365
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
1366
+ let pipelineProducer = null;
1367
+ let pipelineStopped = false;
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 enqueued = await pipelineQueue.put({
1389
+ result,
1390
+ afterSeq: producerAfterSeq,
1391
+ ackUpToSeq: 0,
1392
+ inlineAckGeneration: null,
1393
+ requestedVolatileCursor,
1394
+ weight: rawPageMessages.length,
1395
+ }, rawPageMessages.length);
1396
+ if (!enqueued || pipelineStopped)
1397
+ break;
1398
+ const volatileMessages = client._delivery.volatileMessages(rawPageMessages);
1399
+ if (volatileMessages.length > 0)
1400
+ break;
1401
+ const messages = deduplicateForwardPageMessages(rawPageMessages, producerAfterSeq);
1402
+ const seqs = messages
1403
+ .map((msg) => Number(msg.seq ?? 0))
1404
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1405
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
1406
+ const cursor = isJsonObject(result.cursor)
1407
+ ? result.cursor : null;
1408
+ const shouldContinue = shouldContinueForwardPage({
1409
+ rawCount: messages.length,
1410
+ nextAfterSeq: producerAfterSeq,
1411
+ nextAfter,
1412
+ limit,
1413
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
1414
+ hasMore: result.has_more === true,
1415
+ serverHead: safeNonNegativeSeq(cursor?.latest_seq) ?? 0,
1416
+ observedUpper: Math.max(nextAfter, Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
1417
+ pendingAckSeq: 0,
1418
+ });
1419
+ if (!shouldContinue)
1420
+ break;
1421
+ producerAfterSeq = nextAfter;
1422
+ producerVolatileCursor = '';
1423
+ }
1424
+ })().catch((error) => {
1425
+ pipelineError = error;
1426
+ }).finally(() => {
1427
+ pipelineQueue.close();
1428
+ });
1429
+ };
1430
+ const stopPipeline = async () => {
1431
+ pipelineStopped = true;
1432
+ pipelineQueue.close();
1433
+ if (pipelineProducer) {
1434
+ const producer = pipelineProducer;
1435
+ pipelineProducer = null;
1436
+ await producer;
1437
+ }
1438
+ pipelineRunning = false;
1439
+ };
1440
+ try {
1441
+ while (pageCount < maxPages) {
1442
+ let pipelinePage;
1443
+ if (pipelineRunning) {
1444
+ pipelinePage = await pipelineQueue.take();
1445
+ if (!pipelinePage) {
1446
+ pipelineRunning = false;
1447
+ if (pipelineError)
1448
+ throw pipelineError;
1449
+ break;
1338
1450
  }
1339
- pendingAckSeq = 0;
1340
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1341
- this.clearForwardAck(ns, ackUpToSeq);
1451
+ }
1452
+ pageCount += 1;
1453
+ const volatileCursorBeforePage = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
1454
+ const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
1455
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
1456
+ ? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null
1457
+ : null);
1458
+ let result;
1459
+ if (pipelinePage) {
1460
+ nextAfterSeq = pipelinePage.afterSeq;
1461
+ result = pipelinePage.result;
1342
1462
  }
1343
1463
  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);
1464
+ try {
1465
+ result = await client._callRawV2Rpc('message.v2.pull', {
1466
+ after_seq: nextAfterSeq,
1467
+ limit,
1468
+ ...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
1469
+ ...(opts?.force ? { force: true } : {}),
1470
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
1471
+ }, pullGateKey);
1472
+ }
1473
+ catch (exc) {
1474
+ const text = exc instanceof Error ? exc.message : String(exc);
1475
+ if (!volatileCursor || !text.toLowerCase().includes('volatile cursor epoch mismatch'))
1476
+ throw exc;
1477
+ client._delivery.resetVolatilePullCursor();
1478
+ volatileCursor = '';
1479
+ pageCount -= 1;
1480
+ continue;
1481
+ }
1348
1482
  }
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);
1483
+ if (ackUpToSeq > 0) {
1484
+ const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
1485
+ if (actualAckSeq >= ackUpToSeq) {
1486
+ if (pendingInlineGeneration !== null) {
1487
+ client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
1488
+ }
1489
+ pendingAckSeq = 0;
1490
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1491
+ this.clearForwardAck(ns, ackUpToSeq);
1405
1492
  }
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 ?? '' });
1493
+ else {
1494
+ client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
1495
+ await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
1496
+ pendingAckSeq = 0;
1497
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1498
+ }
1499
+ }
1500
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
1501
+ const volatileMessages = client._delivery.volatileMessages(pageMessages);
1502
+ if (volatileMessages.length > 0) {
1503
+ decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
1504
+ volatileCursor = client._delivery.volatilePullCursor();
1505
+ }
1506
+ const volatileAdvanced = Boolean(volatileCursor) && volatileCursor !== volatileCursorBeforePage;
1507
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
1508
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
1509
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
1510
+ const hasMore = result.has_more === true;
1511
+ const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
1512
+ 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 ?? '')}`);
1513
+ const seqs = messages
1514
+ .map((msg) => Number(msg.seq ?? 0))
1515
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
1516
+ const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
1517
+ const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
1518
+ ? client._seqTracker.snapshotNamespace(ns)
1519
+ : null;
1520
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
1521
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
1522
+ const deferredKeyFetches = new Map();
1523
+ let blockedSeq = 0;
1524
+ const pageDecryptedBefore = decrypted.length;
1525
+ for (const msg of messages) {
1526
+ const seq = Number(msg.seq ?? 0);
1527
+ if (!Number.isFinite(seq) || seq <= 0)
1528
+ continue;
1529
+ const version = String(msg.version ?? 'v2');
1530
+ if (version === 'v1') {
1531
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
1532
+ const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
1533
+ const payloadType = isJsonObject(legacyPayload)
1534
+ ? String(legacyPayload.type ?? '').trim()
1535
+ : '';
1536
+ if (legacyPayload !== undefined && legacyPayload !== null
1537
+ && !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
1538
+ const v1Msg = {
1539
+ message_id: String(msg.message_id ?? ''),
1540
+ from: String(msg.from_aid ?? ''),
1541
+ to: String(legacy.to ?? client._aid ?? ''),
1542
+ seq: msg.seq,
1543
+ type: String(msg.type ?? ''),
1544
+ timestamp: msg.t_server,
1545
+ payload: legacyPayload,
1546
+ encrypted: false,
1547
+ };
1548
+ const direction = String(msg.direction ?? '').trim();
1549
+ if (direction)
1550
+ v1Msg.direction = direction;
1551
+ else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
1552
+ v1Msg.direction = 'inbound';
1415
1553
  }
1416
- }
1417
- if (plaintext) {
1554
+ if (msg.sender_device_id !== undefined)
1555
+ v1Msg.sender_device_id = msg.sender_device_id;
1556
+ attachGatewayProximity(v1Msg, msg);
1557
+ const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
1418
1558
  if (ns)
1419
- await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1559
+ await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
1420
1560
  else
1421
- await client._publishAppEvent('message.received', plaintext, 'pull');
1422
- decrypted.push(plaintext);
1561
+ await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
1562
+ decrypted.push(v1Msg);
1563
+ }
1564
+ else if (isJsonObject(legacyPayload)
1565
+ && ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
1566
+ const deferStatus = {};
1567
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
1568
+ if (deferStatus.deferred) {
1569
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1570
+ if (deferStatus.fromAid) {
1571
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1572
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
1573
+ }
1574
+ }
1575
+ if (plaintext) {
1576
+ if (ns)
1577
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1578
+ else
1579
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1580
+ decrypted.push(plaintext);
1581
+ }
1423
1582
  }
1583
+ else {
1584
+ client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1585
+ }
1586
+ continue;
1587
+ }
1588
+ if (version !== 'v2') {
1589
+ client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
1590
+ continue;
1591
+ }
1592
+ const msgSpkId = String(msg.spk_id ?? '');
1593
+ if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
1594
+ client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
1595
+ }
1596
+ const deferStatus = {};
1597
+ const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
1598
+ if (deferStatus.deferred) {
1599
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
1600
+ }
1601
+ if (deferStatus.deferred && deferStatus.fromAid) {
1602
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
1603
+ deferredKeyFetches.set(key, {
1604
+ fromAid: deferStatus.fromAid,
1605
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
1606
+ groupId: deferStatus.groupId ?? '',
1607
+ });
1608
+ }
1609
+ if (plaintext === null)
1610
+ continue;
1611
+ if (ns) {
1612
+ await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
1613
+ decrypted.push(plaintext);
1424
1614
  }
1425
1615
  else {
1426
- client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
1616
+ await client._publishAppEvent('message.received', plaintext, 'pull');
1617
+ decrypted.push(plaintext);
1427
1618
  }
1428
- continue;
1429
1619
  }
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
- }
1451
- if (plaintext === null)
1452
- continue;
1620
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
1621
+ const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
1622
+ const serverAckSeq = parsedServerAckSeq ?? 0;
1623
+ const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
1453
1624
  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}`);
1625
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
1626
+ let ackSeq = pageContigBefore;
1627
+ let contigAdvanced = false;
1628
+ try {
1629
+ if (commitTarget > pageContigBefore) {
1630
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
1631
+ if (retentionFloor > pageContigBefore) {
1632
+ client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
1633
+ }
1634
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
1635
+ client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
1636
+ }
1637
+ if (seqs.length > 0) {
1638
+ client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1639
+ }
1475
1640
  }
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}`);
1641
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
1642
+ contigAdvanced = ackSeq !== pageContigBefore;
1643
+ if (contigAdvanced) {
1644
+ await client._commitSeqTrackerState(ns);
1645
+ if (pageCount === 1 && deferredServerCursor > 0)
1646
+ this.clearForwardCursor(ns, ackSeq);
1478
1647
  }
1479
- if (seqs.length > 0) {
1480
- client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
1648
+ }
1649
+ catch (exc) {
1650
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1651
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1481
1652
  }
1653
+ client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1654
+ throw exc;
1482
1655
  }
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);
1656
+ if (contigAdvanced)
1657
+ await client._drainOrderedMessages(ns, undefined, false, false);
1658
+ for (const fetch of deferredKeyFetches.values()) {
1659
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
1489
1660
  }
1490
- }
1491
- catch (exc) {
1492
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1493
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
1661
+ if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1662
+ this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1663
+ }
1664
+ // 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
1665
+ // 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
1666
+ const knownServerAckSeq = hasServerAckSeq
1667
+ ? serverAckSeq
1668
+ : (pageCount === 1 ? deferredServerCursor : 0);
1669
+ const ackNeeded = ackSeq > 0
1670
+ && ackSeq > lastAutoAckSeq
1671
+ && ((hasServerAckSeq && ackSeq > serverAckSeq)
1672
+ || (contigAdvanced && ackSeq > knownServerAckSeq));
1673
+ if (ackNeeded) {
1674
+ this.recordForwardAck(ns, ackSeq);
1675
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1676
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
1677
+ if (canContinuePage && !pipelineRunning) {
1678
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
1679
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1680
+ }
1681
+ else {
1682
+ await this.confirmForwardP2PAck(ackSeq);
1683
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
1684
+ }
1494
1685
  }
1495
- client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
1496
- throw exc;
1497
- }
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);
1502
1686
  }
1503
- if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
1504
- this.recordForwardCursor(ns, serverAckSeq, ackSeq);
1687
+ if (ns)
1688
+ 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);
1689
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
1690
+ const rawCount = messages.length;
1691
+ if (blockedSeq > 0)
1692
+ break;
1693
+ let shouldContinue = shouldContinueForwardPage({
1694
+ rawCount,
1695
+ nextAfterSeq,
1696
+ nextAfter,
1697
+ limit,
1698
+ hasMoreField,
1699
+ hasMore,
1700
+ serverHead,
1701
+ pendingAckSeq,
1702
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1703
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1704
+ });
1705
+ const volatileHasMoreField = Object.prototype.hasOwnProperty.call(result, 'volatile_has_more');
1706
+ const volatileHasMore = result.volatile_has_more === true;
1707
+ if (!shouldContinue && volatileAdvanced && (volatileHasMore || hasMore || (!volatileHasMoreField && limit > 0 && pageMessages.length >= limit))) {
1708
+ shouldContinue = true;
1505
1709
  }
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);
1710
+ const shouldContinueForPipeline = shouldContinueForwardPage({
1711
+ rawCount,
1712
+ nextAfterSeq,
1713
+ nextAfter,
1714
+ limit,
1715
+ hasMoreField,
1716
+ hasMore,
1717
+ serverHead,
1718
+ pendingAckSeq: 0,
1719
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
1720
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
1721
+ });
1722
+ if (!shouldContinue)
1723
+ break;
1724
+ if (shouldContinueForPipeline && !volatileAdvanced && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
1725
+ if (pendingAckSeq > 0) {
1726
+ await this.confirmForwardP2PAck(pendingAckSeq);
1727
+ pendingAckSeq = 0;
1526
1728
  }
1729
+ startPipeline(nextAfter, volatileCursor);
1730
+ }
1731
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
1732
+ const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
1733
+ after_seq: nextAfter, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
1734
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
1735
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
1736
+ pullGateKey = nextKey;
1527
1737
  }
1738
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
1739
+ if (tailDelayMs > 0)
1740
+ await v2Sleep(tailDelayMs);
1741
+ nextAfterSeq = nextAfter;
1528
1742
  }
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;
1743
+ }
1744
+ finally {
1745
+ await stopPipeline();
1565
1746
  }
1566
1747
  if (pageCount >= maxPages) {
1567
1748
  client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
@@ -1931,83 +2112,197 @@ export class V2E2EECoordinator {
1931
2112
  let lastAutoAckSeq = 0;
1932
2113
  let pendingAckSeq = deferredForwardAck;
1933
2114
  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);
2115
+ let pipelineRunning = false;
2116
+ let pipelineError = null;
2117
+ const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
2118
+ let pipelineProducer = null;
2119
+ let pipelineStopped = false;
2120
+ const startPipeline = (startAfterSeq) => {
2121
+ if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
2122
+ return;
2123
+ pipelineRunning = true;
2124
+ pipelineProducer = (async () => {
2125
+ let producerAfterSeq = startAfterSeq;
2126
+ for (let produced = pageCount; produced < maxPages; produced += 1) {
2127
+ if (pipelineStopped)
2128
+ break;
2129
+ const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2130
+ group_id: wireGroupId,
2131
+ after_seq: producerAfterSeq,
2132
+ limit,
2133
+ ...cursorParams,
2134
+ ...(opts?.force ? { force: true } : {}),
2135
+ }, groupAid), pullGateKey);
2136
+ if (pipelineStopped)
2137
+ break;
2138
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2139
+ const enqueued = await pipelineQueue.put({
2140
+ result,
2141
+ afterSeq: producerAfterSeq,
2142
+ ackUpToSeq: 0,
2143
+ inlineAckGeneration: null,
2144
+ requestedVolatileCursor: '',
2145
+ weight: pageMessages.length,
2146
+ }, pageMessages.length);
2147
+ if (!enqueued || pipelineStopped)
2148
+ break;
2149
+ const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
2150
+ const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
2151
+ const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
2152
+ const cursor = isJsonObject(result.cursor)
2153
+ ? result.cursor : null;
2154
+ const shouldContinue = shouldContinueForwardPage({
2155
+ rawCount: messages.length,
2156
+ nextAfterSeq: producerAfterSeq,
2157
+ nextAfter,
2158
+ limit,
2159
+ hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
2160
+ hasMore: result.has_more === true,
2161
+ serverHead: Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0),
2162
+ observedUpper: Math.max(nextAfter, typeof client._seqTracker.getMaxSeenSeq === 'function' ? client._seqTracker.getMaxSeenSeq(ns) : 0),
2163
+ pendingAckSeq: 0,
2164
+ });
2165
+ if (!shouldContinue)
2166
+ break;
2167
+ producerAfterSeq = nextAfter;
2168
+ }
2169
+ })().catch((error) => {
2170
+ pipelineError = error;
2171
+ }).finally(() => {
2172
+ pipelineQueue.close();
2173
+ });
2174
+ };
2175
+ const stopPipeline = async () => {
2176
+ pipelineStopped = true;
2177
+ pipelineQueue.close();
2178
+ if (pipelineProducer) {
2179
+ const producer = pipelineProducer;
2180
+ pipelineProducer = null;
2181
+ await producer;
2182
+ }
2183
+ pipelineRunning = false;
2184
+ };
2185
+ try {
2186
+ while (pageCount < maxPages) {
2187
+ let pipelinePage;
2188
+ if (pipelineRunning) {
2189
+ pipelinePage = await pipelineQueue.take();
2190
+ if (!pipelinePage) {
2191
+ pipelineRunning = false;
2192
+ if (pipelineError)
2193
+ throw pipelineError;
2194
+ break;
1953
2195
  }
1954
- pendingAckSeq = 0;
1955
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
1956
- this.clearForwardAck(ns, ackUpToSeq);
2196
+ }
2197
+ pageCount += 1;
2198
+ const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
2199
+ const pendingInlineGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
2200
+ ? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null
2201
+ : null);
2202
+ let result;
2203
+ if (pipelinePage) {
2204
+ nextAfterSeq = pipelinePage.afterSeq;
2205
+ result = pipelinePage.result;
1957
2206
  }
1958
2207
  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);
2208
+ result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
2209
+ group_id: wireGroupId,
2210
+ after_seq: nextAfterSeq,
2211
+ limit,
2212
+ ...cursorParams,
2213
+ ...(opts?.force ? { force: true } : {}),
2214
+ ...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
2215
+ }, groupAid), pullGateKey);
1963
2216
  }
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;
2217
+ if (ackUpToSeq > 0) {
2218
+ const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
2219
+ if (actualAckSeq >= ackUpToSeq) {
2220
+ if (pendingInlineGeneration !== null) {
2221
+ client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
2222
+ }
2223
+ pendingAckSeq = 0;
2224
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2225
+ this.clearForwardAck(ns, ackUpToSeq);
2007
2226
  }
2008
- if (payloadObj) {
2009
- const payloadType = String(payloadObj.type ?? '').trim();
2010
- if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2227
+ else {
2228
+ client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
2229
+ await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
2230
+ pendingAckSeq = 0;
2231
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
2232
+ }
2233
+ }
2234
+ const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
2235
+ if (resultGroupAid)
2236
+ groupAid = resultGroupAid;
2237
+ const eventGroupAid = resultGroupAid || groupAid || gid;
2238
+ const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
2239
+ const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
2240
+ const cursor = isJsonObject(result.cursor) ? result.cursor : null;
2241
+ const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
2242
+ const hasMore = result.has_more === true;
2243
+ const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
2244
+ const seqs = messages
2245
+ .map((msg) => Number(msg.seq ?? 0))
2246
+ .filter((seq) => Number.isFinite(seq) && seq > 0);
2247
+ const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
2248
+ const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
2249
+ ? client._seqTracker.snapshotNamespace(ns)
2250
+ : null;
2251
+ const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
2252
+ const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
2253
+ const deferredKeyFetches = new Map();
2254
+ let blockedSeq = 0;
2255
+ const pageDecryptedBefore = decrypted.length;
2256
+ for (const msg of messages) {
2257
+ const seq = Number(msg.seq ?? 0);
2258
+ if (!Number.isFinite(seq) || seq <= 0)
2259
+ continue;
2260
+ const version = String(msg.version ?? 'v2');
2261
+ if (version === 'v1') {
2262
+ const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
2263
+ const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
2264
+ const payloadObj = isJsonObject(payload) ? payload : null;
2265
+ // 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
2266
+ if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
2267
+ && client._delivery.recallEventFromGroupMessage(msg)) {
2268
+ await client._delivery.publishGroupRecallTombstone(gid, seq, {
2269
+ ...msg,
2270
+ group_id: gid,
2271
+ group_aid: eventGroupAid,
2272
+ }, 'pull');
2273
+ client._markPublishedSeq(ns, seq);
2274
+ client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
2275
+ continue;
2276
+ }
2277
+ if (payloadObj) {
2278
+ const payloadType = String(payloadObj.type ?? '').trim();
2279
+ if (payloadType !== 'e2ee.encrypted' && payloadType !== 'e2ee.group_encrypted' && payloadType !== 'e2ee.p2p_encrypted') {
2280
+ let v1Msg = {
2281
+ message_id: String(msg.message_id ?? ''),
2282
+ from: String(msg.from_aid ?? ''),
2283
+ group_id: gid,
2284
+ group_aid: eventGroupAid,
2285
+ seq: msg.seq,
2286
+ type: String(msg.type ?? ''),
2287
+ timestamp: msg.t_server,
2288
+ payload,
2289
+ encrypted: false,
2290
+ };
2291
+ if (msg.sender_device_id !== undefined)
2292
+ v1Msg.sender_device_id = msg.sender_device_id;
2293
+ v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2294
+ attachGatewayProximity(v1Msg, msg);
2295
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2296
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2297
+ }
2298
+ else {
2299
+ client._markPublishedSeq(ns, seq);
2300
+ }
2301
+ decrypted.push(v1Msg);
2302
+ continue;
2303
+ }
2304
+ }
2305
+ else if (payload !== undefined && payload !== null) {
2011
2306
  let v1Msg = {
2012
2307
  message_id: String(msg.message_id ?? ''),
2013
2308
  from: String(msg.from_aid ?? ''),
@@ -2019,194 +2314,195 @@ export class V2E2EECoordinator {
2019
2314
  payload,
2020
2315
  encrypted: false,
2021
2316
  };
2317
+ if (msg.sender_device_id !== undefined)
2318
+ v1Msg.sender_device_id = msg.sender_device_id;
2022
2319
  v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
2023
2320
  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
- }
2321
+ await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
2030
2322
  decrypted.push(v1Msg);
2031
2323
  continue;
2032
2324
  }
2325
+ if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(String(payloadObj.type ?? '').trim())) {
2326
+ const deferStatus = {};
2327
+ const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
2328
+ if (deferStatus.deferred) {
2329
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2330
+ if (deferStatus.fromAid) {
2331
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2332
+ deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
2333
+ }
2334
+ }
2335
+ if (plaintext) {
2336
+ plaintext.group_id = gid;
2337
+ plaintext.group_aid = eventGroupAid;
2338
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2339
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2340
+ }
2341
+ else {
2342
+ client._markPublishedSeq(ns, seq);
2343
+ }
2344
+ decrypted.push(plaintext);
2345
+ }
2346
+ }
2347
+ client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
2348
+ continue;
2033
2349
  }
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);
2350
+ if (version !== 'v2') {
2351
+ client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
2050
2352
  continue;
2051
2353
  }
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 ?? '' });
2354
+ const deferStatus = {};
2355
+ let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
2356
+ if (deferStatus.deferred) {
2357
+ blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
2358
+ }
2359
+ if (deferStatus.deferred && deferStatus.fromAid) {
2360
+ const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
2361
+ deferredKeyFetches.set(key, {
2362
+ fromAid: deferStatus.fromAid,
2363
+ senderDeviceId: deferStatus.senderDeviceId ?? '',
2364
+ groupId: deferStatus.groupId ?? '',
2365
+ });
2366
+ }
2367
+ if (plaintext === null)
2368
+ continue;
2369
+ plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
2370
+ plaintext.group_id = gid;
2371
+ plaintext.group_aid = eventGroupAid;
2372
+ if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
2373
+ await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
2374
+ }
2375
+ else {
2376
+ client._markPublishedSeq(ns, seq);
2377
+ }
2378
+ decrypted.push(plaintext);
2379
+ }
2380
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
2381
+ const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
2382
+ const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
2383
+ const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
2384
+ const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
2385
+ const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
2386
+ const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, cursorCurrentSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
2387
+ let ackSeq = pageContigBefore;
2388
+ let contigAdvanced = false;
2389
+ if (ownsCursor) {
2390
+ try {
2391
+ if (commitTarget > pageContigBefore) {
2392
+ client._seqTracker.forceContiguousSeq(ns, commitTarget);
2393
+ if (effectiveFloor > pageContigBefore) {
2394
+ client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
2060
2395
  }
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);
2396
+ if (deferredServerCursor > pageContigBefore && pageCount === 1) {
2397
+ client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
2067
2398
  }
2068
- else {
2069
- client._markPublishedSeq(ns, seq);
2399
+ if (seqs.length > 0) {
2400
+ client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
2070
2401
  }
2071
- decrypted.push(plaintext);
2402
+ }
2403
+ ackSeq = client._seqTracker.getContiguousSeq(ns);
2404
+ contigAdvanced = ackSeq !== pageContigBefore;
2405
+ if (contigAdvanced) {
2406
+ await client._commitSeqTrackerState(ns);
2407
+ if (pageCount === 1 && deferredServerCursor > 0)
2408
+ this.clearForwardCursor(ns, ackSeq);
2072
2409
  }
2073
2410
  }
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
- });
2411
+ catch (exc) {
2412
+ if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2413
+ client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2414
+ }
2415
+ client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2416
+ throw exc;
2417
+ }
2418
+ if (contigAdvanced)
2419
+ await client._drainOrderedMessages(ns, undefined, false, false);
2093
2420
  }
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);
2421
+ for (const fetch of deferredKeyFetches.values()) {
2422
+ this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
2101
2423
  }
2102
- else {
2103
- client._markPublishedSeq(ns, seq);
2424
+ 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);
2425
+ if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
2426
+ this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
2104
2427
  }
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);
2428
+ // 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
2429
+ // 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
2430
+ const knownServerCursorSeq = hasServerCursor
2431
+ ? cursorCurrentSeq
2432
+ : (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
2433
+ const ackNeeded = ackSeq > 0
2434
+ && ackSeq > lastAutoAckSeq
2435
+ && ownsCursor
2436
+ && ((hasServerCursor && ackSeq > cursorCurrentSeq)
2437
+ || (contigAdvanced && ackSeq > knownServerCursorSeq));
2438
+ if (ackNeeded) {
2439
+ this.recordForwardAck(ns, ackSeq);
2440
+ const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
2441
+ const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
2442
+ if (canContinuePage && !pipelineRunning) {
2443
+ pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
2444
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2136
2445
  }
2137
- }
2138
- catch (exc) {
2139
- if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
2140
- client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
2446
+ else {
2447
+ await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2448
+ lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2141
2449
  }
2142
- client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
2143
- throw exc;
2144
2450
  }
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
2451
  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);
2452
+ if (!ownsCursor)
2453
+ break;
2454
+ const rawCount = messages.length;
2455
+ if (blockedSeq > 0)
2456
+ break;
2457
+ const shouldContinue = shouldContinueForwardPage({
2458
+ rawCount,
2459
+ nextAfterSeq,
2460
+ nextAfter,
2461
+ limit,
2462
+ hasMoreField,
2463
+ hasMore,
2464
+ serverHead,
2465
+ pendingAckSeq,
2466
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2467
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2468
+ });
2469
+ const shouldContinueForPipeline = shouldContinueForwardPage({
2470
+ rawCount,
2471
+ nextAfterSeq,
2472
+ nextAfter,
2473
+ limit,
2474
+ hasMoreField,
2475
+ hasMore,
2476
+ serverHead,
2477
+ pendingAckSeq: 0,
2478
+ observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
2479
+ ? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
2480
+ });
2481
+ if (!shouldContinue)
2482
+ break;
2483
+ if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
2484
+ if (pendingAckSeq > 0) {
2485
+ await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
2486
+ pendingAckSeq = 0;
2487
+ }
2488
+ startPipeline(nextAfter);
2172
2489
  }
2173
- else {
2174
- await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
2175
- lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
2490
+ if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
2491
+ const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
2492
+ group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
2493
+ _group_cursor_params: cursorParams,
2494
+ }, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
2495
+ await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
2496
+ pullGateKey = nextKey;
2176
2497
  }
2498
+ const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
2499
+ if (tailDelayMs > 0)
2500
+ await v2Sleep(tailDelayMs);
2501
+ nextAfterSeq = nextAfter;
2177
2502
  }
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;
2503
+ }
2504
+ finally {
2505
+ await stopPipeline();
2210
2506
  }
2211
2507
  if (pageCount >= maxPages) {
2212
2508
  client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
@@ -2729,7 +3025,11 @@ export class V2E2EECoordinator {
2729
3025
  const senderDeviceId = String(aad.from_device ?? '').trim();
2730
3026
  if (senderDeviceId)
2731
3027
  result.sender_device_id = senderDeviceId;
2732
- result.direction = fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound';
3028
+ const direction = String(msg.direction ?? '').trim();
3029
+ if (direction)
3030
+ result.direction = direction;
3031
+ else if (fromAid && fromAid !== client._aid)
3032
+ result.direction = 'inbound';
2733
3033
  if (msg.t_server !== undefined)
2734
3034
  result.t_server = msg.t_server;
2735
3035
  if (msg.device_id !== undefined)