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