@agentunion/fastaun 0.5.6 → 0.5.9
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 +106 -0
- package/_packed_docs/CHANGELOG.md +106 -0
- package/_packed_docs/INDEX.md +12 -11
- package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
- package/_packed_docs/agent.md/SCHEMA.md +83 -58
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
- package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
- package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
- 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 +422 -331
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
- package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
- package/_packed_docs/message-online-push-alignment.md +572 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
- package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
- package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
- package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
- package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
- package/_packed_docs/sdk/INDEX.md +22 -22
- package/_packed_docs/sdk/README.md +6 -5
- package/dist/agent-md-schema.d.ts +16 -0
- package/dist/agent-md-schema.js +109 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +7 -0
- package/dist/agent-md.js +91 -9
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +5 -1
- package/dist/aid-store.js +25 -6
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.js +32 -12
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +65 -13
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +2 -1
- package/dist/cert-utils.js +32 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +36 -7
- package/dist/client/delivery.js +701 -216
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +2 -2
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.d.ts +15 -1
- package/dist/client/lifecycle.js +475 -152
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +24 -6
- package/dist/client/rpc-pipeline.js +372 -131
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -3
- package/dist/client/v2-e2ee.js +376 -93
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -10
- package/dist/client.js +515 -150
- package/dist/client.js.map +1 -1
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +3 -0
- package/dist/discovery.js.map +1 -1
- package/dist/facades.d.ts +23 -1
- package/dist/facades.js +59 -7
- package/dist/facades.js.map +1 -1
- package/dist/group-id.js +2 -17
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/net.d.ts +3 -0
- package/dist/net.js +13 -0
- package/dist/net.js.map +1 -1
- package/dist/seq-tracker.js +6 -1
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +29 -3
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.js +8 -1
- package/dist/transport.js.map +1 -1
- package/dist/validators.js +8 -14
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/dist/client/v2-e2ee.js
CHANGED
|
@@ -18,6 +18,9 @@ function strictWindowSeq(value, field, positive = false) {
|
|
|
18
18
|
throw new ValidationError(`${field} must be >= ${minimum}`);
|
|
19
19
|
return value;
|
|
20
20
|
}
|
|
21
|
+
function safeNonNegativeSeq(value) {
|
|
22
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
23
|
+
}
|
|
21
24
|
function strictWindowLimit(value) {
|
|
22
25
|
const limit = strictWindowSeq(value, 'limit', true);
|
|
23
26
|
if (limit > V2_PULL_MAX_PAGE_LIMIT)
|
|
@@ -50,19 +53,29 @@ function deduplicateForwardPageMessages(value, afterSeq) {
|
|
|
50
53
|
if (!isJsonObject(raw))
|
|
51
54
|
continue;
|
|
52
55
|
const row = raw;
|
|
53
|
-
const seq =
|
|
54
|
-
if (
|
|
56
|
+
const seq = safeNonNegativeSeq(row.seq);
|
|
57
|
+
if (seq === null || seq <= 0 || seq <= afterSeq || bySeq.has(seq))
|
|
55
58
|
continue;
|
|
56
59
|
bySeq.set(seq, row);
|
|
57
60
|
}
|
|
58
61
|
return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
|
|
59
62
|
}
|
|
60
|
-
function
|
|
63
|
+
function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
|
|
64
|
+
// 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
|
|
65
|
+
if (rawCount <= 0 || nextAfter <= nextAfterSeq)
|
|
66
|
+
return false;
|
|
67
|
+
if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
|
|
68
|
+
return true;
|
|
69
|
+
// 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
|
|
70
|
+
return !hasMoreField && limit > 0 && rawCount >= limit;
|
|
71
|
+
}
|
|
72
|
+
function validateTailPage(result, afterSeq, pageLimit) {
|
|
61
73
|
if (!isJsonObject(result))
|
|
62
74
|
throw new ValidationError('tail response must be an object');
|
|
63
75
|
const row = result;
|
|
64
76
|
if (row.window_mode !== 'tail')
|
|
65
77
|
throw new ValidationError('tail response window_mode must equal tail');
|
|
78
|
+
strictWindowSeq(afterSeq, 'after_seq');
|
|
66
79
|
const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
|
|
67
80
|
const head = strictWindowSeq(row.head_seq, 'head_seq');
|
|
68
81
|
const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
|
|
@@ -72,7 +85,21 @@ function validateTailPage(result, pageLimit) {
|
|
|
72
85
|
throw new ValidationError('covered_through_seq must equal head_seq');
|
|
73
86
|
if (head - windowStart + 1 > pageLimit)
|
|
74
87
|
throw new ValidationError('tail proven window exceeds limit');
|
|
75
|
-
|
|
88
|
+
let lowerSeq = windowStart;
|
|
89
|
+
if (head <= afterSeq) {
|
|
90
|
+
if (windowStart !== head)
|
|
91
|
+
throw new ValidationError('empty tail window must end at head_seq');
|
|
92
|
+
lowerSeq = head + 1;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (windowStart < afterSeq + 1)
|
|
96
|
+
throw new ValidationError('tail window_start_seq is below after_seq');
|
|
97
|
+
lowerSeq = Math.max(windowStart, afterSeq + 1);
|
|
98
|
+
}
|
|
99
|
+
const messages = deduplicateWindowMessages(row.messages, lowerSeq, head, 'the proven tail window');
|
|
100
|
+
if (head <= afterSeq && messages.length > 0)
|
|
101
|
+
throw new ValidationError('tail response must not include messages at or below after_seq');
|
|
102
|
+
return { windowStart, head, messages };
|
|
76
103
|
}
|
|
77
104
|
function validateHistoryPage(result, beforeSeq) {
|
|
78
105
|
if (!isJsonObject(result))
|
|
@@ -257,12 +284,61 @@ export class V2E2EECoordinator {
|
|
|
257
284
|
_initSessionPromise = null;
|
|
258
285
|
targetSetCache = new Map();
|
|
259
286
|
senderIKSyncFetches = new Map();
|
|
287
|
+
/** 服务端 ACK cursor 高于本地 A 时,延迟到下一次 Forward Pull 再恢复。 */
|
|
288
|
+
pendingForwardCursors = new Map();
|
|
289
|
+
/** 本地 A 已提交但服务端 ACK 尚未确认时,下一次 Forward Pull 必须继续推进。 */
|
|
290
|
+
pendingForwardAcks = new Map();
|
|
260
291
|
constructor(runtime) {
|
|
261
292
|
this.runtime = runtime;
|
|
262
293
|
}
|
|
263
294
|
get client() {
|
|
264
295
|
return this.runtime.client;
|
|
265
296
|
}
|
|
297
|
+
resetForwardCursorState() {
|
|
298
|
+
this.pendingForwardCursors.clear();
|
|
299
|
+
this.pendingForwardAcks.clear();
|
|
300
|
+
}
|
|
301
|
+
pendingForwardCursor(ns, localAck) {
|
|
302
|
+
if (!ns)
|
|
303
|
+
return 0;
|
|
304
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
305
|
+
if (pending <= localAck) {
|
|
306
|
+
if (pending > 0)
|
|
307
|
+
this.pendingForwardCursors.delete(ns);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
return pending;
|
|
311
|
+
}
|
|
312
|
+
recordForwardCursor(ns, cursor, localAck) {
|
|
313
|
+
if (!ns || !Number.isSafeInteger(cursor) || cursor <= localAck)
|
|
314
|
+
return;
|
|
315
|
+
this.pendingForwardCursors.set(ns, Math.max(this.pendingForwardCursors.get(ns) ?? 0, cursor));
|
|
316
|
+
}
|
|
317
|
+
clearForwardCursor(ns, committedAck) {
|
|
318
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
319
|
+
if (pending > 0 && committedAck >= pending)
|
|
320
|
+
this.pendingForwardCursors.delete(ns);
|
|
321
|
+
}
|
|
322
|
+
pendingForwardAck(ns, localAck) {
|
|
323
|
+
if (!ns)
|
|
324
|
+
return 0;
|
|
325
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
326
|
+
if (!Number.isSafeInteger(pending) || pending <= 0 || localAck <= 0 || pending > localAck) {
|
|
327
|
+
this.pendingForwardAcks.delete(ns);
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
return pending;
|
|
331
|
+
}
|
|
332
|
+
recordForwardAck(ns, ackSeq) {
|
|
333
|
+
if (!ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0)
|
|
334
|
+
return;
|
|
335
|
+
this.pendingForwardAcks.set(ns, Math.max(this.pendingForwardAcks.get(ns) ?? 0, ackSeq));
|
|
336
|
+
}
|
|
337
|
+
clearForwardAck(ns, confirmedSeq) {
|
|
338
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
339
|
+
if (pending > 0 && confirmedSeq >= pending)
|
|
340
|
+
this.pendingForwardAcks.delete(ns);
|
|
341
|
+
}
|
|
266
342
|
get bootstrapCache() {
|
|
267
343
|
const client = this.client;
|
|
268
344
|
if (!(client._v2BootstrapCache instanceof Map)) {
|
|
@@ -590,7 +666,8 @@ export class V2E2EECoordinator {
|
|
|
590
666
|
createdAt: Date.now(),
|
|
591
667
|
});
|
|
592
668
|
client._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || '-'} group=${groupId || '<p2p>'} pending=${client._v2SenderIKPending.size}`);
|
|
593
|
-
|
|
669
|
+
if (args.startFetch !== false)
|
|
670
|
+
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
|
|
594
671
|
}
|
|
595
672
|
scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
|
|
596
673
|
const client = this.client;
|
|
@@ -644,8 +721,9 @@ export class V2E2EECoordinator {
|
|
|
644
721
|
const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
|
|
645
722
|
for (const [key, entry] of pendingItems) {
|
|
646
723
|
let plaintext = null;
|
|
724
|
+
const retryStatus = {};
|
|
647
725
|
try {
|
|
648
|
-
plaintext = await client._decryptV2Message(entry.msg, false);
|
|
726
|
+
plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
|
|
649
727
|
}
|
|
650
728
|
catch (exc) {
|
|
651
729
|
client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${formatE2EEError(exc)}`);
|
|
@@ -653,6 +731,10 @@ export class V2E2EECoordinator {
|
|
|
653
731
|
continue;
|
|
654
732
|
}
|
|
655
733
|
if (plaintext === null) {
|
|
734
|
+
if (retryStatus.deferred) {
|
|
735
|
+
client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
656
738
|
client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
|
|
657
739
|
const isGroup = Boolean(entry.groupId);
|
|
658
740
|
const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
|
|
@@ -904,7 +986,7 @@ export class V2E2EECoordinator {
|
|
|
904
986
|
history_error: { code: 'decrypt_failed', message: reason },
|
|
905
987
|
};
|
|
906
988
|
}
|
|
907
|
-
async decodeP2PWindowMessage(msg, publish, history, source) {
|
|
989
|
+
async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
|
|
908
990
|
const client = this.client;
|
|
909
991
|
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
910
992
|
let plaintext = null;
|
|
@@ -939,16 +1021,27 @@ export class V2E2EECoordinator {
|
|
|
939
1021
|
}
|
|
940
1022
|
if (publish) {
|
|
941
1023
|
const event = client._delivery.p2pAppEventForMessage(plaintext);
|
|
942
|
-
|
|
1024
|
+
if (orderedPublish) {
|
|
1025
|
+
await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
await client._publishAppEvent(event.event, event.payload, source);
|
|
1029
|
+
}
|
|
943
1030
|
}
|
|
944
1031
|
return plaintext;
|
|
945
1032
|
}
|
|
946
|
-
async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
|
|
1033
|
+
async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
|
|
947
1034
|
const client = this.client;
|
|
948
1035
|
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
949
1036
|
let plaintext = null;
|
|
950
1037
|
if (String(msg.version ?? '') === 'v1') {
|
|
951
1038
|
const payload = msg.payload;
|
|
1039
|
+
if (payload === undefined || payload === null) {
|
|
1040
|
+
if (history)
|
|
1041
|
+
return this.historyDecryptFailure(msg, 'legacy payload is missing');
|
|
1042
|
+
client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
952
1045
|
if (isJsonObject(payload)
|
|
953
1046
|
&& ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
|
|
954
1047
|
if (history)
|
|
@@ -984,8 +1077,14 @@ export class V2E2EECoordinator {
|
|
|
984
1077
|
client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
985
1078
|
return null;
|
|
986
1079
|
}
|
|
987
|
-
if (publish)
|
|
988
|
-
|
|
1080
|
+
if (publish) {
|
|
1081
|
+
if (orderedPublish) {
|
|
1082
|
+
await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
await client._publishAppEvent('group.message_created', plaintext, source);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
989
1088
|
return plaintext;
|
|
990
1089
|
}
|
|
991
1090
|
async applyTailFloor(ns, floor) {
|
|
@@ -994,9 +1093,9 @@ export class V2E2EECoordinator {
|
|
|
994
1093
|
return;
|
|
995
1094
|
client._seqTracker.forceContiguousSeq(ns, floor);
|
|
996
1095
|
client._persistSeq(ns);
|
|
997
|
-
client._saveSeqTrackerState?.();
|
|
1096
|
+
await client._saveSeqTrackerState?.();
|
|
998
1097
|
}
|
|
999
|
-
async pullV2TailInternal(params) {
|
|
1098
|
+
async pullV2TailInternal(params, deferPublish = false) {
|
|
1000
1099
|
const client = this.client;
|
|
1001
1100
|
if (params.window_mode !== 'tail')
|
|
1002
1101
|
throw new ValidationError('window_mode must equal tail');
|
|
@@ -1009,7 +1108,7 @@ export class V2E2EECoordinator {
|
|
|
1009
1108
|
const floor = Number(result.retention_floor_seq ?? 0);
|
|
1010
1109
|
const previous = ns ? client._seqTracker.getSyncState(ns) : null;
|
|
1011
1110
|
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1012
|
-
const page = validateTailPage(result, limit);
|
|
1111
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1013
1112
|
if (ns) {
|
|
1014
1113
|
client._seqTracker.commitTailWindow(ns, {
|
|
1015
1114
|
windowStartSeq: page.windowStart,
|
|
@@ -1018,11 +1117,11 @@ export class V2E2EECoordinator {
|
|
|
1018
1117
|
pageLimit: limit,
|
|
1019
1118
|
});
|
|
1020
1119
|
client._persistSeq(ns);
|
|
1021
|
-
client._saveSeqTrackerState?.();
|
|
1120
|
+
await client._saveSeqTrackerState?.();
|
|
1022
1121
|
}
|
|
1023
1122
|
const decoded = [];
|
|
1024
1123
|
for (const msg of page.messages) {
|
|
1025
|
-
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
|
|
1124
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
|
|
1026
1125
|
if (plaintext)
|
|
1027
1126
|
decoded.push(plaintext);
|
|
1028
1127
|
}
|
|
@@ -1067,13 +1166,18 @@ export class V2E2EECoordinator {
|
|
|
1067
1166
|
}
|
|
1068
1167
|
const decrypted = [];
|
|
1069
1168
|
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
1169
|
+
let pullGateKey = ns
|
|
1170
|
+
? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
|
|
1171
|
+
: '';
|
|
1172
|
+
const deferredServerCursor = ns ? this.pendingForwardCursor(ns, ns ? client._seqTracker.getContiguousSeq(ns) : 0) : 0;
|
|
1173
|
+
const deferredForwardAck = ns ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1070
1174
|
let pageCount = 0;
|
|
1071
1175
|
let lastAutoAckSeq = 0;
|
|
1072
|
-
let pendingAckSeq =
|
|
1176
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1073
1177
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1074
1178
|
while (pageCount < maxPages) {
|
|
1075
1179
|
pageCount += 1;
|
|
1076
|
-
const ackUpToSeq =
|
|
1180
|
+
const ackUpToSeq = pendingAckSeq;
|
|
1077
1181
|
const inlineAckGeneration = ackUpToSeq > 0
|
|
1078
1182
|
? client._delivery.captureP2PInlineAckGeneration()
|
|
1079
1183
|
: null;
|
|
@@ -1085,15 +1189,29 @@ export class V2E2EECoordinator {
|
|
|
1085
1189
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1086
1190
|
});
|
|
1087
1191
|
if (ackUpToSeq > 0) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1192
|
+
const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
|
|
1193
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1194
|
+
if (inlineAckGeneration !== null) {
|
|
1195
|
+
client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
|
|
1196
|
+
}
|
|
1197
|
+
pendingAckSeq = 0;
|
|
1198
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1199
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1203
|
+
await this.confirmForwardP2PAck(ackUpToSeq);
|
|
1204
|
+
pendingAckSeq = 0;
|
|
1205
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1090
1206
|
}
|
|
1091
|
-
pendingAckSeq = 0;
|
|
1092
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1093
1207
|
}
|
|
1094
1208
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1095
1209
|
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1096
|
-
|
|
1210
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1211
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1212
|
+
const hasMore = result.has_more === true;
|
|
1213
|
+
const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
|
|
1214
|
+
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 ?? '')}`);
|
|
1097
1215
|
for (const msg of messages) {
|
|
1098
1216
|
client._logMessageDebug('pull-raw', 'message.v2.pull', 'message.received', msg);
|
|
1099
1217
|
}
|
|
@@ -1108,6 +1226,8 @@ export class V2E2EECoordinator {
|
|
|
1108
1226
|
if (seqs.length > 0) {
|
|
1109
1227
|
pageMaxSeq = Math.max(...seqs);
|
|
1110
1228
|
}
|
|
1229
|
+
const deferredKeyFetches = new Map();
|
|
1230
|
+
let blockedSeq = 0;
|
|
1111
1231
|
for (const msg of messages) {
|
|
1112
1232
|
const seq = Number(msg.seq ?? 0);
|
|
1113
1233
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -1133,7 +1253,7 @@ export class V2E2EECoordinator {
|
|
|
1133
1253
|
attachGatewayProximity(v1Msg, msg);
|
|
1134
1254
|
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
1135
1255
|
if (ns) {
|
|
1136
|
-
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
|
|
1256
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1137
1257
|
}
|
|
1138
1258
|
else {
|
|
1139
1259
|
await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
|
|
@@ -1154,13 +1274,25 @@ export class V2E2EECoordinator {
|
|
|
1154
1274
|
if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
|
|
1155
1275
|
client._v2Session.trackOldSPKMaxSeq(spkId, seq);
|
|
1156
1276
|
}
|
|
1157
|
-
const
|
|
1277
|
+
const deferStatus = {};
|
|
1278
|
+
const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1279
|
+
if (deferStatus.deferred) {
|
|
1280
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1281
|
+
}
|
|
1282
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1283
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1284
|
+
deferredKeyFetches.set(key, {
|
|
1285
|
+
fromAid: deferStatus.fromAid,
|
|
1286
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1287
|
+
groupId: deferStatus.groupId ?? '',
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1158
1290
|
if (plaintext === null) {
|
|
1159
1291
|
client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
|
|
1160
1292
|
continue;
|
|
1161
1293
|
}
|
|
1162
1294
|
if (ns) {
|
|
1163
|
-
await client._publishPulledMessage('message.received', ns, seq, plaintext);
|
|
1295
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
1164
1296
|
}
|
|
1165
1297
|
else {
|
|
1166
1298
|
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
@@ -1169,10 +1301,11 @@ export class V2E2EECoordinator {
|
|
|
1169
1301
|
client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
|
|
1170
1302
|
}
|
|
1171
1303
|
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
1172
|
-
const
|
|
1304
|
+
const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
|
|
1305
|
+
const serverAckSeq = parsedServerAckSeq ?? 0;
|
|
1173
1306
|
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
1174
1307
|
if (ns) {
|
|
1175
|
-
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1308
|
+
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, blockedSeq > 0 ? blockedSeq - 1 : (seqs.length > 0 ? pageMaxSeq : pageContigBefore));
|
|
1176
1309
|
let ackSeq = pageContigBefore;
|
|
1177
1310
|
let contigAdvanced = false;
|
|
1178
1311
|
try {
|
|
@@ -1181,6 +1314,9 @@ export class V2E2EECoordinator {
|
|
|
1181
1314
|
if (retentionFloor > pageContigBefore) {
|
|
1182
1315
|
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
|
|
1183
1316
|
}
|
|
1317
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1318
|
+
client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
|
|
1319
|
+
}
|
|
1184
1320
|
if (seqs.length > 0) {
|
|
1185
1321
|
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1186
1322
|
}
|
|
@@ -1188,10 +1324,9 @@ export class V2E2EECoordinator {
|
|
|
1188
1324
|
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1189
1325
|
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1190
1326
|
if (contigAdvanced) {
|
|
1191
|
-
await client.
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
client._commitSeqTrackerState(ns);
|
|
1327
|
+
await client._commitSeqTrackerState(ns);
|
|
1328
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1329
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1195
1330
|
}
|
|
1196
1331
|
}
|
|
1197
1332
|
catch (exc) {
|
|
@@ -1201,14 +1336,27 @@ export class V2E2EECoordinator {
|
|
|
1201
1336
|
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1202
1337
|
throw exc;
|
|
1203
1338
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1339
|
+
if (contigAdvanced)
|
|
1340
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1341
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1342
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1343
|
+
}
|
|
1344
|
+
if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
|
|
1345
|
+
this.recordForwardCursor(ns, serverAckSeq, ackSeq);
|
|
1346
|
+
}
|
|
1347
|
+
// 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
|
|
1348
|
+
// 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
|
|
1349
|
+
const knownServerAckSeq = hasServerAckSeq
|
|
1350
|
+
? serverAckSeq
|
|
1351
|
+
: (pageCount === 1 ? deferredServerCursor : 0);
|
|
1352
|
+
const ackNeeded = ackSeq > 0
|
|
1206
1353
|
&& ackSeq > lastAutoAckSeq
|
|
1207
|
-
&&
|
|
1208
|
-
|
|
1354
|
+
&& ((hasServerAckSeq && ackSeq > serverAckSeq)
|
|
1355
|
+
|| (contigAdvanced && ackSeq > knownServerAckSeq));
|
|
1209
1356
|
if (ackNeeded) {
|
|
1357
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1210
1358
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1211
|
-
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
|
|
1359
|
+
const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq;
|
|
1212
1360
|
if (canContinuePage) {
|
|
1213
1361
|
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
1214
1362
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
@@ -1216,18 +1364,35 @@ export class V2E2EECoordinator {
|
|
|
1216
1364
|
}
|
|
1217
1365
|
else {
|
|
1218
1366
|
client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
1219
|
-
await this.
|
|
1367
|
+
await this.confirmForwardP2PAck(ackSeq);
|
|
1220
1368
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1221
1369
|
}
|
|
1222
1370
|
}
|
|
1223
1371
|
}
|
|
1372
|
+
if (!ns) {
|
|
1373
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1374
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1224
1377
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1225
1378
|
const rawCount = messages.length;
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1379
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1380
|
+
rawCount,
|
|
1381
|
+
nextAfterSeq,
|
|
1382
|
+
nextAfter,
|
|
1383
|
+
limit,
|
|
1384
|
+
hasMoreField,
|
|
1385
|
+
hasMore,
|
|
1386
|
+
serverHead,
|
|
1387
|
+
pendingAckSeq,
|
|
1388
|
+
});
|
|
1229
1389
|
if (!shouldContinue)
|
|
1230
1390
|
break;
|
|
1391
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1392
|
+
const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1393
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1394
|
+
pullGateKey = nextKey;
|
|
1395
|
+
}
|
|
1231
1396
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1232
1397
|
if (tailDelayMs > 0)
|
|
1233
1398
|
await v2Sleep(tailDelayMs);
|
|
@@ -1237,14 +1402,35 @@ export class V2E2EECoordinator {
|
|
|
1237
1402
|
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
1238
1403
|
if (pendingAckSeq > 0) {
|
|
1239
1404
|
client._clientLog.debug(`message.v2.pull flushing pending auto-ack after max_pages: ns=${ns}, ack_seq=${pendingAckSeq}`);
|
|
1240
|
-
await this.
|
|
1405
|
+
await this.confirmForwardP2PAck(pendingAckSeq);
|
|
1241
1406
|
pendingAckSeq = 0;
|
|
1242
1407
|
}
|
|
1243
1408
|
}
|
|
1244
1409
|
client._clientLog.debug(`message.v2.pull done: requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns || '<none>'}`);
|
|
1245
1410
|
return decrypted;
|
|
1246
1411
|
}
|
|
1412
|
+
async confirmForwardP2PAck(upToSeq) {
|
|
1413
|
+
const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
|
|
1414
|
+
try {
|
|
1415
|
+
const result = await this.ackV2ForForward(upToSeq);
|
|
1416
|
+
const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
|
|
1417
|
+
if (actualAckSeq < upToSeq) {
|
|
1418
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
1419
|
+
}
|
|
1420
|
+
this.clearForwardAck(ns, upToSeq);
|
|
1421
|
+
}
|
|
1422
|
+
catch (exc) {
|
|
1423
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1424
|
+
throw exc;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1247
1427
|
async ackV2(upToSeq) {
|
|
1428
|
+
return this.ackV2WithConfirmation(upToSeq, false);
|
|
1429
|
+
}
|
|
1430
|
+
async ackV2ForForward(upToSeq) {
|
|
1431
|
+
return this.ackV2WithConfirmation(upToSeq, true);
|
|
1432
|
+
}
|
|
1433
|
+
async ackV2WithConfirmation(upToSeq, requireServerWatermark) {
|
|
1248
1434
|
const client = this.client;
|
|
1249
1435
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1250
1436
|
let seq = Number(upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
@@ -1263,7 +1449,12 @@ export class V2E2EECoordinator {
|
|
|
1263
1449
|
const result = isJsonObject(raw)
|
|
1264
1450
|
? { ...raw }
|
|
1265
1451
|
: { result: raw };
|
|
1266
|
-
const actualAckSeq =
|
|
1452
|
+
const actualAckSeq = requireServerWatermark
|
|
1453
|
+
? client._delivery.resolveForwardAckSeq(result, seq)
|
|
1454
|
+
: client._delivery.resolveActualAckSeq(result, seq);
|
|
1455
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
1456
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1457
|
+
}
|
|
1267
1458
|
result.ack_seq = actualAckSeq;
|
|
1268
1459
|
result.success = true;
|
|
1269
1460
|
if (inlineAckGeneration !== null) {
|
|
@@ -1319,7 +1510,7 @@ export class V2E2EECoordinator {
|
|
|
1319
1510
|
client._clientLog.debug(`group.v2.send ok: group=${gid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
|
|
1320
1511
|
return result;
|
|
1321
1512
|
};
|
|
1322
|
-
const markSentSeq = (result) => {
|
|
1513
|
+
const markSentSeq = async (result) => {
|
|
1323
1514
|
if (!isJsonObject(result))
|
|
1324
1515
|
return;
|
|
1325
1516
|
const obj = result;
|
|
@@ -1329,12 +1520,12 @@ export class V2E2EECoordinator {
|
|
|
1329
1520
|
const ns = `group:${gid}`;
|
|
1330
1521
|
client._seqTracker.onMessageSeq(ns, seq);
|
|
1331
1522
|
client._markPublishedSeq(ns, seq);
|
|
1332
|
-
client._saveSeqTrackerState();
|
|
1523
|
+
await client._saveSeqTrackerState();
|
|
1333
1524
|
client._clientLog.debug(`group.v2.send marked own seq: group=${gid}, ns=${ns}, seq=${seq}`);
|
|
1334
1525
|
};
|
|
1335
1526
|
try {
|
|
1336
1527
|
const result = await attempt(true);
|
|
1337
|
-
markSentSeq(result);
|
|
1528
|
+
await markSentSeq(result);
|
|
1338
1529
|
return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
|
|
1339
1530
|
group_id: gid,
|
|
1340
1531
|
payload,
|
|
@@ -1350,7 +1541,7 @@ export class V2E2EECoordinator {
|
|
|
1350
1541
|
client._clientLog.debug(`V2 group speculative send rejected (code=${String(excCode)}), refreshing bootstrap`);
|
|
1351
1542
|
this.deleteBootstrapCacheEntry(`group:${gid}`);
|
|
1352
1543
|
const result = await attempt(false);
|
|
1353
|
-
markSentSeq(result);
|
|
1544
|
+
await markSentSeq(result);
|
|
1354
1545
|
return client._delivery.attachSendResultEnvelope('group.send', withExplicitGroupAid({
|
|
1355
1546
|
group_id: gid,
|
|
1356
1547
|
payload,
|
|
@@ -1509,7 +1700,7 @@ export class V2E2EECoordinator {
|
|
|
1509
1700
|
});
|
|
1510
1701
|
return envelope;
|
|
1511
1702
|
}
|
|
1512
|
-
async pullGroupV2TailInternal(params) {
|
|
1703
|
+
async pullGroupV2TailInternal(params, deferPublish = false) {
|
|
1513
1704
|
const client = this.client;
|
|
1514
1705
|
if (params.window_mode !== 'tail')
|
|
1515
1706
|
throw new ValidationError('window_mode must equal tail');
|
|
@@ -1535,7 +1726,7 @@ export class V2E2EECoordinator {
|
|
|
1535
1726
|
const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
|
|
1536
1727
|
const previous = client._seqTracker.getSyncState(ns);
|
|
1537
1728
|
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1538
|
-
const page = validateTailPage(result, limit);
|
|
1729
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1539
1730
|
client._seqTracker.commitTailWindow(ns, {
|
|
1540
1731
|
windowStartSeq: page.windowStart,
|
|
1541
1732
|
headSeq: page.head,
|
|
@@ -1543,10 +1734,11 @@ export class V2E2EECoordinator {
|
|
|
1543
1734
|
pageLimit: limit,
|
|
1544
1735
|
});
|
|
1545
1736
|
client._persistSeq(ns);
|
|
1546
|
-
client._saveSeqTrackerState?.();
|
|
1737
|
+
await client._saveSeqTrackerState?.();
|
|
1547
1738
|
const decoded = [];
|
|
1548
1739
|
for (const msg of page.messages) {
|
|
1549
|
-
|
|
1740
|
+
client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
1741
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
|
|
1550
1742
|
if (plaintext)
|
|
1551
1743
|
decoded.push(plaintext);
|
|
1552
1744
|
}
|
|
@@ -1625,12 +1817,19 @@ export class V2E2EECoordinator {
|
|
|
1625
1817
|
const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
|
|
1626
1818
|
const cursorParams = opts?.cursorParams ?? {};
|
|
1627
1819
|
const ownsCursor = opts?.ownsCursor !== false;
|
|
1820
|
+
let pullGateKey = ownsCursor
|
|
1821
|
+
? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
|
|
1822
|
+
: '';
|
|
1628
1823
|
let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
|
|
1629
1824
|
? afterSeq
|
|
1630
1825
|
: (afterSeq || client._seqTracker.getContiguousSeq(ns));
|
|
1826
|
+
const deferredServerCursor = ownsCursor
|
|
1827
|
+
? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns))
|
|
1828
|
+
: 0;
|
|
1829
|
+
const deferredForwardAck = ownsCursor ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1631
1830
|
let pageCount = 0;
|
|
1632
1831
|
let lastAutoAckSeq = 0;
|
|
1633
|
-
let pendingAckSeq =
|
|
1832
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1634
1833
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1635
1834
|
while (pageCount < maxPages) {
|
|
1636
1835
|
pageCount += 1;
|
|
@@ -1648,11 +1847,21 @@ export class V2E2EECoordinator {
|
|
|
1648
1847
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1649
1848
|
}, groupAid));
|
|
1650
1849
|
if (ackUpToSeq > 0) {
|
|
1651
|
-
|
|
1652
|
-
|
|
1850
|
+
const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
|
|
1851
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1852
|
+
if (inlineAckGeneration !== null) {
|
|
1853
|
+
client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
|
|
1854
|
+
}
|
|
1855
|
+
pendingAckSeq = 0;
|
|
1856
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1857
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1858
|
+
}
|
|
1859
|
+
else {
|
|
1860
|
+
client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1861
|
+
await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
|
|
1862
|
+
pendingAckSeq = 0;
|
|
1863
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1653
1864
|
}
|
|
1654
|
-
pendingAckSeq = 0;
|
|
1655
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1656
1865
|
}
|
|
1657
1866
|
const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1658
1867
|
if (resultGroupAid)
|
|
@@ -1661,7 +1870,10 @@ export class V2E2EECoordinator {
|
|
|
1661
1870
|
const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
|
|
1662
1871
|
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1663
1872
|
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1664
|
-
|
|
1873
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1874
|
+
const hasMore = result.has_more === true;
|
|
1875
|
+
const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
|
|
1876
|
+
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 ?? '')}`);
|
|
1665
1877
|
for (const msg of messages) {
|
|
1666
1878
|
client._logMessageDebug('pull-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
1667
1879
|
}
|
|
@@ -1676,6 +1888,8 @@ export class V2E2EECoordinator {
|
|
|
1676
1888
|
if (seqs.length > 0) {
|
|
1677
1889
|
pageMaxSeq = Math.max(...seqs);
|
|
1678
1890
|
}
|
|
1891
|
+
const deferredKeyFetches = new Map();
|
|
1892
|
+
let blockedSeq = 0;
|
|
1679
1893
|
for (const msg of messages) {
|
|
1680
1894
|
const seq = Number(msg.seq ?? 0);
|
|
1681
1895
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -1711,7 +1925,7 @@ export class V2E2EECoordinator {
|
|
|
1711
1925
|
};
|
|
1712
1926
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1713
1927
|
attachGatewayProximity(v1Msg, msg);
|
|
1714
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1928
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1715
1929
|
decrypted.push(v1Msg);
|
|
1716
1930
|
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
1717
1931
|
continue;
|
|
@@ -1731,7 +1945,7 @@ export class V2E2EECoordinator {
|
|
|
1731
1945
|
};
|
|
1732
1946
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1733
1947
|
attachGatewayProximity(v1Msg, msg);
|
|
1734
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1948
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1735
1949
|
decrypted.push(v1Msg);
|
|
1736
1950
|
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
1737
1951
|
continue;
|
|
@@ -1743,7 +1957,19 @@ export class V2E2EECoordinator {
|
|
|
1743
1957
|
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1744
1958
|
continue;
|
|
1745
1959
|
}
|
|
1746
|
-
|
|
1960
|
+
const deferStatus = {};
|
|
1961
|
+
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1962
|
+
if (deferStatus.deferred) {
|
|
1963
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1964
|
+
}
|
|
1965
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1966
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1967
|
+
deferredKeyFetches.set(key, {
|
|
1968
|
+
fromAid: deferStatus.fromAid,
|
|
1969
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1970
|
+
groupId: deferStatus.groupId ?? '',
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1747
1973
|
if (plaintext === null) {
|
|
1748
1974
|
client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
|
|
1749
1975
|
continue;
|
|
@@ -1751,52 +1977,71 @@ export class V2E2EECoordinator {
|
|
|
1751
1977
|
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1752
1978
|
plaintext.group_id = gid;
|
|
1753
1979
|
plaintext.group_aid = eventGroupAid;
|
|
1754
|
-
await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
|
|
1980
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
1755
1981
|
decrypted.push(plaintext);
|
|
1756
1982
|
client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
1757
1983
|
}
|
|
1758
|
-
const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
|
|
1759
1984
|
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
1985
|
+
const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
|
|
1986
|
+
const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
|
|
1760
1987
|
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
|
|
1761
1988
|
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
1762
1989
|
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
1763
|
-
const commitTarget = Math.max(pageContigBefore, effectiveFloor, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1990
|
+
const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, blockedSeq > 0 ? blockedSeq - 1 : (seqs.length > 0 ? pageMaxSeq : pageContigBefore));
|
|
1764
1991
|
let ackSeq = pageContigBefore;
|
|
1765
1992
|
let contigAdvanced = false;
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1993
|
+
if (ownsCursor) {
|
|
1994
|
+
try {
|
|
1995
|
+
if (commitTarget > pageContigBefore) {
|
|
1996
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1997
|
+
if (effectiveFloor > pageContigBefore) {
|
|
1998
|
+
client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
|
|
1999
|
+
}
|
|
2000
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
2001
|
+
client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
|
|
2002
|
+
}
|
|
2003
|
+
if (seqs.length > 0) {
|
|
2004
|
+
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
2005
|
+
}
|
|
1774
2006
|
}
|
|
1775
|
-
}
|
|
1776
|
-
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1777
|
-
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1778
|
-
if (contigAdvanced) {
|
|
1779
|
-
await client._drainOrderedMessages(ns, undefined, true);
|
|
1780
2007
|
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1781
2008
|
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1782
|
-
|
|
2009
|
+
if (contigAdvanced) {
|
|
2010
|
+
await client._commitSeqTrackerState(ns);
|
|
2011
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
2012
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
2013
|
+
}
|
|
1783
2014
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2015
|
+
catch (exc) {
|
|
2016
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
2017
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
2018
|
+
}
|
|
2019
|
+
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
2020
|
+
throw exc;
|
|
1788
2021
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
2022
|
+
if (contigAdvanced)
|
|
2023
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
2024
|
+
}
|
|
2025
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
2026
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1791
2027
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
2028
|
+
if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
|
|
2029
|
+
this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
|
|
2030
|
+
}
|
|
2031
|
+
// 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
|
|
2032
|
+
// 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
|
|
2033
|
+
const knownServerCursorSeq = hasServerCursor
|
|
2034
|
+
? cursorCurrentSeq
|
|
2035
|
+
: (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
|
|
2036
|
+
const ackNeeded = ackSeq > 0
|
|
1794
2037
|
&& ackSeq > lastAutoAckSeq
|
|
1795
2038
|
&& ownsCursor
|
|
1796
|
-
&& (
|
|
2039
|
+
&& ((hasServerCursor && ackSeq > cursorCurrentSeq)
|
|
2040
|
+
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
1797
2041
|
if (ackNeeded) {
|
|
2042
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1798
2043
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1799
|
-
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
|
|
2044
|
+
const canContinuePage = blockedSeq <= 0 && messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
|
|
1800
2045
|
if (canContinuePage) {
|
|
1801
2046
|
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
1802
2047
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
@@ -1804,7 +2049,7 @@ export class V2E2EECoordinator {
|
|
|
1804
2049
|
}
|
|
1805
2050
|
else {
|
|
1806
2051
|
client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
1807
|
-
await this.
|
|
2052
|
+
await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
|
|
1808
2053
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1809
2054
|
}
|
|
1810
2055
|
}
|
|
@@ -1812,11 +2057,23 @@ export class V2E2EECoordinator {
|
|
|
1812
2057
|
if (!ownsCursor)
|
|
1813
2058
|
break;
|
|
1814
2059
|
const rawCount = messages.length;
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
2060
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
2061
|
+
rawCount,
|
|
2062
|
+
nextAfterSeq,
|
|
2063
|
+
nextAfter,
|
|
2064
|
+
limit,
|
|
2065
|
+
hasMoreField,
|
|
2066
|
+
hasMore,
|
|
2067
|
+
serverHead,
|
|
2068
|
+
pendingAckSeq,
|
|
2069
|
+
});
|
|
1818
2070
|
if (!shouldContinue)
|
|
1819
2071
|
break;
|
|
2072
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
2073
|
+
const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
2074
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
2075
|
+
pullGateKey = nextKey;
|
|
2076
|
+
}
|
|
1820
2077
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1821
2078
|
if (tailDelayMs > 0)
|
|
1822
2079
|
await v2Sleep(tailDelayMs);
|
|
@@ -1826,14 +2083,35 @@ export class V2E2EECoordinator {
|
|
|
1826
2083
|
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
1827
2084
|
if (pendingAckSeq > 0) {
|
|
1828
2085
|
client._clientLog.debug(`group.v2.pull flushing pending auto-ack after max_pages: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
|
|
1829
|
-
await this.
|
|
2086
|
+
await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
|
|
1830
2087
|
pendingAckSeq = 0;
|
|
1831
2088
|
}
|
|
1832
2089
|
}
|
|
1833
2090
|
client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
|
|
1834
2091
|
return decrypted;
|
|
1835
2092
|
}
|
|
2093
|
+
async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
|
|
2094
|
+
const ns = `group:${String(groupId ?? '').trim()}`;
|
|
2095
|
+
try {
|
|
2096
|
+
const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
|
|
2097
|
+
const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
|
|
2098
|
+
if (actualAckSeq < upToSeq) {
|
|
2099
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
2100
|
+
}
|
|
2101
|
+
this.clearForwardAck(ns, upToSeq);
|
|
2102
|
+
}
|
|
2103
|
+
catch (exc) {
|
|
2104
|
+
this.recordForwardAck(ns, upToSeq);
|
|
2105
|
+
throw exc;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
1836
2108
|
async ackGroupV2(groupId, upToSeq, groupAid) {
|
|
2109
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, false);
|
|
2110
|
+
}
|
|
2111
|
+
async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
|
|
2112
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true);
|
|
2113
|
+
}
|
|
2114
|
+
async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark) {
|
|
1837
2115
|
const client = this.client;
|
|
1838
2116
|
const gid = String(groupId ?? '').trim();
|
|
1839
2117
|
if (!gid)
|
|
@@ -1852,7 +2130,12 @@ export class V2E2EECoordinator {
|
|
|
1852
2130
|
const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
|
|
1853
2131
|
client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
|
|
1854
2132
|
const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
|
|
1855
|
-
const actualAckSeq =
|
|
2133
|
+
const actualAckSeq = requireServerWatermark
|
|
2134
|
+
? client._delivery.resolveForwardAckSeq(result, seq)
|
|
2135
|
+
: client._delivery.resolveActualAckSeq(result, seq);
|
|
2136
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
2137
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
2138
|
+
}
|
|
1856
2139
|
if (inlineAckGeneration !== null) {
|
|
1857
2140
|
client._delivery.onGroupV2AckSucceeded(gid, actualAckSeq, inlineAckGeneration);
|
|
1858
2141
|
}
|