@agentunion/fastaun-browser 0.5.16 → 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 +334 -258
- package/_packed_docs/CHANGELOG.md +334 -258
- package/_packed_docs/INDEX.md +36 -5
- package/_packed_docs/KITE_DOCS_GUIDE.md +13 -5
- package/_packed_docs/agent.md/SCHEMA.md +52 -33
- 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 +1170 -1164
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1215 -1213
- 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/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -43
- 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/10-Group-/345/255/220/345/215/217/350/256/256.md +80 -17
- 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 +69 -10
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +7 -4
- package/_packed_docs/sdk/09-group-rpc-manual.md +167 -35
- package/_packed_docs/sdk/09-message-rpc-manual.md +72 -42
- package/_packed_docs/sdk/09-storage-rpc-manual.md +48 -18
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +16 -12
- package/_packed_docs/sdk/INDEX.md +14 -13
- 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 +3 -1
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +13 -4
- 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 +2763 -1591
- 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 +19 -0
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +198 -12
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts +4 -0
- package/dist/client/group-state.d.ts.map +1 -1
- package/dist/client/group-state.js +63 -0
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +46 -5
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +7 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +918 -463
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +73 -14
- package/dist/client.js.map +1 -1
- package/dist/facades.d.ts +7 -0
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +21 -0
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.d.ts.map +1 -1
- package/dist/group-fs.js +8 -5
- package/dist/group-fs.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/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.d.ts.map +1 -1
- package/dist/storage/vfs.js +68 -5
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +34 -3
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.js +34 -34
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -13
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;
|
|
@@ -948,6 +994,12 @@ export class V2E2EECoordinator {
|
|
|
948
994
|
await client._publishAppEvent(eventName, safeEvent, 'pending_retry');
|
|
949
995
|
continue;
|
|
950
996
|
}
|
|
997
|
+
if (!entry.groupId && String(entry.msg.storage ?? '') === 'volatile') {
|
|
998
|
+
await this.publishPendingV2VolatileMessage(entry.msg, plaintext, 'pending_retry');
|
|
999
|
+
client._v2SenderIKPending.delete(key);
|
|
1000
|
+
client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
951
1003
|
client._v2SenderIKPending.delete(key);
|
|
952
1004
|
const seq = Number(entry.msg.seq ?? 0);
|
|
953
1005
|
if (entry.groupId) {
|
|
@@ -967,10 +1019,35 @@ export class V2E2EECoordinator {
|
|
|
967
1019
|
client._v2SenderIKFetching.delete(fetchKey);
|
|
968
1020
|
}
|
|
969
1021
|
}
|
|
1022
|
+
async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
|
|
1023
|
+
const delivery = this.client._delivery;
|
|
1024
|
+
const claim = delivery.beginVolatileDelivery(rawMessage);
|
|
1025
|
+
if (claim === 'inflight')
|
|
1026
|
+
return;
|
|
1027
|
+
if (claim === 'seen') {
|
|
1028
|
+
await delivery.ackVolatileMessages([rawMessage], true, false);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
const publicMessage = delivery.publicVolatileMessage(plaintext);
|
|
1033
|
+
for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
|
|
1034
|
+
if (key in rawMessage)
|
|
1035
|
+
publicMessage[key] = rawMessage[key];
|
|
1036
|
+
}
|
|
1037
|
+
const appEvent = delivery.p2pAppEventForMessage(publicMessage);
|
|
1038
|
+
await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
1039
|
+
delivery.markVolatileSeen(rawMessage);
|
|
1040
|
+
}
|
|
1041
|
+
finally {
|
|
1042
|
+
delivery.finishVolatileDelivery(rawMessage);
|
|
1043
|
+
}
|
|
1044
|
+
await delivery.ackVolatileMessages([rawMessage], true, false);
|
|
1045
|
+
}
|
|
970
1046
|
async sendV2(to, payload, opts) {
|
|
971
1047
|
const client = this.client;
|
|
972
1048
|
await client._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
|
|
973
1049
|
const toAid = String(to ?? '').trim();
|
|
1050
|
+
const targetDeviceId = String(opts?.deviceId ?? '').trim();
|
|
974
1051
|
if (!toAid)
|
|
975
1052
|
throw new ValidationError("message.send requires 'to'");
|
|
976
1053
|
if (!isJsonObject(payload))
|
|
@@ -981,6 +1058,7 @@ export class V2E2EECoordinator {
|
|
|
981
1058
|
payload,
|
|
982
1059
|
messageId: opts?.messageId,
|
|
983
1060
|
timestamp: opts?.timestamp,
|
|
1061
|
+
deviceId: targetDeviceId || undefined,
|
|
984
1062
|
protectedHeaders: opts?.protectedHeaders,
|
|
985
1063
|
context: opts?.context,
|
|
986
1064
|
useCache,
|
|
@@ -989,6 +1067,8 @@ export class V2E2EECoordinator {
|
|
|
989
1067
|
to: toAid,
|
|
990
1068
|
payload: envelope,
|
|
991
1069
|
message_id: opts?.messageId,
|
|
1070
|
+
...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
|
|
1071
|
+
...(targetDeviceId ? { device_id: targetDeviceId } : {}),
|
|
992
1072
|
encrypt: false,
|
|
993
1073
|
_skip_send_result_envelope: true,
|
|
994
1074
|
});
|
|
@@ -1046,6 +1126,11 @@ export class V2E2EECoordinator {
|
|
|
1046
1126
|
to: String(legacy.to ?? client._aid ?? ''), seq, type: String(msg.type ?? ''),
|
|
1047
1127
|
timestamp: msg.t_server, payload: payload, encrypted: false,
|
|
1048
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;
|
|
1049
1134
|
attachGatewayProximity(plaintext, msg);
|
|
1050
1135
|
}
|
|
1051
1136
|
else if (isJsonObject(payload)
|
|
@@ -1065,6 +1150,9 @@ export class V2E2EECoordinator {
|
|
|
1065
1150
|
client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
|
|
1066
1151
|
return null;
|
|
1067
1152
|
}
|
|
1153
|
+
if (publish && client._delivery.isSelfSentP2PMessage?.(plaintext)) {
|
|
1154
|
+
return plaintext;
|
|
1155
|
+
}
|
|
1068
1156
|
if (publish) {
|
|
1069
1157
|
const event = client._delivery.p2pAppEventForMessage(plaintext);
|
|
1070
1158
|
if (orderedPublish) {
|
|
@@ -1121,6 +1209,8 @@ export class V2E2EECoordinator {
|
|
|
1121
1209
|
group_id: groupId, group_aid: groupAid, seq, type: String(msg.type ?? ''),
|
|
1122
1210
|
timestamp: msg.t_server, payload: payload, encrypted: false,
|
|
1123
1211
|
};
|
|
1212
|
+
if (msg.sender_device_id !== undefined)
|
|
1213
|
+
plaintext.sender_device_id = msg.sender_device_id;
|
|
1124
1214
|
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1125
1215
|
attachGatewayProximity(plaintext, msg);
|
|
1126
1216
|
}
|
|
@@ -1232,6 +1322,7 @@ export class V2E2EECoordinator {
|
|
|
1232
1322
|
const client = this.client;
|
|
1233
1323
|
await client._ensureV2SessionReady('message.pull');
|
|
1234
1324
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1325
|
+
let volatileCursor = String(opts?.afterVolatileCursor ?? client._delivery.volatilePullCursor() ?? '').trim();
|
|
1235
1326
|
if (ns)
|
|
1236
1327
|
client._delivery.onPullStarted?.(ns);
|
|
1237
1328
|
if (opts?.windowMode === 'tail') {
|
|
@@ -1249,7 +1340,7 @@ export class V2E2EECoordinator {
|
|
|
1249
1340
|
}
|
|
1250
1341
|
if (ns && !opts?.gateLocked) {
|
|
1251
1342
|
const key = pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1252
|
-
after_seq: afterSeq, force: opts?.force === true, limit,
|
|
1343
|
+
after_seq: afterSeq, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
|
|
1253
1344
|
}, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1254
1345
|
return await client._runPullSerialized(key, () => this.pullV2Impl(afterSeq, limit, {
|
|
1255
1346
|
...(opts ?? {}),
|
|
@@ -1260,7 +1351,7 @@ export class V2E2EECoordinator {
|
|
|
1260
1351
|
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
1261
1352
|
let pullGateKey = ns
|
|
1262
1353
|
? pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1263
|
-
after_seq: afterSeq, force: opts?.force === true, limit,
|
|
1354
|
+
after_seq: afterSeq, after_volatile_cursor: volatileCursor, force: opts?.force === true, limit,
|
|
1264
1355
|
}, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`)
|
|
1265
1356
|
: '';
|
|
1266
1357
|
const deferredServerCursor = ns ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
@@ -1269,239 +1360,389 @@ export class V2E2EECoordinator {
|
|
|
1269
1360
|
let lastAutoAckSeq = 0;
|
|
1270
1361
|
let pendingAckSeq = deferredForwardAck;
|
|
1271
1362
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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;
|
|
1289
1450
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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;
|
|
1293
1462
|
}
|
|
1294
1463
|
else {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
+
}
|
|
1299
1482
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
.map((msg) => Number(msg.seq ?? 0))
|
|
1310
|
-
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1311
|
-
const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
1312
|
-
const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1313
|
-
? client._seqTracker.snapshotNamespace(ns)
|
|
1314
|
-
: null;
|
|
1315
|
-
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
1316
|
-
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
1317
|
-
const deferredKeyFetches = new Map();
|
|
1318
|
-
let blockedSeq = 0;
|
|
1319
|
-
const pageDecryptedBefore = decrypted.length;
|
|
1320
|
-
for (const msg of messages) {
|
|
1321
|
-
const seq = Number(msg.seq ?? 0);
|
|
1322
|
-
if (!Number.isFinite(seq) || seq <= 0)
|
|
1323
|
-
continue;
|
|
1324
|
-
const version = String(msg.version ?? 'v2');
|
|
1325
|
-
if (version === 'v1') {
|
|
1326
|
-
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
1327
|
-
const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
|
|
1328
|
-
const payloadType = isJsonObject(legacyPayload)
|
|
1329
|
-
? String(legacyPayload.type ?? '').trim()
|
|
1330
|
-
: '';
|
|
1331
|
-
if (legacyPayload !== undefined && legacyPayload !== null
|
|
1332
|
-
&& !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
|
|
1333
|
-
const v1Msg = {
|
|
1334
|
-
message_id: String(msg.message_id ?? ''),
|
|
1335
|
-
from: String(msg.from_aid ?? ''),
|
|
1336
|
-
to: String(legacy.to ?? client._aid ?? ''),
|
|
1337
|
-
seq: msg.seq,
|
|
1338
|
-
type: String(msg.type ?? ''),
|
|
1339
|
-
timestamp: msg.t_server,
|
|
1340
|
-
payload: legacyPayload,
|
|
1341
|
-
encrypted: false,
|
|
1342
|
-
};
|
|
1343
|
-
attachGatewayProximity(v1Msg, msg);
|
|
1344
|
-
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
1345
|
-
if (ns)
|
|
1346
|
-
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1347
|
-
else
|
|
1348
|
-
await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
|
|
1349
|
-
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);
|
|
1350
1492
|
}
|
|
1351
|
-
else
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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';
|
|
1360
1553
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
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);
|
|
1363
1558
|
if (ns)
|
|
1364
|
-
await client._publishPulledMessage(
|
|
1559
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1365
1560
|
else
|
|
1366
|
-
await client._publishAppEvent(
|
|
1367
|
-
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
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
|
|
1368
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);
|
|
1369
1614
|
}
|
|
1370
1615
|
else {
|
|
1371
|
-
client.
|
|
1616
|
+
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
1617
|
+
decrypted.push(plaintext);
|
|
1372
1618
|
}
|
|
1373
|
-
continue;
|
|
1374
|
-
}
|
|
1375
|
-
if (version !== 'v2') {
|
|
1376
|
-
client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1377
|
-
continue;
|
|
1378
|
-
}
|
|
1379
|
-
const msgSpkId = String(msg.spk_id ?? '');
|
|
1380
|
-
if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
|
|
1381
|
-
client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
|
|
1382
1619
|
}
|
|
1383
|
-
const
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
}
|
|
1388
|
-
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1389
|
-
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1390
|
-
deferredKeyFetches.set(key, {
|
|
1391
|
-
fromAid: deferStatus.fromAid,
|
|
1392
|
-
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1393
|
-
groupId: deferStatus.groupId ?? '',
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
if (plaintext === null)
|
|
1397
|
-
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));
|
|
1398
1624
|
if (ns) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
let contigAdvanced = false;
|
|
1415
|
-
try {
|
|
1416
|
-
if (commitTarget > pageContigBefore) {
|
|
1417
|
-
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1418
|
-
if (retentionFloor > pageContigBefore) {
|
|
1419
|
-
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
|
+
}
|
|
1420
1640
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
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);
|
|
1423
1647
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1648
|
+
}
|
|
1649
|
+
catch (exc) {
|
|
1650
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1651
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1426
1652
|
}
|
|
1653
|
+
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1654
|
+
throw exc;
|
|
1427
1655
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1433
|
-
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);
|
|
1434
1660
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
+
}
|
|
1439
1685
|
}
|
|
1440
|
-
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1441
|
-
throw exc;
|
|
1442
|
-
}
|
|
1443
|
-
if (contigAdvanced)
|
|
1444
|
-
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1445
|
-
for (const fetch of deferredKeyFetches.values()) {
|
|
1446
|
-
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1447
1686
|
}
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
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;
|
|
1450
1709
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
await this.confirmForwardP2PAck(ackSeq);
|
|
1470
|
-
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;
|
|
1471
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;
|
|
1472
1737
|
}
|
|
1738
|
+
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1739
|
+
if (tailDelayMs > 0)
|
|
1740
|
+
await v2Sleep(tailDelayMs);
|
|
1741
|
+
nextAfterSeq = nextAfter;
|
|
1473
1742
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const rawCount = messages.length;
|
|
1478
|
-
if (blockedSeq > 0)
|
|
1479
|
-
break;
|
|
1480
|
-
const shouldContinue = shouldContinueForwardPage({
|
|
1481
|
-
rawCount,
|
|
1482
|
-
nextAfterSeq,
|
|
1483
|
-
nextAfter,
|
|
1484
|
-
limit,
|
|
1485
|
-
hasMoreField,
|
|
1486
|
-
hasMore,
|
|
1487
|
-
serverHead,
|
|
1488
|
-
pendingAckSeq,
|
|
1489
|
-
observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
|
|
1490
|
-
? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
|
|
1491
|
-
});
|
|
1492
|
-
if (!shouldContinue)
|
|
1493
|
-
break;
|
|
1494
|
-
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1495
|
-
const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1496
|
-
after_seq: nextAfter, force: opts?.force === true, limit,
|
|
1497
|
-
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1498
|
-
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1499
|
-
pullGateKey = nextKey;
|
|
1500
|
-
}
|
|
1501
|
-
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1502
|
-
if (tailDelayMs > 0)
|
|
1503
|
-
await v2Sleep(tailDelayMs);
|
|
1504
|
-
nextAfterSeq = nextAfter;
|
|
1743
|
+
}
|
|
1744
|
+
finally {
|
|
1745
|
+
await stopPipeline();
|
|
1505
1746
|
}
|
|
1506
1747
|
if (pageCount >= maxPages) {
|
|
1507
1748
|
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
@@ -1513,6 +1754,73 @@ export class V2E2EECoordinator {
|
|
|
1513
1754
|
client._delivery.onPullWorkSettled?.();
|
|
1514
1755
|
return deduplicatePulledResults(decrypted);
|
|
1515
1756
|
}
|
|
1757
|
+
async publishV2VolatileMessages(messages, source, advanceCursor) {
|
|
1758
|
+
const client = this.client;
|
|
1759
|
+
if (!client._v2Session)
|
|
1760
|
+
return [];
|
|
1761
|
+
const processed = [];
|
|
1762
|
+
const plaintextMessages = [];
|
|
1763
|
+
let complete = true;
|
|
1764
|
+
for (const rawMessage of messages) {
|
|
1765
|
+
const claim = client._delivery.beginVolatileDelivery(rawMessage);
|
|
1766
|
+
if (claim === 'seen') {
|
|
1767
|
+
processed.push(rawMessage);
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (claim === 'inflight') {
|
|
1771
|
+
complete = false;
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
let plaintext;
|
|
1775
|
+
try {
|
|
1776
|
+
if (client._isEncryptedPushMessage(rawMessage)) {
|
|
1777
|
+
plaintext = await client._decryptV2Message(rawMessage, true, true, true, true, undefined, false, source);
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
const legacy = isJsonObject(rawMessage.legacy_v1)
|
|
1781
|
+
? rawMessage.legacy_v1
|
|
1782
|
+
: {};
|
|
1783
|
+
plaintext = {
|
|
1784
|
+
message_id: String(rawMessage.message_id ?? ''),
|
|
1785
|
+
from: String(rawMessage.from_aid ?? rawMessage.from ?? ''),
|
|
1786
|
+
to: String(legacy.to ?? rawMessage.to ?? client._aid ?? ''),
|
|
1787
|
+
type: String(rawMessage.type ?? ''),
|
|
1788
|
+
timestamp: rawMessage.t_server ?? rawMessage.timestamp,
|
|
1789
|
+
payload: legacy.payload !== undefined ? legacy.payload : rawMessage.payload,
|
|
1790
|
+
encrypted: false,
|
|
1791
|
+
};
|
|
1792
|
+
for (const key of ['context', 'protected_headers', 'headers', 'client_signature']) {
|
|
1793
|
+
if (key in rawMessage)
|
|
1794
|
+
plaintext[key] = rawMessage[key];
|
|
1795
|
+
}
|
|
1796
|
+
if (rawMessage.sender_device_id !== undefined)
|
|
1797
|
+
plaintext.sender_device_id = rawMessage.sender_device_id;
|
|
1798
|
+
attachGatewayProximity(plaintext, rawMessage);
|
|
1799
|
+
}
|
|
1800
|
+
if (!plaintext) {
|
|
1801
|
+
complete = false;
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
const publicMessage = client._delivery.publicVolatileMessage(plaintext);
|
|
1805
|
+
for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
|
|
1806
|
+
if (key in rawMessage)
|
|
1807
|
+
publicMessage[key] = rawMessage[key];
|
|
1808
|
+
}
|
|
1809
|
+
const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
|
|
1810
|
+
await client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
1811
|
+
client._delivery.markVolatileSeen(rawMessage);
|
|
1812
|
+
processed.push(rawMessage);
|
|
1813
|
+
plaintextMessages.push(publicMessage);
|
|
1814
|
+
}
|
|
1815
|
+
finally {
|
|
1816
|
+
client._delivery.finishVolatileDelivery(rawMessage);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (processed.length > 0) {
|
|
1820
|
+
await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
|
|
1821
|
+
}
|
|
1822
|
+
return plaintextMessages;
|
|
1823
|
+
}
|
|
1516
1824
|
async confirmForwardP2PAck(upToSeq, inlineGeneration) {
|
|
1517
1825
|
const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
|
|
1518
1826
|
try {
|
|
@@ -1528,41 +1836,51 @@ export class V2E2EECoordinator {
|
|
|
1528
1836
|
throw exc;
|
|
1529
1837
|
}
|
|
1530
1838
|
}
|
|
1531
|
-
async ackV2(upToSeq, inlineGeneration) {
|
|
1532
|
-
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false);
|
|
1839
|
+
async ackV2(upToSeq, inlineGeneration, volatileCursors = [], volatileIds = []) {
|
|
1840
|
+
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false, volatileCursors, volatileIds);
|
|
1533
1841
|
}
|
|
1534
1842
|
async ackV2ForForward(upToSeq, inlineGeneration) {
|
|
1535
1843
|
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, true);
|
|
1536
1844
|
}
|
|
1537
|
-
async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark) {
|
|
1845
|
+
async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark, volatileCursors = [], volatileIds = []) {
|
|
1538
1846
|
const client = this.client;
|
|
1539
1847
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1540
1848
|
let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
|
|
1541
1849
|
if (typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq <= 0)
|
|
1850
|
+
seq = 0;
|
|
1851
|
+
if (seq <= 0 && volatileCursors.length === 0 && volatileIds.length === 0)
|
|
1542
1852
|
return { acked: 0 };
|
|
1543
|
-
|
|
1544
|
-
|
|
1853
|
+
if (seq > 0)
|
|
1854
|
+
seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
|
|
1855
|
+
if ((!Number.isSafeInteger(seq) || seq <= 0) && volatileCursors.length === 0 && volatileIds.length === 0) {
|
|
1545
1856
|
return { acked: 0 };
|
|
1857
|
+
}
|
|
1546
1858
|
// ACK 发出前绑定当时的 inline pending;迟到响应不得结算重置后新建的 pending。
|
|
1547
1859
|
const ackGeneration = inlineGeneration ?? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null;
|
|
1548
1860
|
const v2SessionAtStart = client._v2Session;
|
|
1549
1861
|
const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
|
|
1550
|
-
const raw = await client._callRawV2Rpc('message.v2.ack', {
|
|
1862
|
+
const raw = await client._callRawV2Rpc('message.v2.ack', {
|
|
1863
|
+
...(seq > 0 ? { up_to_seq: seq } : {}),
|
|
1864
|
+
...(volatileCursors.length > 0 ? { volatile_cursors: volatileCursors } : {}),
|
|
1865
|
+
...(volatileIds.length > 0 ? { volatile_ids: volatileIds } : {}),
|
|
1866
|
+
});
|
|
1551
1867
|
const result = isJsonObject(raw)
|
|
1552
1868
|
? { ...raw }
|
|
1553
1869
|
: { result: raw };
|
|
1554
|
-
const actualAckSeq =
|
|
1555
|
-
? (
|
|
1556
|
-
|
|
1557
|
-
|
|
1870
|
+
const actualAckSeq = seq > 0
|
|
1871
|
+
? (requireServerWatermark
|
|
1872
|
+
? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
|
|
1873
|
+
: (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq))
|
|
1874
|
+
: 0;
|
|
1875
|
+
if (seq > 0 && requireServerWatermark && actualAckSeq < seq) {
|
|
1558
1876
|
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1559
1877
|
}
|
|
1560
1878
|
result.ack_seq = actualAckSeq;
|
|
1561
1879
|
result.success = true;
|
|
1562
|
-
if (ackGeneration !== null) {
|
|
1880
|
+
if (seq > 0 && ackGeneration !== null) {
|
|
1563
1881
|
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
|
|
1564
1882
|
}
|
|
1565
|
-
if (v2SessionAtStart
|
|
1883
|
+
if (seq > 0 && v2SessionAtStart
|
|
1566
1884
|
&& client._v2Session === v2SessionAtStart
|
|
1567
1885
|
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
|
|
1568
1886
|
try {
|
|
@@ -1794,83 +2112,197 @@ export class V2E2EECoordinator {
|
|
|
1794
2112
|
let lastAutoAckSeq = 0;
|
|
1795
2113
|
let pendingAckSeq = deferredForwardAck;
|
|
1796
2114
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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;
|
|
1816
2195
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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;
|
|
1820
2206
|
}
|
|
1821
2207
|
else {
|
|
1822
|
-
client.
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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);
|
|
1826
2216
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const hasMore = result.has_more === true;
|
|
1837
|
-
const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
|
|
1838
|
-
const seqs = messages
|
|
1839
|
-
.map((msg) => Number(msg.seq ?? 0))
|
|
1840
|
-
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1841
|
-
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
1842
|
-
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1843
|
-
? client._seqTracker.snapshotNamespace(ns)
|
|
1844
|
-
: null;
|
|
1845
|
-
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
1846
|
-
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
1847
|
-
const deferredKeyFetches = new Map();
|
|
1848
|
-
let blockedSeq = 0;
|
|
1849
|
-
const pageDecryptedBefore = decrypted.length;
|
|
1850
|
-
for (const msg of messages) {
|
|
1851
|
-
const seq = Number(msg.seq ?? 0);
|
|
1852
|
-
if (!Number.isFinite(seq) || seq <= 0)
|
|
1853
|
-
continue;
|
|
1854
|
-
const version = String(msg.version ?? 'v2');
|
|
1855
|
-
if (version === 'v1') {
|
|
1856
|
-
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
1857
|
-
const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
|
|
1858
|
-
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
1859
|
-
// 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
|
|
1860
|
-
if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
|
|
1861
|
-
&& client._delivery.recallEventFromGroupMessage(msg)) {
|
|
1862
|
-
await client._delivery.publishGroupRecallTombstone(gid, seq, {
|
|
1863
|
-
...msg,
|
|
1864
|
-
group_id: gid,
|
|
1865
|
-
group_aid: eventGroupAid,
|
|
1866
|
-
}, 'pull');
|
|
1867
|
-
client._markPublishedSeq(ns, seq);
|
|
1868
|
-
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
1869
|
-
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);
|
|
1870
2226
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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) {
|
|
1874
2306
|
let v1Msg = {
|
|
1875
2307
|
message_id: String(msg.message_id ?? ''),
|
|
1876
2308
|
from: String(msg.from_aid ?? ''),
|
|
@@ -1882,194 +2314,195 @@ export class V2E2EECoordinator {
|
|
|
1882
2314
|
payload,
|
|
1883
2315
|
encrypted: false,
|
|
1884
2316
|
};
|
|
2317
|
+
if (msg.sender_device_id !== undefined)
|
|
2318
|
+
v1Msg.sender_device_id = msg.sender_device_id;
|
|
1885
2319
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1886
2320
|
attachGatewayProximity(v1Msg, msg);
|
|
1887
|
-
|
|
1888
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1889
|
-
}
|
|
1890
|
-
else {
|
|
1891
|
-
client._markPublishedSeq(ns, seq);
|
|
1892
|
-
}
|
|
2321
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1893
2322
|
decrypted.push(v1Msg);
|
|
1894
2323
|
continue;
|
|
1895
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;
|
|
1896
2349
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
message_id: String(msg.message_id ?? ''),
|
|
1900
|
-
from: String(msg.from_aid ?? ''),
|
|
1901
|
-
group_id: gid,
|
|
1902
|
-
group_aid: eventGroupAid,
|
|
1903
|
-
seq: msg.seq,
|
|
1904
|
-
type: String(msg.type ?? ''),
|
|
1905
|
-
timestamp: msg.t_server,
|
|
1906
|
-
payload,
|
|
1907
|
-
encrypted: false,
|
|
1908
|
-
};
|
|
1909
|
-
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1910
|
-
attachGatewayProximity(v1Msg, msg);
|
|
1911
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1912
|
-
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 ?? '')}`);
|
|
1913
2352
|
continue;
|
|
1914
2353
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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}`);
|
|
1923
2395
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
plaintext.group_id = gid;
|
|
1927
|
-
plaintext.group_aid = eventGroupAid;
|
|
1928
|
-
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
1929
|
-
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}`);
|
|
1930
2398
|
}
|
|
1931
|
-
|
|
1932
|
-
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}`);
|
|
1933
2401
|
}
|
|
1934
|
-
|
|
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);
|
|
1935
2409
|
}
|
|
1936
2410
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
if (deferStatus.deferred) {
|
|
1947
|
-
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1948
|
-
}
|
|
1949
|
-
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1950
|
-
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1951
|
-
deferredKeyFetches.set(key, {
|
|
1952
|
-
fromAid: deferStatus.fromAid,
|
|
1953
|
-
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1954
|
-
groupId: deferStatus.groupId ?? '',
|
|
1955
|
-
});
|
|
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);
|
|
1956
2420
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1960
|
-
plaintext.group_id = gid;
|
|
1961
|
-
plaintext.group_aid = eventGroupAid;
|
|
1962
|
-
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
1963
|
-
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);
|
|
1964
2423
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
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);
|
|
1967
2427
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
}
|
|
1986
|
-
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1987
|
-
client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
|
|
1988
|
-
}
|
|
1989
|
-
if (seqs.length > 0) {
|
|
1990
|
-
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1994
|
-
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1995
|
-
if (contigAdvanced) {
|
|
1996
|
-
await client._commitSeqTrackerState(ns);
|
|
1997
|
-
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1998
|
-
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);
|
|
1999
2445
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
2446
|
+
else {
|
|
2447
|
+
await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
|
|
2448
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
2004
2449
|
}
|
|
2005
|
-
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
2006
|
-
throw exc;
|
|
2007
2450
|
}
|
|
2008
|
-
if (contigAdvanced)
|
|
2009
|
-
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
2010
|
-
}
|
|
2011
|
-
for (const fetch of deferredKeyFetches.values()) {
|
|
2012
|
-
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
2013
|
-
}
|
|
2014
|
-
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);
|
|
2015
|
-
if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
|
|
2016
|
-
this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
|
|
2017
|
-
}
|
|
2018
|
-
// 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
|
|
2019
|
-
// 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
|
|
2020
|
-
const knownServerCursorSeq = hasServerCursor
|
|
2021
|
-
? cursorCurrentSeq
|
|
2022
|
-
: (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
|
|
2023
|
-
const ackNeeded = ackSeq > 0
|
|
2024
|
-
&& ackSeq > lastAutoAckSeq
|
|
2025
|
-
&& ownsCursor
|
|
2026
|
-
&& ((hasServerCursor && ackSeq > cursorCurrentSeq)
|
|
2027
|
-
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
2028
|
-
if (ackNeeded) {
|
|
2029
|
-
this.recordForwardAck(ns, ackSeq);
|
|
2030
2451
|
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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);
|
|
2035
2489
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
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;
|
|
2039
2497
|
}
|
|
2498
|
+
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
2499
|
+
if (tailDelayMs > 0)
|
|
2500
|
+
await v2Sleep(tailDelayMs);
|
|
2501
|
+
nextAfterSeq = nextAfter;
|
|
2040
2502
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
const rawCount = messages.length;
|
|
2045
|
-
if (blockedSeq > 0)
|
|
2046
|
-
break;
|
|
2047
|
-
const shouldContinue = shouldContinueForwardPage({
|
|
2048
|
-
rawCount,
|
|
2049
|
-
nextAfterSeq,
|
|
2050
|
-
nextAfter,
|
|
2051
|
-
limit,
|
|
2052
|
-
hasMoreField,
|
|
2053
|
-
hasMore,
|
|
2054
|
-
serverHead,
|
|
2055
|
-
pendingAckSeq,
|
|
2056
|
-
observedUpper: typeof client._seqTracker.getMaxSeenSeq === 'function'
|
|
2057
|
-
? client._seqTracker.getMaxSeenSeq(ns) : nextAfter,
|
|
2058
|
-
});
|
|
2059
|
-
if (!shouldContinue)
|
|
2060
|
-
break;
|
|
2061
|
-
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
2062
|
-
const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
2063
|
-
group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
|
|
2064
|
-
_group_cursor_params: cursorParams,
|
|
2065
|
-
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
2066
|
-
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
2067
|
-
pullGateKey = nextKey;
|
|
2068
|
-
}
|
|
2069
|
-
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
2070
|
-
if (tailDelayMs > 0)
|
|
2071
|
-
await v2Sleep(tailDelayMs);
|
|
2072
|
-
nextAfterSeq = nextAfter;
|
|
2503
|
+
}
|
|
2504
|
+
finally {
|
|
2505
|
+
await stopPipeline();
|
|
2073
2506
|
}
|
|
2074
2507
|
if (pageCount >= maxPages) {
|
|
2075
2508
|
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
@@ -2546,7 +2979,6 @@ export class V2E2EECoordinator {
|
|
|
2546
2979
|
seq: msg.seq ?? null,
|
|
2547
2980
|
timestamp: msg.timestamp ?? msg.t_server ?? null,
|
|
2548
2981
|
device_id: msg.device_id ?? null,
|
|
2549
|
-
slot_id: msg.slot_id ?? null,
|
|
2550
2982
|
_decrypt_error: 'encrypted push payload is not decryptable on raw push path',
|
|
2551
2983
|
_decrypt_stage: 'push_envelope',
|
|
2552
2984
|
};
|
|
@@ -2590,13 +3022,18 @@ export class V2E2EECoordinator {
|
|
|
2590
3022
|
encrypted: true,
|
|
2591
3023
|
e2ee: e2eeMeta,
|
|
2592
3024
|
};
|
|
2593
|
-
|
|
3025
|
+
const senderDeviceId = String(aad.from_device ?? '').trim();
|
|
3026
|
+
if (senderDeviceId)
|
|
3027
|
+
result.sender_device_id = senderDeviceId;
|
|
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';
|
|
2594
3033
|
if (msg.t_server !== undefined)
|
|
2595
3034
|
result.t_server = msg.t_server;
|
|
2596
3035
|
if (msg.device_id !== undefined)
|
|
2597
3036
|
result.device_id = msg.device_id;
|
|
2598
|
-
if (msg.slot_id !== undefined)
|
|
2599
|
-
result.slot_id = msg.slot_id;
|
|
2600
3037
|
attachGatewayProximity(result, msg);
|
|
2601
3038
|
if (group) {
|
|
2602
3039
|
result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
|
|
@@ -2674,6 +3111,10 @@ export class V2E2EECoordinator {
|
|
|
2674
3111
|
if (peerDevices.length === 0) {
|
|
2675
3112
|
throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
|
|
2676
3113
|
}
|
|
3114
|
+
const targetDeviceId = String(opts.deviceId ?? '').trim();
|
|
3115
|
+
if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
|
|
3116
|
+
throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
|
|
3117
|
+
}
|
|
2677
3118
|
let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
|
|
2678
3119
|
if (targetSet) {
|
|
2679
3120
|
targetSet = this.cloneP2PTargetSet(targetSet);
|
|
@@ -2751,6 +3192,20 @@ export class V2E2EECoordinator {
|
|
|
2751
3192
|
});
|
|
2752
3193
|
targetSet = this.cloneP2PTargetSet(targetSet);
|
|
2753
3194
|
}
|
|
3195
|
+
if (targetDeviceId) {
|
|
3196
|
+
let peerTargetFound = false;
|
|
3197
|
+
targetSet.targets = targetSet.targets.filter((target) => {
|
|
3198
|
+
const isPeerTarget = target.aid === to
|
|
3199
|
+
&& (target.deviceId === targetDeviceId || (wrapPolicy?.scope === 'aid' && !target.deviceId))
|
|
3200
|
+
&& target.role !== 'self_sync';
|
|
3201
|
+
if (isPeerTarget)
|
|
3202
|
+
peerTargetFound = true;
|
|
3203
|
+
return isPeerTarget || target.role === 'self_sync';
|
|
3204
|
+
});
|
|
3205
|
+
if (!peerTargetFound) {
|
|
3206
|
+
throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
2754
3209
|
const sender = await session.getSenderIdentity();
|
|
2755
3210
|
const envelope = await encryptP2PMessage(sender, targetSet, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
|
|
2756
3211
|
return envelope;
|