@agentunion/fastaun 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 +336 -260
- package/_packed_docs/CHANGELOG.md +336 -260
- 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.js +2 -0
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +3 -1
- package/dist/agent-md.js +10 -2
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +58 -0
- package/dist/aid-store.js +234 -3
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +17 -1
- package/dist/aid.js +48 -2
- package/dist/aid.js.map +1 -1
- package/dist/client/delivery.d.ts +19 -0
- package/dist/client/delivery.js +264 -31
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts +4 -0
- package/dist/client/group-state.js +63 -0
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/rpc-pipeline.js +29 -14
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +17 -2
- package/dist/client/v2-e2ee.js +971 -520
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +120 -19
- package/dist/client.js.map +1 -1
- package/dist/facades.d.ts +7 -0
- package/dist/facades.js +11 -0
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.js +7 -5
- package/dist/group-fs.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ownership-proof.d.ts +40 -0
- package/dist/ownership-proof.js +140 -0
- package/dist/ownership-proof.js.map +1 -0
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.js +61 -6
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +31 -1
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -14
package/dist/client/v2-e2ee.js
CHANGED
|
@@ -10,6 +10,52 @@ const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
|
|
|
10
10
|
const V2_PULL_DEFAULT_PAGE_LIMIT = 50;
|
|
11
11
|
const V2_PULL_MAX_PAGE_LIMIT = 50;
|
|
12
12
|
const V2_SENDER_IK_SYNC_FETCH_TIMEOUT_MS = 3_000;
|
|
13
|
+
const V2_PULL_PIPELINE_QUEUE_CAPACITY = 100;
|
|
14
|
+
class V2PullPageQueue {
|
|
15
|
+
capacity;
|
|
16
|
+
items = [];
|
|
17
|
+
takers = [];
|
|
18
|
+
putters = [];
|
|
19
|
+
queuedWeight = 0;
|
|
20
|
+
closed = false;
|
|
21
|
+
constructor(capacity) {
|
|
22
|
+
this.capacity = capacity;
|
|
23
|
+
}
|
|
24
|
+
async put(item, weight = 1) {
|
|
25
|
+
const itemWeight = Math.max(1, Math.trunc(weight));
|
|
26
|
+
while (!this.closed && this.items.length > 0 && this.queuedWeight + itemWeight > this.capacity) {
|
|
27
|
+
await new Promise((resolve) => this.putters.push(resolve));
|
|
28
|
+
}
|
|
29
|
+
if (this.closed)
|
|
30
|
+
return false;
|
|
31
|
+
const taker = this.takers.shift();
|
|
32
|
+
if (taker)
|
|
33
|
+
taker(item);
|
|
34
|
+
else {
|
|
35
|
+
this.items.push(item);
|
|
36
|
+
this.queuedWeight += itemWeight;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
async take() {
|
|
41
|
+
const item = this.items.shift();
|
|
42
|
+
if (item !== undefined) {
|
|
43
|
+
this.queuedWeight = Math.max(0, this.queuedWeight - Math.max(1, Math.trunc(Number(item.weight ?? 1))));
|
|
44
|
+
this.putters.shift()?.();
|
|
45
|
+
return item;
|
|
46
|
+
}
|
|
47
|
+
if (this.closed)
|
|
48
|
+
return undefined;
|
|
49
|
+
return await new Promise((resolve) => this.takers.push(resolve));
|
|
50
|
+
}
|
|
51
|
+
close() {
|
|
52
|
+
this.closed = true;
|
|
53
|
+
for (const resolve of this.takers.splice(0))
|
|
54
|
+
resolve(undefined);
|
|
55
|
+
for (const resolve of this.putters.splice(0))
|
|
56
|
+
resolve();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
13
59
|
function pullGateKeyForClient(client, method, params, fallback) {
|
|
14
60
|
const key = client._rpcPipeline?.pullGateKeyForCall?.(method, params);
|
|
15
61
|
return typeof key === 'string' && key ? key : fallback;
|
|
@@ -773,6 +819,12 @@ export class V2E2EECoordinator {
|
|
|
773
819
|
}
|
|
774
820
|
continue;
|
|
775
821
|
}
|
|
822
|
+
if (!entry.groupId && String(entry.msg.storage ?? '') === 'volatile') {
|
|
823
|
+
await this.publishPendingV2VolatileMessage(entry.msg, plaintext, 'pending_retry');
|
|
824
|
+
pending.delete(key);
|
|
825
|
+
client._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
776
828
|
pending.delete(key);
|
|
777
829
|
const seq = Number(entry.msg.seq ?? 0);
|
|
778
830
|
if (entry.groupId) {
|
|
@@ -795,6 +847,30 @@ export class V2E2EECoordinator {
|
|
|
795
847
|
}
|
|
796
848
|
}
|
|
797
849
|
}
|
|
850
|
+
async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
|
|
851
|
+
const delivery = this.client._delivery;
|
|
852
|
+
const claim = delivery.beginVolatileDelivery(rawMessage);
|
|
853
|
+
if (claim === 'inflight')
|
|
854
|
+
return;
|
|
855
|
+
if (claim === 'seen') {
|
|
856
|
+
await delivery.ackVolatileMessages([rawMessage], true, false);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
const publicMessage = delivery.publicVolatileMessage(plaintext);
|
|
861
|
+
for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
|
|
862
|
+
if (Object.prototype.hasOwnProperty.call(rawMessage, key))
|
|
863
|
+
publicMessage[key] = rawMessage[key];
|
|
864
|
+
}
|
|
865
|
+
const appEvent = delivery.p2pAppEventForMessage(publicMessage);
|
|
866
|
+
await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
867
|
+
delivery.markVolatileSeen(rawMessage);
|
|
868
|
+
}
|
|
869
|
+
finally {
|
|
870
|
+
delivery.finishVolatileDelivery(rawMessage);
|
|
871
|
+
}
|
|
872
|
+
await delivery.ackVolatileMessages([rawMessage], true, false);
|
|
873
|
+
}
|
|
798
874
|
async buildV2P2PEnvelope(opts) {
|
|
799
875
|
const client = this.client;
|
|
800
876
|
if (!client._v2Session) {
|
|
@@ -841,6 +917,10 @@ export class V2E2EECoordinator {
|
|
|
841
917
|
if (peerDevices.length === 0) {
|
|
842
918
|
throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
|
|
843
919
|
}
|
|
920
|
+
const targetDeviceId = String(opts.deviceId ?? '').trim();
|
|
921
|
+
if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
|
|
922
|
+
throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
|
|
923
|
+
}
|
|
844
924
|
let targetSet = useCache ? this.getTargetSetCache(to) : undefined;
|
|
845
925
|
if (targetSet) {
|
|
846
926
|
targetSet = this.cloneP2PTargetSet(targetSet);
|
|
@@ -926,6 +1006,20 @@ export class V2E2EECoordinator {
|
|
|
926
1006
|
if (targetSet.targets.length === 0) {
|
|
927
1007
|
throw new E2EEError(`V2 bootstrap: no usable devices found for ${to}`);
|
|
928
1008
|
}
|
|
1009
|
+
if (targetDeviceId) {
|
|
1010
|
+
let peerTargetFound = false;
|
|
1011
|
+
targetSet.targets = targetSet.targets.filter((target) => {
|
|
1012
|
+
const isPeerTarget = target.aid === to
|
|
1013
|
+
&& (target.deviceId === targetDeviceId || (wrapPolicy.scope === 'aid' && !target.deviceId))
|
|
1014
|
+
&& target.role !== 'self_sync';
|
|
1015
|
+
if (isPeerTarget)
|
|
1016
|
+
peerTargetFound = true;
|
|
1017
|
+
return isPeerTarget || target.role === 'self_sync';
|
|
1018
|
+
});
|
|
1019
|
+
if (!peerTargetFound) {
|
|
1020
|
+
throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
929
1023
|
const envelope = encryptP2PMessage(session.getSenderIdentity(), targetSet, opts.payload, {
|
|
930
1024
|
messageId: opts.messageId,
|
|
931
1025
|
timestamp: opts.timestamp,
|
|
@@ -954,6 +1048,7 @@ export class V2E2EECoordinator {
|
|
|
954
1048
|
const client = this.client;
|
|
955
1049
|
await client._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
|
|
956
1050
|
const toAid = String(to ?? '').trim();
|
|
1051
|
+
const targetDeviceId = String(opts?.deviceId ?? '').trim();
|
|
957
1052
|
if (!toAid)
|
|
958
1053
|
throw new ValidationError("message.send requires 'to'");
|
|
959
1054
|
if (!isJsonObject(payload))
|
|
@@ -971,6 +1066,7 @@ export class V2E2EECoordinator {
|
|
|
971
1066
|
payload,
|
|
972
1067
|
messageId,
|
|
973
1068
|
timestamp: opts?.timestamp,
|
|
1069
|
+
deviceId: targetDeviceId || undefined,
|
|
974
1070
|
protectedHeaders: opts?.protectedHeaders,
|
|
975
1071
|
context: opts?.context,
|
|
976
1072
|
useCache,
|
|
@@ -979,6 +1075,8 @@ export class V2E2EECoordinator {
|
|
|
979
1075
|
to: toAid,
|
|
980
1076
|
payload: envelope,
|
|
981
1077
|
message_id: messageId,
|
|
1078
|
+
...(targetDeviceId ? { device_id: targetDeviceId } : {}),
|
|
1079
|
+
...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
|
|
982
1080
|
encrypt: false,
|
|
983
1081
|
_skip_send_result_envelope: true,
|
|
984
1082
|
});
|
|
@@ -992,6 +1090,7 @@ export class V2E2EECoordinator {
|
|
|
992
1090
|
payload,
|
|
993
1091
|
message_id: messageId,
|
|
994
1092
|
timestamp: opts?.timestamp,
|
|
1093
|
+
ttl: opts?.ttl,
|
|
995
1094
|
protected_headers: opts?.protectedHeaders,
|
|
996
1095
|
context: opts?.context,
|
|
997
1096
|
}, result, true);
|
|
@@ -1007,6 +1106,7 @@ export class V2E2EECoordinator {
|
|
|
1007
1106
|
payload,
|
|
1008
1107
|
message_id: messageId,
|
|
1009
1108
|
timestamp: opts?.timestamp,
|
|
1109
|
+
ttl: opts?.ttl,
|
|
1010
1110
|
protected_headers: opts?.protectedHeaders,
|
|
1011
1111
|
context: opts?.context,
|
|
1012
1112
|
}, result, true);
|
|
@@ -1043,6 +1143,11 @@ export class V2E2EECoordinator {
|
|
|
1043
1143
|
payload: payload,
|
|
1044
1144
|
encrypted: false,
|
|
1045
1145
|
};
|
|
1146
|
+
if (msg.sender_device_id !== undefined)
|
|
1147
|
+
plaintext.sender_device_id = msg.sender_device_id;
|
|
1148
|
+
const direction = String(msg.direction ?? '').trim();
|
|
1149
|
+
if (direction)
|
|
1150
|
+
plaintext.direction = direction;
|
|
1046
1151
|
attachGatewayProximity(plaintext, msg);
|
|
1047
1152
|
}
|
|
1048
1153
|
else if (isJsonObject(payload)
|
|
@@ -1060,6 +1165,9 @@ export class V2E2EECoordinator {
|
|
|
1060
1165
|
client._clientLog.warn(`P2P Tail 单条消息解密失败,连续性证明仍保留: seq=${seq}`);
|
|
1061
1166
|
return null;
|
|
1062
1167
|
}
|
|
1168
|
+
if (publish && client._delivery.isSelfSentP2PMessage?.(plaintext)) {
|
|
1169
|
+
return plaintext;
|
|
1170
|
+
}
|
|
1063
1171
|
if (publish) {
|
|
1064
1172
|
const event = client._delivery.p2pAppEventForMessage(plaintext);
|
|
1065
1173
|
await client._publishAppEvent(event.event, event.payload, source);
|
|
@@ -1112,6 +1220,8 @@ export class V2E2EECoordinator {
|
|
|
1112
1220
|
payload: payload,
|
|
1113
1221
|
encrypted: false,
|
|
1114
1222
|
};
|
|
1223
|
+
if (msg.sender_device_id !== undefined)
|
|
1224
|
+
plaintext.sender_device_id = msg.sender_device_id;
|
|
1115
1225
|
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1116
1226
|
attachGatewayProximity(plaintext, msg);
|
|
1117
1227
|
}
|
|
@@ -1223,6 +1333,7 @@ export class V2E2EECoordinator {
|
|
|
1223
1333
|
}), true);
|
|
1224
1334
|
}
|
|
1225
1335
|
const decrypted = [];
|
|
1336
|
+
let volatileCursor = String(opts?.afterVolatileCursor ?? client._delivery.volatilePullCursor?.() ?? '').trim();
|
|
1226
1337
|
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
1227
1338
|
let pullGateKey = ns
|
|
1228
1339
|
? pullGateKeyForClient(client, 'message.v2.pull', {
|
|
@@ -1235,264 +1346,412 @@ export class V2E2EECoordinator {
|
|
|
1235
1346
|
let lastAutoAckSeq = 0;
|
|
1236
1347
|
let pendingAckSeq = deferredForwardAck;
|
|
1237
1348
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1349
|
+
let pipelineRunning = false;
|
|
1350
|
+
let pipelineError = null;
|
|
1351
|
+
const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
|
|
1352
|
+
let pipelineProducer = null;
|
|
1353
|
+
let pipelineStopped = false;
|
|
1354
|
+
const startPipeline = (startAfterSeq, startVolatileCursor) => {
|
|
1355
|
+
if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
|
|
1356
|
+
return;
|
|
1357
|
+
pipelineRunning = true;
|
|
1358
|
+
pipelineProducer = (async () => {
|
|
1359
|
+
let producerAfterSeq = startAfterSeq;
|
|
1360
|
+
let producerVolatileCursor = startVolatileCursor;
|
|
1361
|
+
for (let produced = pageCount; produced < maxPages; produced += 1) {
|
|
1362
|
+
if (pipelineStopped)
|
|
1363
|
+
break;
|
|
1364
|
+
const requestedVolatileCursor = producerVolatileCursor;
|
|
1365
|
+
const result = await client._callRawV2Rpc('message.v2.pull', {
|
|
1366
|
+
after_seq: producerAfterSeq,
|
|
1367
|
+
limit,
|
|
1368
|
+
...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
|
|
1369
|
+
...(opts?.force ? { force: true } : {}),
|
|
1370
|
+
}, pullGateKey);
|
|
1371
|
+
if (pipelineStopped)
|
|
1372
|
+
break;
|
|
1373
|
+
const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1374
|
+
const enqueued = await pipelineQueue.put({
|
|
1375
|
+
result,
|
|
1376
|
+
afterSeq: producerAfterSeq,
|
|
1377
|
+
ackUpToSeq: 0,
|
|
1378
|
+
inlineAckGeneration: null,
|
|
1379
|
+
requestedVolatileCursor,
|
|
1380
|
+
weight: rawPageMessages.length,
|
|
1381
|
+
}, rawPageMessages.length);
|
|
1382
|
+
if (!enqueued || pipelineStopped)
|
|
1383
|
+
break;
|
|
1384
|
+
const volatileMessages = client._delivery.volatileMessages?.(rawPageMessages) ?? [];
|
|
1385
|
+
if (volatileMessages.length > 0)
|
|
1386
|
+
break;
|
|
1387
|
+
const messages = deduplicateForwardPageMessages(rawPageMessages, producerAfterSeq);
|
|
1388
|
+
const seqs = messages
|
|
1389
|
+
.map((msg) => Number(msg.seq ?? 0))
|
|
1390
|
+
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1391
|
+
const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
|
|
1392
|
+
const cursor = isJsonObject(result.cursor)
|
|
1393
|
+
? result.cursor : null;
|
|
1394
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1395
|
+
rawCount: messages.length,
|
|
1396
|
+
nextAfterSeq: producerAfterSeq,
|
|
1397
|
+
nextAfter,
|
|
1398
|
+
limit,
|
|
1399
|
+
hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
|
|
1400
|
+
hasMore: result.has_more === true,
|
|
1401
|
+
serverHead: safeNonNegativeSeq(cursor?.latest_seq) ?? 0,
|
|
1402
|
+
observedUpper: Math.max(nextAfter, Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
|
|
1403
|
+
pendingAckSeq: 0,
|
|
1404
|
+
});
|
|
1405
|
+
if (!shouldContinue)
|
|
1406
|
+
break;
|
|
1407
|
+
producerAfterSeq = nextAfter;
|
|
1408
|
+
producerVolatileCursor = '';
|
|
1409
|
+
}
|
|
1410
|
+
})().catch((error) => {
|
|
1411
|
+
pipelineError = error;
|
|
1412
|
+
}).finally(() => {
|
|
1413
|
+
pipelineQueue.close();
|
|
1414
|
+
});
|
|
1415
|
+
};
|
|
1416
|
+
const stopPipeline = async () => {
|
|
1417
|
+
pipelineStopped = true;
|
|
1418
|
+
pipelineQueue.close();
|
|
1419
|
+
if (pipelineProducer) {
|
|
1420
|
+
const producer = pipelineProducer;
|
|
1421
|
+
pipelineProducer = null;
|
|
1422
|
+
await producer;
|
|
1423
|
+
}
|
|
1424
|
+
pipelineRunning = false;
|
|
1425
|
+
};
|
|
1426
|
+
try {
|
|
1427
|
+
while (pageCount < maxPages) {
|
|
1428
|
+
let pipelinePage;
|
|
1429
|
+
if (pipelineRunning) {
|
|
1430
|
+
pipelinePage = await pipelineQueue.take();
|
|
1431
|
+
if (!pipelinePage) {
|
|
1432
|
+
pipelineRunning = false;
|
|
1433
|
+
if (pipelineError)
|
|
1434
|
+
throw pipelineError;
|
|
1435
|
+
break;
|
|
1256
1436
|
}
|
|
1257
|
-
pendingAckSeq = 0;
|
|
1258
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1259
|
-
this.clearForwardAck(ns, ackUpToSeq);
|
|
1260
1437
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1438
|
+
pageCount += 1;
|
|
1439
|
+
const requestedVolatileCursor = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
|
|
1440
|
+
const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
|
|
1441
|
+
const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
|
|
1442
|
+
? client._delivery.captureP2PInlineAckGeneration()
|
|
1443
|
+
: null);
|
|
1444
|
+
client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
|
|
1445
|
+
let result;
|
|
1446
|
+
if (pipelinePage) {
|
|
1447
|
+
nextAfterSeq = pipelinePage.afterSeq;
|
|
1448
|
+
result = pipelinePage.result;
|
|
1266
1449
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
|
|
1277
|
-
}
|
|
1278
|
-
const seqs = messages
|
|
1279
|
-
.map((msg) => Number(msg.seq ?? 0))
|
|
1280
|
-
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1281
|
-
const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
1282
|
-
const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1283
|
-
? client._seqTracker.snapshotNamespace(ns)
|
|
1284
|
-
: null;
|
|
1285
|
-
let pageMaxSeq = nextAfterSeq;
|
|
1286
|
-
if (seqs.length > 0) {
|
|
1287
|
-
pageMaxSeq = Math.max(...seqs);
|
|
1288
|
-
}
|
|
1289
|
-
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
1290
|
-
const deferredKeyFetches = new Map();
|
|
1291
|
-
let blockedSeq = 0;
|
|
1292
|
-
const pageDecryptedBefore = decrypted.length;
|
|
1293
|
-
for (const msg of messages) {
|
|
1294
|
-
const seq = Number(msg.seq ?? 0);
|
|
1295
|
-
if (!Number.isFinite(seq) || seq <= 0)
|
|
1296
|
-
continue;
|
|
1297
|
-
const version = String(msg.version ?? 'v2');
|
|
1298
|
-
if (version === 'v1') {
|
|
1299
|
-
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
1300
|
-
const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
|
|
1301
|
-
const payloadType = isJsonObject(legacyPayload)
|
|
1302
|
-
? String(legacyPayload.type ?? '').trim()
|
|
1303
|
-
: '';
|
|
1304
|
-
if (legacyPayload !== undefined && legacyPayload !== null
|
|
1305
|
-
&& !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
|
|
1306
|
-
const v1Msg = {
|
|
1307
|
-
message_id: String(msg.message_id ?? ''),
|
|
1308
|
-
from: String(msg.from_aid ?? ''),
|
|
1309
|
-
to: String(legacy.to ?? client._aid ?? ''),
|
|
1310
|
-
seq: msg.seq,
|
|
1311
|
-
type: String(msg.type ?? ''),
|
|
1312
|
-
timestamp: msg.t_server,
|
|
1313
|
-
payload: legacyPayload,
|
|
1314
|
-
encrypted: false,
|
|
1315
|
-
};
|
|
1316
|
-
attachGatewayProximity(v1Msg, msg);
|
|
1317
|
-
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
1318
|
-
if (ns) {
|
|
1319
|
-
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1320
|
-
}
|
|
1321
|
-
else {
|
|
1322
|
-
await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
|
|
1323
|
-
}
|
|
1324
|
-
decrypted.push(v1Msg);
|
|
1325
|
-
client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
|
|
1450
|
+
else {
|
|
1451
|
+
try {
|
|
1452
|
+
result = await client._callRawV2Rpc('message.v2.pull', {
|
|
1453
|
+
after_seq: nextAfterSeq,
|
|
1454
|
+
limit,
|
|
1455
|
+
...(volatileCursor ? { after_volatile_cursor: volatileCursor } : {}),
|
|
1456
|
+
...(opts?.force ? { force: true } : {}),
|
|
1457
|
+
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1458
|
+
}, pullGateKey);
|
|
1326
1459
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
|
|
1331
|
-
if (deferStatus.deferred) {
|
|
1332
|
-
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1333
|
-
if (deferStatus.fromAid) {
|
|
1334
|
-
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1335
|
-
deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
|
|
1336
|
-
}
|
|
1460
|
+
catch (exc) {
|
|
1461
|
+
if (!volatileCursor || !String(exc instanceof Error ? exc.message : exc).toLowerCase().includes('volatile cursor epoch mismatch')) {
|
|
1462
|
+
throw exc;
|
|
1337
1463
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1464
|
+
client._delivery.resetVolatilePullCursor?.();
|
|
1465
|
+
volatileCursor = '';
|
|
1466
|
+
pageCount -= 1;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (ackUpToSeq > 0) {
|
|
1471
|
+
const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
|
|
1472
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1473
|
+
if (inlineAckGeneration !== null) {
|
|
1474
|
+
client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
|
|
1344
1475
|
}
|
|
1476
|
+
pendingAckSeq = 0;
|
|
1477
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1478
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1345
1479
|
}
|
|
1346
1480
|
else {
|
|
1347
|
-
client._clientLog.
|
|
1481
|
+
client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1482
|
+
await this.confirmForwardP2PAck(ackUpToSeq);
|
|
1483
|
+
pendingAckSeq = 0;
|
|
1484
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1348
1485
|
}
|
|
1349
|
-
continue;
|
|
1350
1486
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
|
|
1357
|
-
client._v2Session.trackOldSPKMaxSeq(spkId, seq);
|
|
1487
|
+
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1488
|
+
const volatileMessages = client._delivery.volatileMessages?.(pageMessages) ?? [];
|
|
1489
|
+
if (volatileMessages.length > 0) {
|
|
1490
|
+
decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
|
|
1491
|
+
volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
|
|
1358
1492
|
}
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
|
|
1493
|
+
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1494
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1495
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1496
|
+
const hasMore = result.has_more === true;
|
|
1497
|
+
const volatileHasMore = result.volatile_has_more === true;
|
|
1498
|
+
const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
|
|
1499
|
+
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 ?? '')}`);
|
|
1500
|
+
for (const msg of messages) {
|
|
1501
|
+
client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
|
|
1363
1502
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1503
|
+
const seqs = messages
|
|
1504
|
+
.map((msg) => Number(msg.seq ?? 0))
|
|
1505
|
+
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1506
|
+
const pageContigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
1507
|
+
const pageTrackerSnapshot = ns && typeof client._seqTracker.snapshotNamespace === 'function'
|
|
1508
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
1509
|
+
: null;
|
|
1510
|
+
let pageMaxSeq = nextAfterSeq;
|
|
1511
|
+
if (seqs.length > 0) {
|
|
1512
|
+
pageMaxSeq = Math.max(...seqs);
|
|
1371
1513
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1514
|
+
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
1515
|
+
const deferredKeyFetches = new Map();
|
|
1516
|
+
let blockedSeq = 0;
|
|
1517
|
+
const pageDecryptedBefore = decrypted.length;
|
|
1518
|
+
for (const msg of messages) {
|
|
1519
|
+
const seq = Number(msg.seq ?? 0);
|
|
1520
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
1521
|
+
continue;
|
|
1522
|
+
const version = String(msg.version ?? 'v2');
|
|
1523
|
+
if (version === 'v1') {
|
|
1524
|
+
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
1525
|
+
const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
|
|
1526
|
+
const payloadType = isJsonObject(legacyPayload)
|
|
1527
|
+
? String(legacyPayload.type ?? '').trim()
|
|
1528
|
+
: '';
|
|
1529
|
+
if (legacyPayload !== undefined && legacyPayload !== null
|
|
1530
|
+
&& !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
|
|
1531
|
+
const v1Msg = {
|
|
1532
|
+
message_id: String(msg.message_id ?? ''),
|
|
1533
|
+
from: String(msg.from_aid ?? ''),
|
|
1534
|
+
to: String(legacy.to ?? client._aid ?? ''),
|
|
1535
|
+
seq: msg.seq,
|
|
1536
|
+
type: String(msg.type ?? ''),
|
|
1537
|
+
timestamp: msg.t_server,
|
|
1538
|
+
payload: legacyPayload,
|
|
1539
|
+
encrypted: false,
|
|
1540
|
+
};
|
|
1541
|
+
const direction = String(msg.direction ?? '').trim();
|
|
1542
|
+
if (direction)
|
|
1543
|
+
v1Msg.direction = direction;
|
|
1544
|
+
else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
|
|
1545
|
+
v1Msg.direction = 'inbound';
|
|
1546
|
+
}
|
|
1547
|
+
if (msg.sender_device_id !== undefined)
|
|
1548
|
+
v1Msg.sender_device_id = msg.sender_device_id;
|
|
1549
|
+
attachGatewayProximity(v1Msg, msg);
|
|
1550
|
+
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
1551
|
+
if (ns) {
|
|
1552
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
|
|
1556
|
+
}
|
|
1557
|
+
decrypted.push(v1Msg);
|
|
1558
|
+
client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
|
|
1559
|
+
}
|
|
1560
|
+
else if (isJsonObject(legacyPayload)
|
|
1561
|
+
&& ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
|
|
1562
|
+
const deferStatus = {};
|
|
1563
|
+
const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
|
|
1564
|
+
if (deferStatus.deferred) {
|
|
1565
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1566
|
+
if (deferStatus.fromAid) {
|
|
1567
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1568
|
+
deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (plaintext) {
|
|
1572
|
+
if (ns)
|
|
1573
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
1574
|
+
else
|
|
1575
|
+
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
1576
|
+
decrypted.push(plaintext);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
|
|
1581
|
+
}
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (version !== 'v2') {
|
|
1585
|
+
client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const spkId = String(msg.spk_id ?? '');
|
|
1589
|
+
if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
|
|
1590
|
+
client._v2Session.trackOldSPKMaxSeq(spkId, seq);
|
|
1591
|
+
}
|
|
1592
|
+
const deferStatus = {};
|
|
1593
|
+
const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
|
|
1594
|
+
if (deferStatus.deferred) {
|
|
1595
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1596
|
+
}
|
|
1597
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1598
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1599
|
+
deferredKeyFetches.set(key, {
|
|
1600
|
+
fromAid: deferStatus.fromAid,
|
|
1601
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1602
|
+
groupId: deferStatus.groupId ?? '',
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
if (plaintext === null) {
|
|
1606
|
+
client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (ns) {
|
|
1610
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
1611
|
+
}
|
|
1612
|
+
else {
|
|
1613
|
+
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
1614
|
+
}
|
|
1615
|
+
decrypted.push(plaintext);
|
|
1616
|
+
client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
|
|
1375
1617
|
}
|
|
1618
|
+
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
1619
|
+
const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
|
|
1620
|
+
const serverAckSeq = parsedServerAckSeq ?? 0;
|
|
1621
|
+
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
1376
1622
|
if (ns) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
let contigAdvanced = false;
|
|
1393
|
-
try {
|
|
1394
|
-
if (commitTarget > pageContigBefore) {
|
|
1395
|
-
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1396
|
-
if (retentionFloor > pageContigBefore) {
|
|
1397
|
-
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
|
|
1623
|
+
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
|
|
1624
|
+
let ackSeq = pageContigBefore;
|
|
1625
|
+
let contigAdvanced = false;
|
|
1626
|
+
try {
|
|
1627
|
+
if (commitTarget > pageContigBefore) {
|
|
1628
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1629
|
+
if (retentionFloor > pageContigBefore) {
|
|
1630
|
+
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
|
|
1631
|
+
}
|
|
1632
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1633
|
+
client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
|
|
1634
|
+
}
|
|
1635
|
+
if (seqs.length > 0) {
|
|
1636
|
+
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1637
|
+
}
|
|
1398
1638
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1639
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1640
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1641
|
+
if (contigAdvanced) {
|
|
1642
|
+
await client._commitSeqTrackerState(ns);
|
|
1643
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1644
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1401
1645
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1646
|
+
}
|
|
1647
|
+
catch (exc) {
|
|
1648
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1649
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1404
1650
|
}
|
|
1651
|
+
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1652
|
+
throw exc;
|
|
1405
1653
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1411
|
-
this.clearForwardCursor(ns, ackSeq);
|
|
1654
|
+
if (contigAdvanced)
|
|
1655
|
+
await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
|
|
1656
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1657
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1412
1658
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1659
|
+
if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
|
|
1660
|
+
this.recordForwardCursor(ns, serverAckSeq, ackSeq);
|
|
1661
|
+
}
|
|
1662
|
+
// 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
|
|
1663
|
+
// 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
|
|
1664
|
+
const knownServerAckSeq = hasServerAckSeq
|
|
1665
|
+
? serverAckSeq
|
|
1666
|
+
: (pageCount === 1 ? deferredServerCursor : 0);
|
|
1667
|
+
const ackNeeded = ackSeq > 0
|
|
1668
|
+
&& ackSeq > lastAutoAckSeq
|
|
1669
|
+
&& ((hasServerAckSeq && ackSeq > serverAckSeq)
|
|
1670
|
+
|| (contigAdvanced && ackSeq > knownServerAckSeq));
|
|
1671
|
+
if (ackNeeded) {
|
|
1672
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1673
|
+
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
1674
|
+
const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
|
|
1675
|
+
if (canContinuePage && !pipelineRunning) {
|
|
1676
|
+
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
1677
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1678
|
+
client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
|
|
1679
|
+
}
|
|
1680
|
+
else {
|
|
1681
|
+
client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
1682
|
+
await this.confirmForwardP2PAck(ackSeq);
|
|
1683
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1684
|
+
}
|
|
1417
1685
|
}
|
|
1418
|
-
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1419
|
-
throw exc;
|
|
1420
|
-
}
|
|
1421
|
-
if (contigAdvanced)
|
|
1422
|
-
await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
|
|
1423
|
-
for (const fetch of deferredKeyFetches.values()) {
|
|
1424
|
-
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1425
|
-
}
|
|
1426
|
-
if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
|
|
1427
|
-
this.recordForwardCursor(ns, serverAckSeq, ackSeq);
|
|
1428
1686
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
? serverAckSeq
|
|
1433
|
-
: (pageCount === 1 ? deferredServerCursor : 0);
|
|
1434
|
-
const ackNeeded = ackSeq > 0
|
|
1435
|
-
&& ackSeq > lastAutoAckSeq
|
|
1436
|
-
&& ((hasServerAckSeq && ackSeq > serverAckSeq)
|
|
1437
|
-
|| (contigAdvanced && ackSeq > knownServerAckSeq));
|
|
1438
|
-
if (ackNeeded) {
|
|
1439
|
-
this.recordForwardAck(ns, ackSeq);
|
|
1440
|
-
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
1441
|
-
const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
|
|
1442
|
-
if (canContinuePage) {
|
|
1443
|
-
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
1444
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1445
|
-
client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
|
|
1687
|
+
if (!ns) {
|
|
1688
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1689
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1446
1690
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1691
|
+
}
|
|
1692
|
+
if (ns)
|
|
1693
|
+
client._delivery.syncProgress(ns, {
|
|
1694
|
+
rawCount: messages.length,
|
|
1695
|
+
pulledCount: decrypted.length - pageDecryptedBefore,
|
|
1696
|
+
remaining: result.remaining,
|
|
1697
|
+
hasMore: hasMoreField ? hasMore : undefined,
|
|
1698
|
+
currentSeq: client._seqTracker.getContiguousSeq(ns),
|
|
1699
|
+
targetSeq: serverHead,
|
|
1700
|
+
});
|
|
1701
|
+
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
1702
|
+
const rawCount = messages.length;
|
|
1703
|
+
if (blockedSeq > 0)
|
|
1704
|
+
break;
|
|
1705
|
+
const shouldContinuePersistent = shouldContinueForwardPage({
|
|
1706
|
+
rawCount,
|
|
1707
|
+
nextAfterSeq,
|
|
1708
|
+
nextAfter,
|
|
1709
|
+
limit,
|
|
1710
|
+
hasMoreField,
|
|
1711
|
+
hasMore,
|
|
1712
|
+
serverHead,
|
|
1713
|
+
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
|
|
1714
|
+
pendingAckSeq,
|
|
1715
|
+
});
|
|
1716
|
+
const shouldContinueForPipeline = shouldContinueForwardPage({
|
|
1717
|
+
rawCount,
|
|
1718
|
+
nextAfterSeq,
|
|
1719
|
+
nextAfter,
|
|
1720
|
+
limit,
|
|
1721
|
+
hasMoreField,
|
|
1722
|
+
hasMore,
|
|
1723
|
+
serverHead,
|
|
1724
|
+
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
|
|
1725
|
+
pendingAckSeq: 0,
|
|
1726
|
+
});
|
|
1727
|
+
const shouldContinueVolatile = (hasMore || volatileHasMore)
|
|
1728
|
+
&& volatileMessages.length > 0
|
|
1729
|
+
&& Boolean(volatileCursor)
|
|
1730
|
+
&& volatileCursor !== requestedVolatileCursor;
|
|
1731
|
+
if (!shouldContinuePersistent && !shouldContinueVolatile)
|
|
1732
|
+
break;
|
|
1733
|
+
if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
|
|
1734
|
+
if (pendingAckSeq > 0) {
|
|
1735
|
+
await this.confirmForwardP2PAck(pendingAckSeq);
|
|
1736
|
+
pendingAckSeq = 0;
|
|
1451
1737
|
}
|
|
1738
|
+
startPipeline(nextAfter, volatileCursor);
|
|
1452
1739
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1740
|
+
if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1741
|
+
const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1742
|
+
after_seq: nextAfter, force: opts?.force === true, limit,
|
|
1743
|
+
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1744
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1745
|
+
pullGateKey = nextKey;
|
|
1457
1746
|
}
|
|
1747
|
+
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1748
|
+
if (tailDelayMs > 0)
|
|
1749
|
+
await v2Sleep(tailDelayMs);
|
|
1750
|
+
nextAfterSeq = nextAfter;
|
|
1458
1751
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
pulledCount: decrypted.length - pageDecryptedBefore,
|
|
1463
|
-
remaining: result.remaining,
|
|
1464
|
-
hasMore: hasMoreField ? hasMore : undefined,
|
|
1465
|
-
currentSeq: client._seqTracker.getContiguousSeq(ns),
|
|
1466
|
-
targetSeq: serverHead,
|
|
1467
|
-
});
|
|
1468
|
-
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
1469
|
-
const rawCount = messages.length;
|
|
1470
|
-
if (blockedSeq > 0)
|
|
1471
|
-
break;
|
|
1472
|
-
const shouldContinue = shouldContinueForwardPage({
|
|
1473
|
-
rawCount,
|
|
1474
|
-
nextAfterSeq,
|
|
1475
|
-
nextAfter,
|
|
1476
|
-
limit,
|
|
1477
|
-
hasMoreField,
|
|
1478
|
-
hasMore,
|
|
1479
|
-
serverHead,
|
|
1480
|
-
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
|
|
1481
|
-
pendingAckSeq,
|
|
1482
|
-
});
|
|
1483
|
-
if (!shouldContinue)
|
|
1484
|
-
break;
|
|
1485
|
-
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1486
|
-
const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1487
|
-
after_seq: nextAfter, force: opts?.force === true, limit,
|
|
1488
|
-
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1489
|
-
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1490
|
-
pullGateKey = nextKey;
|
|
1491
|
-
}
|
|
1492
|
-
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1493
|
-
if (tailDelayMs > 0)
|
|
1494
|
-
await v2Sleep(tailDelayMs);
|
|
1495
|
-
nextAfterSeq = nextAfter;
|
|
1752
|
+
}
|
|
1753
|
+
finally {
|
|
1754
|
+
await stopPipeline();
|
|
1496
1755
|
}
|
|
1497
1756
|
if (pageCount >= maxPages) {
|
|
1498
1757
|
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
@@ -1518,6 +1777,73 @@ export class V2E2EECoordinator {
|
|
|
1518
1777
|
throw error;
|
|
1519
1778
|
}
|
|
1520
1779
|
}
|
|
1780
|
+
async publishV2VolatileMessages(messages, source, advanceCursor) {
|
|
1781
|
+
const client = this.client;
|
|
1782
|
+
if (!client._v2Session)
|
|
1783
|
+
return [];
|
|
1784
|
+
const processed = [];
|
|
1785
|
+
const plaintextMessages = [];
|
|
1786
|
+
let complete = true;
|
|
1787
|
+
for (const rawMessage of messages) {
|
|
1788
|
+
const claim = client._delivery.beginVolatileDelivery(rawMessage);
|
|
1789
|
+
if (claim === 'seen') {
|
|
1790
|
+
processed.push(rawMessage);
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
if (claim === 'inflight') {
|
|
1794
|
+
complete = false;
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
let plaintext;
|
|
1798
|
+
try {
|
|
1799
|
+
if (this.isEncryptedPushMessage(rawMessage)) {
|
|
1800
|
+
plaintext = await client._decryptV2Message(rawMessage, true, true, true, true, undefined, false, source);
|
|
1801
|
+
}
|
|
1802
|
+
else {
|
|
1803
|
+
const legacy = isJsonObject(rawMessage.legacy_v1)
|
|
1804
|
+
? rawMessage.legacy_v1
|
|
1805
|
+
: {};
|
|
1806
|
+
plaintext = {
|
|
1807
|
+
message_id: String(rawMessage.message_id ?? ''),
|
|
1808
|
+
from: String(rawMessage.from_aid ?? rawMessage.from ?? ''),
|
|
1809
|
+
to: String(legacy.to ?? rawMessage.to ?? client._aid ?? ''),
|
|
1810
|
+
type: String(rawMessage.type ?? ''),
|
|
1811
|
+
timestamp: rawMessage.t_server ?? rawMessage.timestamp,
|
|
1812
|
+
payload: legacy.payload !== undefined ? legacy.payload : rawMessage.payload,
|
|
1813
|
+
encrypted: false,
|
|
1814
|
+
};
|
|
1815
|
+
for (const key of ['context', 'protected_headers', 'headers', 'client_signature']) {
|
|
1816
|
+
if (Object.prototype.hasOwnProperty.call(rawMessage, key))
|
|
1817
|
+
plaintext[key] = rawMessage[key];
|
|
1818
|
+
}
|
|
1819
|
+
if (rawMessage.sender_device_id !== undefined)
|
|
1820
|
+
plaintext.sender_device_id = rawMessage.sender_device_id;
|
|
1821
|
+
attachGatewayProximity(plaintext, rawMessage);
|
|
1822
|
+
}
|
|
1823
|
+
if (!plaintext) {
|
|
1824
|
+
complete = false;
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
const publicMessage = client._delivery.publicVolatileMessage(plaintext);
|
|
1828
|
+
for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
|
|
1829
|
+
if (Object.prototype.hasOwnProperty.call(rawMessage, key))
|
|
1830
|
+
publicMessage[key] = rawMessage[key];
|
|
1831
|
+
}
|
|
1832
|
+
const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
|
|
1833
|
+
await client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
1834
|
+
client._delivery.markVolatileSeen(rawMessage);
|
|
1835
|
+
processed.push(rawMessage);
|
|
1836
|
+
plaintextMessages.push(publicMessage);
|
|
1837
|
+
}
|
|
1838
|
+
finally {
|
|
1839
|
+
client._delivery.finishVolatileDelivery(rawMessage);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
if (processed.length > 0) {
|
|
1843
|
+
await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
|
|
1844
|
+
}
|
|
1845
|
+
return plaintextMessages;
|
|
1846
|
+
}
|
|
1521
1847
|
async confirmForwardP2PAck(upToSeq) {
|
|
1522
1848
|
const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
|
|
1523
1849
|
try {
|
|
@@ -1533,28 +1859,41 @@ export class V2E2EECoordinator {
|
|
|
1533
1859
|
throw exc;
|
|
1534
1860
|
}
|
|
1535
1861
|
}
|
|
1536
|
-
async ackV2(upToSeq) {
|
|
1537
|
-
return this.ackV2WithConfirmation(upToSeq, false);
|
|
1862
|
+
async ackV2(upToSeq, opts) {
|
|
1863
|
+
return this.ackV2WithConfirmation(upToSeq, false, opts);
|
|
1538
1864
|
}
|
|
1539
1865
|
async ackV2ForForward(upToSeq) {
|
|
1540
1866
|
return this.ackV2WithConfirmation(upToSeq, true);
|
|
1541
1867
|
}
|
|
1542
|
-
async ackV2WithConfirmation(upToSeq, requireServerWatermark) {
|
|
1868
|
+
async ackV2WithConfirmation(upToSeq, requireServerWatermark, opts) {
|
|
1543
1869
|
const client = this.client;
|
|
1544
1870
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1545
|
-
|
|
1546
|
-
|
|
1871
|
+
const volatileCursors = Array.isArray(opts?.volatileCursors) ? opts?.volatileCursors : [];
|
|
1872
|
+
const volatileIds = Array.isArray(opts?.volatileIds) ? opts?.volatileIds : [];
|
|
1873
|
+
let seq = Number(upToSeq ?? ((volatileCursors.length > 0 || volatileIds.length > 0)
|
|
1874
|
+
? 0
|
|
1875
|
+
: (ns ? client._seqTracker.getContiguousSeq(ns) : 0)));
|
|
1876
|
+
if ((!Number.isFinite(seq) || seq <= 0) && volatileCursors.length === 0 && volatileIds.length === 0) {
|
|
1547
1877
|
client._clientLog.debug(`message.v2.ack skipped: ns=${ns || '<none>'}, up_to_seq=${String(upToSeq ?? '')}`);
|
|
1548
1878
|
return { acked: 0 };
|
|
1549
1879
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1880
|
+
if (Number.isFinite(seq) && seq > 0) {
|
|
1881
|
+
seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
|
|
1882
|
+
}
|
|
1883
|
+
else {
|
|
1884
|
+
seq = 0;
|
|
1885
|
+
}
|
|
1886
|
+
if (seq <= 0 && volatileCursors.length === 0 && volatileIds.length === 0)
|
|
1552
1887
|
return { acked: 0 };
|
|
1553
1888
|
const inlineAckGeneration = client._delivery.captureP2PInlineAckGeneration();
|
|
1554
1889
|
const v2SessionAtStart = client._v2Session;
|
|
1555
1890
|
const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
|
|
1556
1891
|
client._clientLog.debug(`message.v2.ack send: ns=${ns || '<none>'}, up_to_seq=${seq}`);
|
|
1557
|
-
const raw = await client._callRawV2Rpc('message.v2.ack', {
|
|
1892
|
+
const raw = await client._callRawV2Rpc('message.v2.ack', {
|
|
1893
|
+
...(seq > 0 ? { up_to_seq: seq } : {}),
|
|
1894
|
+
...(volatileCursors.length > 0 ? { volatile_cursors: volatileCursors } : {}),
|
|
1895
|
+
...(volatileIds.length > 0 ? { volatile_ids: volatileIds } : {}),
|
|
1896
|
+
});
|
|
1558
1897
|
const result = isJsonObject(raw)
|
|
1559
1898
|
? { ...raw }
|
|
1560
1899
|
: { result: raw };
|
|
@@ -1566,7 +1905,7 @@ export class V2E2EECoordinator {
|
|
|
1566
1905
|
}
|
|
1567
1906
|
result.ack_seq = actualAckSeq;
|
|
1568
1907
|
result.success = true;
|
|
1569
|
-
if (inlineAckGeneration !== null) {
|
|
1908
|
+
if (seq > 0 && inlineAckGeneration !== null) {
|
|
1570
1909
|
client._delivery.onP2PV2AckSucceeded(actualAckSeq, inlineAckGeneration);
|
|
1571
1910
|
}
|
|
1572
1911
|
if (v2SessionAtStart
|
|
@@ -1664,7 +2003,6 @@ export class V2E2EECoordinator {
|
|
|
1664
2003
|
let epoch = 0;
|
|
1665
2004
|
let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
|
|
1666
2005
|
let wrapPolicy = normalizeV2WrapPolicy(undefined);
|
|
1667
|
-
let usedCachedBootstrap = false;
|
|
1668
2006
|
const cached = useCache ? this.getBootstrapCacheEntry(cacheKey) : undefined;
|
|
1669
2007
|
if (cached && Date.now() - Number(cached.cachedAt ?? 0) < V2_BOOTSTRAP_TTL_MS) {
|
|
1670
2008
|
allDevices = (cached.devices ?? []);
|
|
@@ -1672,7 +2010,6 @@ export class V2E2EECoordinator {
|
|
|
1672
2010
|
epoch = Number(cached.epoch ?? 0) || 0;
|
|
1673
2011
|
stateCommitment = cached.stateCommitment ?? stateCommitment;
|
|
1674
2012
|
wrapPolicy = cached.wrapPolicy ?? wrapPolicy;
|
|
1675
|
-
usedCachedBootstrap = true;
|
|
1676
2013
|
client._clientLog.debug(`group.v2.bootstrap cache hit: group=${groupId}, devices=${allDevices.length}, audit=${auditRecipientsRaw.length}, epoch=${epoch}, state_version=${stateCommitment.state_version}`);
|
|
1677
2014
|
}
|
|
1678
2015
|
else {
|
|
@@ -1736,13 +2073,10 @@ export class V2E2EECoordinator {
|
|
|
1736
2073
|
targets.push(target);
|
|
1737
2074
|
}
|
|
1738
2075
|
if (targets.length === 0) {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
});
|
|
1744
|
-
}
|
|
1745
|
-
throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
|
|
2076
|
+
throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
|
|
2077
|
+
code: V2_GROUP_STALE_BOOTSTRAP_CODE,
|
|
2078
|
+
data: { expected_peer_aids: [] },
|
|
2079
|
+
});
|
|
1746
2080
|
}
|
|
1747
2081
|
for (const dev of auditRecipientsRaw) {
|
|
1748
2082
|
const target = await client._v2BuildTargetFromDevice({
|
|
@@ -1761,13 +2095,10 @@ export class V2E2EECoordinator {
|
|
|
1761
2095
|
targetSet = this.cloneGroupTargetSet(targetSet);
|
|
1762
2096
|
}
|
|
1763
2097
|
if (targetSet.targets.length === 0) {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
});
|
|
1769
|
-
}
|
|
1770
|
-
throw new E2EEError(`V2 group: no target devices for group ${groupId}`);
|
|
2098
|
+
throw new E2EEError(`V2 group: no target devices for group ${groupId}; bootstrap may be stale`, {
|
|
2099
|
+
code: V2_GROUP_STALE_BOOTSTRAP_CODE,
|
|
2100
|
+
data: { expected_peer_aids: [] },
|
|
2101
|
+
});
|
|
1771
2102
|
}
|
|
1772
2103
|
const envelope = encryptGroupMessage(session.getSenderIdentity(), groupId, epoch, targetSet.targets, opts.payload, {
|
|
1773
2104
|
messageId: opts.messageId,
|
|
@@ -1958,91 +2289,208 @@ export class V2E2EECoordinator {
|
|
|
1958
2289
|
let lastAutoAckSeq = 0;
|
|
1959
2290
|
let pendingAckSeq = deferredForwardAck;
|
|
1960
2291
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2292
|
+
let pipelineRunning = false;
|
|
2293
|
+
let pipelineError = null;
|
|
2294
|
+
const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
|
|
2295
|
+
let pipelineProducer = null;
|
|
2296
|
+
let pipelineStopped = false;
|
|
2297
|
+
const startPipeline = (startAfterSeq) => {
|
|
2298
|
+
if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
|
|
2299
|
+
return;
|
|
2300
|
+
pipelineRunning = true;
|
|
2301
|
+
pipelineProducer = (async () => {
|
|
2302
|
+
let producerAfterSeq = startAfterSeq;
|
|
2303
|
+
for (let produced = pageCount; produced < maxPages; produced += 1) {
|
|
2304
|
+
if (pipelineStopped)
|
|
2305
|
+
break;
|
|
2306
|
+
const result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
2307
|
+
group_id: wireGroupId,
|
|
2308
|
+
after_seq: producerAfterSeq,
|
|
2309
|
+
limit,
|
|
2310
|
+
...cursorParams,
|
|
2311
|
+
...(opts?.force ? { force: true } : {}),
|
|
2312
|
+
}, groupAid), pullGateKey);
|
|
2313
|
+
if (pipelineStopped)
|
|
2314
|
+
break;
|
|
2315
|
+
const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
|
|
2316
|
+
const enqueued = await pipelineQueue.put({
|
|
2317
|
+
result,
|
|
2318
|
+
afterSeq: producerAfterSeq,
|
|
2319
|
+
ackUpToSeq: 0,
|
|
2320
|
+
inlineAckGeneration: null,
|
|
2321
|
+
requestedVolatileCursor: '',
|
|
2322
|
+
weight: pageMessages.length,
|
|
2323
|
+
}, pageMessages.length);
|
|
2324
|
+
if (!enqueued || pipelineStopped)
|
|
2325
|
+
break;
|
|
2326
|
+
const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
|
|
2327
|
+
const seqs = messages
|
|
2328
|
+
.map((msg) => Number(msg.seq ?? 0))
|
|
2329
|
+
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
2330
|
+
const nextAfter = Math.max(contiguousPullSeq(producerAfterSeq, seqs, result.contiguous_seq), producerAfterSeq);
|
|
2331
|
+
const cursor = isJsonObject(result.cursor)
|
|
2332
|
+
? result.cursor : null;
|
|
2333
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
2334
|
+
rawCount: messages.length,
|
|
2335
|
+
nextAfterSeq: producerAfterSeq,
|
|
2336
|
+
nextAfter,
|
|
2337
|
+
limit,
|
|
2338
|
+
hasMoreField: Object.prototype.hasOwnProperty.call(result, 'has_more'),
|
|
2339
|
+
hasMore: result.has_more === true,
|
|
2340
|
+
serverHead: Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0),
|
|
2341
|
+
observedUpper: Math.max(nextAfter, client._seqTracker.getMaxSeenSeq(ns)),
|
|
2342
|
+
pendingAckSeq: 0,
|
|
2343
|
+
});
|
|
2344
|
+
if (!shouldContinue)
|
|
2345
|
+
break;
|
|
2346
|
+
producerAfterSeq = nextAfter;
|
|
2347
|
+
}
|
|
2348
|
+
})().catch((error) => {
|
|
2349
|
+
pipelineError = error;
|
|
2350
|
+
}).finally(() => {
|
|
2351
|
+
pipelineQueue.close();
|
|
2352
|
+
});
|
|
2353
|
+
};
|
|
2354
|
+
const stopPipeline = async () => {
|
|
2355
|
+
pipelineStopped = true;
|
|
2356
|
+
pipelineQueue.close();
|
|
2357
|
+
if (pipelineProducer) {
|
|
2358
|
+
const producer = pipelineProducer;
|
|
2359
|
+
pipelineProducer = null;
|
|
2360
|
+
await producer;
|
|
2361
|
+
}
|
|
2362
|
+
pipelineRunning = false;
|
|
2363
|
+
};
|
|
2364
|
+
try {
|
|
2365
|
+
while (pageCount < maxPages) {
|
|
2366
|
+
let pipelinePage;
|
|
2367
|
+
if (pipelineRunning) {
|
|
2368
|
+
pipelinePage = await pipelineQueue.take();
|
|
2369
|
+
if (!pipelinePage) {
|
|
2370
|
+
pipelineRunning = false;
|
|
2371
|
+
if (pipelineError)
|
|
2372
|
+
throw pipelineError;
|
|
2373
|
+
break;
|
|
1981
2374
|
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2375
|
+
}
|
|
2376
|
+
pageCount += 1;
|
|
2377
|
+
const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
|
|
2378
|
+
const inlineAckGeneration = pipelinePage?.inlineAckGeneration ?? (ackUpToSeq > 0
|
|
2379
|
+
? client._delivery.captureGroupInlineAckGeneration()
|
|
2380
|
+
: null);
|
|
2381
|
+
client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
|
|
2382
|
+
let result;
|
|
2383
|
+
if (pipelinePage) {
|
|
2384
|
+
nextAfterSeq = pipelinePage.afterSeq;
|
|
2385
|
+
result = pipelinePage.result;
|
|
1985
2386
|
}
|
|
1986
2387
|
else {
|
|
1987
|
-
client.
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2388
|
+
result = await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
2389
|
+
group_id: wireGroupId,
|
|
2390
|
+
after_seq: nextAfterSeq,
|
|
2391
|
+
limit,
|
|
2392
|
+
...cursorParams,
|
|
2393
|
+
...(opts?.force ? { force: true } : {}),
|
|
2394
|
+
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
2395
|
+
}, groupAid), pullGateKey);
|
|
1991
2396
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
const seqs = messages
|
|
2008
|
-
.map((msg) => Number(msg.seq ?? 0))
|
|
2009
|
-
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
2010
|
-
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
2011
|
-
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
2012
|
-
? client._seqTracker.snapshotNamespace(ns)
|
|
2013
|
-
: null;
|
|
2014
|
-
let pageMaxSeq = nextAfterSeq;
|
|
2015
|
-
if (seqs.length > 0) {
|
|
2016
|
-
pageMaxSeq = Math.max(...seqs);
|
|
2017
|
-
}
|
|
2018
|
-
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
2019
|
-
const deferredKeyFetches = new Map();
|
|
2020
|
-
let blockedSeq = 0;
|
|
2021
|
-
const pageDecryptedBefore = decrypted.length;
|
|
2022
|
-
for (const msg of messages) {
|
|
2023
|
-
const seq = Number(msg.seq ?? 0);
|
|
2024
|
-
if (!Number.isFinite(seq) || seq <= 0)
|
|
2025
|
-
continue;
|
|
2026
|
-
const version = String(msg.version ?? 'v2');
|
|
2027
|
-
if (version === 'v1') {
|
|
2028
|
-
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
2029
|
-
const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
|
|
2030
|
-
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
2031
|
-
const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
|
|
2032
|
-
// 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
|
|
2033
|
-
if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
|
|
2034
|
-
&& client._delivery.recallEventFromGroupMessage(msg)) {
|
|
2035
|
-
await client._delivery.publishGroupRecallTombstone(gid, seq, {
|
|
2036
|
-
...msg,
|
|
2037
|
-
group_id: gid,
|
|
2038
|
-
group_aid: eventGroupAid,
|
|
2039
|
-
}, 'pull');
|
|
2040
|
-
client._markPublishedSeq(ns, seq);
|
|
2041
|
-
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
2042
|
-
continue;
|
|
2397
|
+
if (ackUpToSeq > 0) {
|
|
2398
|
+
const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
|
|
2399
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
2400
|
+
if (inlineAckGeneration !== null) {
|
|
2401
|
+
client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
|
|
2402
|
+
}
|
|
2403
|
+
pendingAckSeq = 0;
|
|
2404
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
2405
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
2406
|
+
}
|
|
2407
|
+
else {
|
|
2408
|
+
client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
2409
|
+
await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
|
|
2410
|
+
pendingAckSeq = 0;
|
|
2411
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
2043
2412
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2413
|
+
}
|
|
2414
|
+
const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
2415
|
+
if (resultGroupAid)
|
|
2416
|
+
groupAid = resultGroupAid;
|
|
2417
|
+
const eventGroupAid = resultGroupAid || groupAid || gid;
|
|
2418
|
+
const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
|
|
2419
|
+
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
2420
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
2421
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
2422
|
+
const hasMore = result.has_more === true;
|
|
2423
|
+
const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
|
|
2424
|
+
client._clientLog.debug(`group.v2.pull page response: group=${gid}, page=${pageCount}, raw_count=${messages.length}, stale_count=${Math.max(0, pageMessages.length - messages.length)}, has_more=${String(result.has_more ?? '')}, server_head=${serverHead}, cursor_current=${String(cursor?.current_seq ?? '')}`);
|
|
2425
|
+
for (const msg of messages) {
|
|
2426
|
+
client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
2427
|
+
}
|
|
2428
|
+
const seqs = messages
|
|
2429
|
+
.map((msg) => Number(msg.seq ?? 0))
|
|
2430
|
+
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
2431
|
+
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
2432
|
+
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
2433
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
2434
|
+
: null;
|
|
2435
|
+
let pageMaxSeq = nextAfterSeq;
|
|
2436
|
+
if (seqs.length > 0) {
|
|
2437
|
+
pageMaxSeq = Math.max(...seqs);
|
|
2438
|
+
}
|
|
2439
|
+
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
2440
|
+
const deferredKeyFetches = new Map();
|
|
2441
|
+
let blockedSeq = 0;
|
|
2442
|
+
const pageDecryptedBefore = decrypted.length;
|
|
2443
|
+
for (const msg of messages) {
|
|
2444
|
+
const seq = Number(msg.seq ?? 0);
|
|
2445
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
2446
|
+
continue;
|
|
2447
|
+
const version = String(msg.version ?? 'v2');
|
|
2448
|
+
if (version === 'v1') {
|
|
2449
|
+
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
2450
|
+
const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
|
|
2451
|
+
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
2452
|
+
const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
|
|
2453
|
+
// 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
|
|
2454
|
+
if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
|
|
2455
|
+
&& client._delivery.recallEventFromGroupMessage(msg)) {
|
|
2456
|
+
await client._delivery.publishGroupRecallTombstone(gid, seq, {
|
|
2457
|
+
...msg,
|
|
2458
|
+
group_id: gid,
|
|
2459
|
+
group_aid: eventGroupAid,
|
|
2460
|
+
}, 'pull');
|
|
2461
|
+
client._markPublishedSeq(ns, seq);
|
|
2462
|
+
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
if (payloadObj) {
|
|
2466
|
+
if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
|
|
2467
|
+
let v1Msg = {
|
|
2468
|
+
message_id: String(msg.message_id ?? ''),
|
|
2469
|
+
from: String(msg.from_aid ?? ''),
|
|
2470
|
+
group_id: gid,
|
|
2471
|
+
group_aid: eventGroupAid,
|
|
2472
|
+
seq: msg.seq,
|
|
2473
|
+
type: String(msg.type ?? ''),
|
|
2474
|
+
timestamp: msg.t_server,
|
|
2475
|
+
payload,
|
|
2476
|
+
encrypted: false,
|
|
2477
|
+
};
|
|
2478
|
+
if (msg.sender_device_id !== undefined)
|
|
2479
|
+
v1Msg.sender_device_id = msg.sender_device_id;
|
|
2480
|
+
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
2481
|
+
attachGatewayProximity(v1Msg, msg);
|
|
2482
|
+
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
2483
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
2484
|
+
}
|
|
2485
|
+
else {
|
|
2486
|
+
client._markPublishedSeq(ns, seq);
|
|
2487
|
+
}
|
|
2488
|
+
decrypted.push(v1Msg);
|
|
2489
|
+
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
else if (payload !== undefined && payload !== null) {
|
|
2046
2494
|
let v1Msg = {
|
|
2047
2495
|
message_id: String(msg.message_id ?? ''),
|
|
2048
2496
|
from: String(msg.from_aid ?? ''),
|
|
@@ -2054,209 +2502,208 @@ export class V2E2EECoordinator {
|
|
|
2054
2502
|
payload,
|
|
2055
2503
|
encrypted: false,
|
|
2056
2504
|
};
|
|
2505
|
+
if (msg.sender_device_id !== undefined)
|
|
2506
|
+
v1Msg.sender_device_id = msg.sender_device_id;
|
|
2057
2507
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
2058
2508
|
attachGatewayProximity(v1Msg, msg);
|
|
2059
|
-
|
|
2060
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
2061
|
-
}
|
|
2062
|
-
else {
|
|
2063
|
-
client._markPublishedSeq(ns, seq);
|
|
2064
|
-
}
|
|
2509
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
2065
2510
|
decrypted.push(v1Msg);
|
|
2066
2511
|
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
2067
2512
|
continue;
|
|
2068
2513
|
}
|
|
2514
|
+
if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
|
|
2515
|
+
const deferStatus = {};
|
|
2516
|
+
const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
|
|
2517
|
+
if (deferStatus.deferred) {
|
|
2518
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
2519
|
+
if (deferStatus.fromAid) {
|
|
2520
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
2521
|
+
deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
if (plaintext) {
|
|
2525
|
+
plaintext.group_id = gid;
|
|
2526
|
+
plaintext.group_aid = eventGroupAid;
|
|
2527
|
+
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
2528
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
2529
|
+
}
|
|
2530
|
+
else {
|
|
2531
|
+
client._markPublishedSeq(ns, seq);
|
|
2532
|
+
}
|
|
2533
|
+
decrypted.push(plaintext);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
else {
|
|
2537
|
+
client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
|
|
2538
|
+
}
|
|
2539
|
+
continue;
|
|
2069
2540
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
message_id: String(msg.message_id ?? ''),
|
|
2073
|
-
from: String(msg.from_aid ?? ''),
|
|
2074
|
-
group_id: gid,
|
|
2075
|
-
group_aid: eventGroupAid,
|
|
2076
|
-
seq: msg.seq,
|
|
2077
|
-
type: String(msg.type ?? ''),
|
|
2078
|
-
timestamp: msg.t_server,
|
|
2079
|
-
payload,
|
|
2080
|
-
encrypted: false,
|
|
2081
|
-
};
|
|
2082
|
-
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
2083
|
-
attachGatewayProximity(v1Msg, msg);
|
|
2084
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
2085
|
-
decrypted.push(v1Msg);
|
|
2086
|
-
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
2541
|
+
if (version !== 'v2') {
|
|
2542
|
+
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
2087
2543
|
continue;
|
|
2088
2544
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2545
|
+
const deferStatus = {};
|
|
2546
|
+
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
|
|
2547
|
+
if (deferStatus.deferred) {
|
|
2548
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
2549
|
+
}
|
|
2550
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
2551
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
2552
|
+
deferredKeyFetches.set(key, {
|
|
2553
|
+
fromAid: deferStatus.fromAid,
|
|
2554
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
2555
|
+
groupId: deferStatus.groupId ?? '',
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
if (plaintext === null) {
|
|
2559
|
+
client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
2563
|
+
plaintext.group_id = gid;
|
|
2564
|
+
plaintext.group_aid = eventGroupAid;
|
|
2565
|
+
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
2566
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
2567
|
+
}
|
|
2568
|
+
else {
|
|
2569
|
+
client._markPublishedSeq(ns, seq);
|
|
2570
|
+
}
|
|
2571
|
+
decrypted.push(plaintext);
|
|
2572
|
+
client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
2573
|
+
}
|
|
2574
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
2575
|
+
const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
|
|
2576
|
+
const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
|
|
2577
|
+
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
|
|
2578
|
+
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
2579
|
+
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
2580
|
+
const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
|
|
2581
|
+
let ackSeq = pageContigBefore;
|
|
2582
|
+
let contigAdvanced = false;
|
|
2583
|
+
if (ownsCursor) {
|
|
2584
|
+
try {
|
|
2585
|
+
if (commitTarget > pageContigBefore) {
|
|
2586
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
2587
|
+
if (effectiveFloor > pageContigBefore) {
|
|
2588
|
+
client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
|
|
2097
2589
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
plaintext.group_id = gid;
|
|
2101
|
-
plaintext.group_aid = eventGroupAid;
|
|
2102
|
-
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
2103
|
-
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
2590
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
2591
|
+
client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
|
|
2104
2592
|
}
|
|
2105
|
-
|
|
2106
|
-
client.
|
|
2593
|
+
if (seqs.length > 0) {
|
|
2594
|
+
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
2107
2595
|
}
|
|
2108
|
-
|
|
2596
|
+
}
|
|
2597
|
+
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
2598
|
+
contigAdvanced = ackSeq !== pageContigBefore;
|
|
2599
|
+
if (contigAdvanced) {
|
|
2600
|
+
await client._commitSeqTrackerState(ns);
|
|
2601
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
2602
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
2109
2603
|
}
|
|
2110
2604
|
}
|
|
2111
|
-
|
|
2112
|
-
client.
|
|
2605
|
+
catch (exc) {
|
|
2606
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
2607
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
2608
|
+
}
|
|
2609
|
+
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
2610
|
+
throw exc;
|
|
2113
2611
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
if (version !== 'v2') {
|
|
2117
|
-
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
2118
|
-
continue;
|
|
2119
|
-
}
|
|
2120
|
-
const deferStatus = {};
|
|
2121
|
-
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
|
|
2122
|
-
if (deferStatus.deferred) {
|
|
2123
|
-
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
2124
|
-
}
|
|
2125
|
-
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
2126
|
-
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
2127
|
-
deferredKeyFetches.set(key, {
|
|
2128
|
-
fromAid: deferStatus.fromAid,
|
|
2129
|
-
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
2130
|
-
groupId: deferStatus.groupId ?? '',
|
|
2131
|
-
});
|
|
2612
|
+
if (contigAdvanced)
|
|
2613
|
+
await client._drainOrderedMessages(ns, undefined, false, false, 'pull');
|
|
2132
2614
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
continue;
|
|
2136
|
-
}
|
|
2137
|
-
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
2138
|
-
plaintext.group_id = gid;
|
|
2139
|
-
plaintext.group_aid = eventGroupAid;
|
|
2140
|
-
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
2141
|
-
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
2615
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
2616
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
2142
2617
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2618
|
+
if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
|
|
2619
|
+
this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
|
|
2145
2620
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
if (
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
}
|
|
2165
|
-
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
2166
|
-
client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
|
|
2167
|
-
}
|
|
2168
|
-
if (seqs.length > 0) {
|
|
2169
|
-
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
2173
|
-
contigAdvanced = ackSeq !== pageContigBefore;
|
|
2174
|
-
if (contigAdvanced) {
|
|
2175
|
-
await client._commitSeqTrackerState(ns);
|
|
2176
|
-
if (pageCount === 1 && deferredServerCursor > 0)
|
|
2177
|
-
this.clearForwardCursor(ns, ackSeq);
|
|
2621
|
+
// 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
|
|
2622
|
+
// 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
|
|
2623
|
+
const knownServerCursorSeq = hasServerCursor
|
|
2624
|
+
? cursorCurrentSeq
|
|
2625
|
+
: (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
|
|
2626
|
+
const ackNeeded = ackSeq > 0
|
|
2627
|
+
&& ackSeq > lastAutoAckSeq
|
|
2628
|
+
&& ownsCursor
|
|
2629
|
+
&& ((hasServerCursor && ackSeq > cursorCurrentSeq)
|
|
2630
|
+
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
2631
|
+
if (ackNeeded) {
|
|
2632
|
+
this.recordForwardAck(ns, ackSeq);
|
|
2633
|
+
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
2634
|
+
const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
|
|
2635
|
+
if (canContinuePage && !pipelineRunning) {
|
|
2636
|
+
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
2637
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
2638
|
+
client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
|
|
2178
2639
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2640
|
+
else {
|
|
2641
|
+
client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
2642
|
+
await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
|
|
2643
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
2183
2644
|
}
|
|
2184
|
-
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
2185
|
-
throw exc;
|
|
2186
2645
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
}
|
|
2196
|
-
// 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
|
|
2197
|
-
// 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
|
|
2198
|
-
const knownServerCursorSeq = hasServerCursor
|
|
2199
|
-
? cursorCurrentSeq
|
|
2200
|
-
: (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
|
|
2201
|
-
const ackNeeded = ackSeq > 0
|
|
2202
|
-
&& ackSeq > lastAutoAckSeq
|
|
2203
|
-
&& ownsCursor
|
|
2204
|
-
&& ((hasServerCursor && ackSeq > cursorCurrentSeq)
|
|
2205
|
-
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
2206
|
-
if (ackNeeded) {
|
|
2207
|
-
this.recordForwardAck(ns, ackSeq);
|
|
2646
|
+
client._delivery.syncProgress(ns, {
|
|
2647
|
+
rawCount: messages.length,
|
|
2648
|
+
pulledCount: decrypted.length - pageDecryptedBefore,
|
|
2649
|
+
remaining: result.remaining,
|
|
2650
|
+
hasMore: hasMoreField ? hasMore : undefined,
|
|
2651
|
+
currentSeq: client._seqTracker.getContiguousSeq(ns),
|
|
2652
|
+
targetSeq: serverHead,
|
|
2653
|
+
});
|
|
2208
2654
|
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2655
|
+
if (!ownsCursor)
|
|
2656
|
+
break;
|
|
2657
|
+
const rawCount = messages.length;
|
|
2658
|
+
if (blockedSeq > 0)
|
|
2659
|
+
break;
|
|
2660
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
2661
|
+
rawCount,
|
|
2662
|
+
nextAfterSeq,
|
|
2663
|
+
nextAfter,
|
|
2664
|
+
limit,
|
|
2665
|
+
hasMoreField,
|
|
2666
|
+
hasMore,
|
|
2667
|
+
serverHead,
|
|
2668
|
+
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
|
|
2669
|
+
pendingAckSeq,
|
|
2670
|
+
});
|
|
2671
|
+
const shouldContinueForPipeline = shouldContinueForwardPage({
|
|
2672
|
+
rawCount,
|
|
2673
|
+
nextAfterSeq,
|
|
2674
|
+
nextAfter,
|
|
2675
|
+
limit,
|
|
2676
|
+
hasMoreField,
|
|
2677
|
+
hasMore,
|
|
2678
|
+
serverHead,
|
|
2679
|
+
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
|
|
2680
|
+
pendingAckSeq: 0,
|
|
2681
|
+
});
|
|
2682
|
+
if (!shouldContinue)
|
|
2683
|
+
break;
|
|
2684
|
+
if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
|
|
2685
|
+
if (pendingAckSeq > 0) {
|
|
2686
|
+
await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
|
|
2687
|
+
pendingAckSeq = 0;
|
|
2688
|
+
}
|
|
2689
|
+
startPipeline(nextAfter);
|
|
2214
2690
|
}
|
|
2215
|
-
|
|
2216
|
-
client
|
|
2217
|
-
|
|
2218
|
-
|
|
2691
|
+
if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
2692
|
+
const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
2693
|
+
group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
|
|
2694
|
+
_group_cursor_params: cursorParams,
|
|
2695
|
+
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
2696
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
2697
|
+
pullGateKey = nextKey;
|
|
2219
2698
|
}
|
|
2699
|
+
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
2700
|
+
if (tailDelayMs > 0)
|
|
2701
|
+
await v2Sleep(tailDelayMs);
|
|
2702
|
+
nextAfterSeq = nextAfter;
|
|
2220
2703
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
remaining: result.remaining,
|
|
2225
|
-
hasMore: hasMoreField ? hasMore : undefined,
|
|
2226
|
-
currentSeq: client._seqTracker.getContiguousSeq(ns),
|
|
2227
|
-
targetSeq: serverHead,
|
|
2228
|
-
});
|
|
2229
|
-
const nextAfter = Math.max(pageContiguousSeq, nextAfterSeq);
|
|
2230
|
-
if (!ownsCursor)
|
|
2231
|
-
break;
|
|
2232
|
-
const rawCount = messages.length;
|
|
2233
|
-
if (blockedSeq > 0)
|
|
2234
|
-
break;
|
|
2235
|
-
const shouldContinue = shouldContinueForwardPage({
|
|
2236
|
-
rawCount,
|
|
2237
|
-
nextAfterSeq,
|
|
2238
|
-
nextAfter,
|
|
2239
|
-
limit,
|
|
2240
|
-
hasMoreField,
|
|
2241
|
-
hasMore,
|
|
2242
|
-
serverHead,
|
|
2243
|
-
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
|
|
2244
|
-
pendingAckSeq,
|
|
2245
|
-
});
|
|
2246
|
-
if (!shouldContinue)
|
|
2247
|
-
break;
|
|
2248
|
-
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
2249
|
-
const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
2250
|
-
group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
|
|
2251
|
-
_group_cursor_params: cursorParams,
|
|
2252
|
-
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
2253
|
-
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
2254
|
-
pullGateKey = nextKey;
|
|
2255
|
-
}
|
|
2256
|
-
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
2257
|
-
if (tailDelayMs > 0)
|
|
2258
|
-
await v2Sleep(tailDelayMs);
|
|
2259
|
-
nextAfterSeq = nextAfter;
|
|
2704
|
+
}
|
|
2705
|
+
finally {
|
|
2706
|
+
await stopPipeline();
|
|
2260
2707
|
}
|
|
2261
2708
|
if (pageCount >= maxPages) {
|
|
2262
2709
|
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
@@ -2751,7 +3198,6 @@ export class V2E2EECoordinator {
|
|
|
2751
3198
|
seq: msg.seq,
|
|
2752
3199
|
timestamp: (msg.timestamp ?? msg.t_server),
|
|
2753
3200
|
device_id: msg.device_id,
|
|
2754
|
-
slot_id: msg.slot_id,
|
|
2755
3201
|
_decrypt_error: 'encrypted push payload is not decryptable on raw push path',
|
|
2756
3202
|
_decrypt_stage: 'push_envelope',
|
|
2757
3203
|
};
|
|
@@ -2795,13 +3241,18 @@ export class V2E2EECoordinator {
|
|
|
2795
3241
|
encrypted: true,
|
|
2796
3242
|
e2ee,
|
|
2797
3243
|
};
|
|
2798
|
-
|
|
3244
|
+
const senderDeviceId = String(aad.from_device ?? '').trim();
|
|
3245
|
+
if (senderDeviceId)
|
|
3246
|
+
result.sender_device_id = senderDeviceId;
|
|
3247
|
+
const direction = String(msg.direction ?? '').trim();
|
|
3248
|
+
if (direction)
|
|
3249
|
+
result.direction = direction;
|
|
3250
|
+
else if (fromAid && fromAid !== client._aid)
|
|
3251
|
+
result.direction = 'inbound';
|
|
2799
3252
|
if (msg.t_server !== undefined)
|
|
2800
3253
|
result.t_server = msg.t_server;
|
|
2801
3254
|
if (msg.device_id !== undefined)
|
|
2802
3255
|
result.device_id = msg.device_id;
|
|
2803
|
-
if (msg.slot_id !== undefined)
|
|
2804
|
-
result.slot_id = msg.slot_id;
|
|
2805
3256
|
attachGatewayProximity(result, msg);
|
|
2806
3257
|
if (group) {
|
|
2807
3258
|
result.group_id = (msg.group_id ?? aad.group_id ?? envelope.group_id);
|
|
@@ -2839,10 +3290,10 @@ export class V2E2EECoordinator {
|
|
|
2839
3290
|
}
|
|
2840
3291
|
return await this.publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
|
|
2841
3292
|
}
|
|
2842
|
-
async decryptV2PushMessage(data) {
|
|
3293
|
+
async decryptV2PushMessage(data, deferStatus) {
|
|
2843
3294
|
if (!isJsonObject(data))
|
|
2844
3295
|
return null;
|
|
2845
|
-
return await this.client._decryptV2Message(data, false,
|
|
3296
|
+
return await this.client._decryptV2Message(data, false, true, false, false, deferStatus, false, 'inline_push');
|
|
2846
3297
|
}
|
|
2847
3298
|
handleGroupChangedSpk(data, groupId, action) {
|
|
2848
3299
|
if (!this.client._v2Session || !groupId || !MEMBERSHIP_ACTIONS.has(action))
|