@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.
- package/CHANGELOG.md +41 -0
- package/_packed_docs/CHANGELOG.md +41 -0
- package/_packed_docs/INDEX.md +28 -3
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -5
- package/_packed_docs/agent.md/SCHEMA.md +37 -18
- 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
- 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
- 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
- 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
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +25 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +42 -4
- package/_packed_docs/sdk/09-storage-rpc-manual.md +25 -4
- package/_packed_docs/sdk/INDEX.md +3 -2
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +2 -0
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +1 -1
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +6 -3
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +45 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +231 -3
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +17 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +49 -2
- package/dist/aid.js.map +1 -1
- package/dist/bundle.js +1315 -628
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +5 -3
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +1 -0
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +51 -7
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +9 -1
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +771 -471
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -2
- package/dist/client.js.map +1 -1
- package/dist/facades.d.ts +2 -0
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +6 -0
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ownership-proof.d.ts +30 -0
- package/dist/ownership-proof.d.ts.map +1 -0
- package/dist/ownership-proof.js +97 -0
- package/dist/ownership-proof.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
package/dist/client/v2-e2ee.js
CHANGED
|
@@ -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
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
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(
|
|
1559
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1420
1560
|
else
|
|
1421
|
-
await client._publishAppEvent(
|
|
1422
|
-
decrypted.push(
|
|
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.
|
|
1616
|
+
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
1617
|
+
decrypted.push(plaintext);
|
|
1427
1618
|
}
|
|
1428
|
-
continue;
|
|
1429
1619
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1480
|
-
|
|
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
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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 (
|
|
1504
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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.
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2035
|
-
|
|
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
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2069
|
-
client.
|
|
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
|
-
|
|
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
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
-
|
|
2095
|
-
|
|
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
|
-
|
|
2103
|
-
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
2139
|
-
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
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
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
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
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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
|
-
|
|
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)
|