@agentunion/fastaun-browser 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 +104 -0
- package/_packed_docs/CHANGELOG.md +104 -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 +14 -0
- package/dist/agent-md-schema.d.ts.map +1 -0
- package/dist/agent-md-schema.js +103 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +7 -0
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +230 -10
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +5 -1
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +24 -4
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +29 -7
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +30253 -21955
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +36 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +40 -5
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +719 -221
- 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.d.ts.map +1 -1
- package/dist/client/lifecycle.js +463 -151
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +25 -6
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +407 -123
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.d.ts.map +1 -1
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +434 -112
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +22 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +500 -116
- package/dist/client.js.map +1 -1
- package/dist/facades.d.ts +23 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +55 -7
- package/dist/facades.js.map +1 -1
- package/dist/group-id.d.ts.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.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +8 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +23 -1
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +8 -1
- package/dist/transport.js.map +1 -1
- package/dist/validators.d.ts.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 +6 -5
package/dist/client/v2-e2ee.js
CHANGED
|
@@ -4,6 +4,10 @@ import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e
|
|
|
4
4
|
import { V2Session, V2KeyStore } from '../v2/session/index.js';
|
|
5
5
|
import { ConnectionState, isJsonObject } from '../types.js';
|
|
6
6
|
import { normalizeGroupMentionMode } from './mention-mode.js';
|
|
7
|
+
function pullGateKeyForClient(client, method, params, fallback) {
|
|
8
|
+
const key = client._rpcPipeline?.pullGateKeyForCall?.(method, params);
|
|
9
|
+
return typeof key === 'string' && key ? key : fallback;
|
|
10
|
+
}
|
|
7
11
|
const V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
|
|
8
12
|
const V2_RETRYABLE_CODES = new Set([-33011, -33012, -33050, -33052, -33054]);
|
|
9
13
|
const V2_GROUP_STALE_BOOTSTRAP_CODE = -33054;
|
|
@@ -21,6 +25,9 @@ function strictWindowSeq(value, field, positive = false) {
|
|
|
21
25
|
throw new ValidationError(`${field} must be >= ${minimum}`);
|
|
22
26
|
return value;
|
|
23
27
|
}
|
|
28
|
+
function safeNonNegativeSeq(value) {
|
|
29
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
30
|
+
}
|
|
24
31
|
function strictWindowLimit(value) {
|
|
25
32
|
const limit = strictWindowSeq(value, 'limit', true);
|
|
26
33
|
if (limit > V2_PULL_MAX_PAGE_LIMIT)
|
|
@@ -53,26 +60,50 @@ function deduplicateForwardPageMessages(value, afterSeq) {
|
|
|
53
60
|
if (!isJsonObject(raw))
|
|
54
61
|
continue;
|
|
55
62
|
const row = raw;
|
|
56
|
-
const seq =
|
|
57
|
-
if (
|
|
63
|
+
const seq = safeNonNegativeSeq(row.seq);
|
|
64
|
+
if (seq === null || seq <= 0 || seq <= afterSeq || bySeq.has(seq))
|
|
58
65
|
continue;
|
|
59
66
|
bySeq.set(seq, row);
|
|
60
67
|
}
|
|
61
68
|
return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
|
|
62
69
|
}
|
|
63
|
-
function
|
|
70
|
+
function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
|
|
71
|
+
// 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
|
|
72
|
+
if (rawCount <= 0 || nextAfter <= nextAfterSeq)
|
|
73
|
+
return false;
|
|
74
|
+
if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
|
|
75
|
+
return true;
|
|
76
|
+
// 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
|
|
77
|
+
return !hasMoreField && limit > 0 && rawCount >= limit;
|
|
78
|
+
}
|
|
79
|
+
function validateTailPage(result, afterSeq, pageLimit) {
|
|
64
80
|
if (!isJsonObject(result))
|
|
65
81
|
throw new ValidationError('tail response must be an object');
|
|
66
82
|
const row = result;
|
|
67
83
|
if (row.window_mode !== 'tail')
|
|
68
84
|
throw new ValidationError('tail response window_mode must equal tail');
|
|
85
|
+
strictWindowSeq(afterSeq, 'after_seq');
|
|
69
86
|
const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
|
|
70
87
|
const head = strictWindowSeq(row.head_seq, 'head_seq');
|
|
71
88
|
const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
|
|
72
89
|
if (windowStart > head || covered !== head || head - windowStart + 1 > pageLimit) {
|
|
73
90
|
throw new ValidationError('tail response proof is invalid');
|
|
74
91
|
}
|
|
75
|
-
|
|
92
|
+
let lowerSeq = windowStart;
|
|
93
|
+
if (head <= afterSeq) {
|
|
94
|
+
if (windowStart !== head)
|
|
95
|
+
throw new ValidationError('empty tail window must end at head_seq');
|
|
96
|
+
lowerSeq = head + 1;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
if (windowStart < afterSeq + 1)
|
|
100
|
+
throw new ValidationError('tail window_start_seq is below after_seq');
|
|
101
|
+
lowerSeq = Math.max(windowStart, afterSeq + 1);
|
|
102
|
+
}
|
|
103
|
+
const messages = deduplicateWindowMessages(row.messages, lowerSeq, head, 'the proven tail window');
|
|
104
|
+
if (head <= afterSeq && messages.length > 0)
|
|
105
|
+
throw new ValidationError('tail response must not include messages at or below after_seq');
|
|
106
|
+
return { windowStart, head, messages };
|
|
76
107
|
}
|
|
77
108
|
function validateHistoryPage(result, beforeSeq) {
|
|
78
109
|
if (!isJsonObject(result))
|
|
@@ -329,6 +360,9 @@ export class V2E2EECoordinator {
|
|
|
329
360
|
runtime;
|
|
330
361
|
targetSetCache = new Map();
|
|
331
362
|
senderIKSyncFetches = new Map();
|
|
363
|
+
/** 服务端 ACK cursor 高于本地 A 时,延迟到下一次 Forward Pull 再恢复。 */
|
|
364
|
+
pendingForwardCursors = new Map();
|
|
365
|
+
pendingForwardAcks = new Map();
|
|
332
366
|
v2SessionInitRetryTimer = null;
|
|
333
367
|
v2SessionInitRetryBackgroundSync = false;
|
|
334
368
|
v2SessionInitRetryIdentityKey = null;
|
|
@@ -338,6 +372,51 @@ export class V2E2EECoordinator {
|
|
|
338
372
|
get client() {
|
|
339
373
|
return this.runtime.client;
|
|
340
374
|
}
|
|
375
|
+
resetForwardCursorState() {
|
|
376
|
+
this.pendingForwardCursors.clear();
|
|
377
|
+
this.pendingForwardAcks.clear();
|
|
378
|
+
}
|
|
379
|
+
pendingForwardCursor(ns, localAck) {
|
|
380
|
+
if (!ns)
|
|
381
|
+
return 0;
|
|
382
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
383
|
+
if (pending <= localAck) {
|
|
384
|
+
if (pending > 0)
|
|
385
|
+
this.pendingForwardCursors.delete(ns);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
return pending;
|
|
389
|
+
}
|
|
390
|
+
recordForwardCursor(ns, cursor, localAck) {
|
|
391
|
+
if (!ns || !Number.isSafeInteger(cursor) || cursor <= localAck)
|
|
392
|
+
return;
|
|
393
|
+
this.pendingForwardCursors.set(ns, Math.max(this.pendingForwardCursors.get(ns) ?? 0, cursor));
|
|
394
|
+
}
|
|
395
|
+
clearForwardCursor(ns, committedAck) {
|
|
396
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
397
|
+
if (pending > 0 && committedAck >= pending)
|
|
398
|
+
this.pendingForwardCursors.delete(ns);
|
|
399
|
+
}
|
|
400
|
+
pendingForwardAck(ns, localAck) {
|
|
401
|
+
if (!ns)
|
|
402
|
+
return 0;
|
|
403
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
404
|
+
if (!Number.isSafeInteger(pending) || pending <= 0 || localAck <= 0 || pending > localAck) {
|
|
405
|
+
this.pendingForwardAcks.delete(ns);
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
return pending;
|
|
409
|
+
}
|
|
410
|
+
recordForwardAck(ns, ackSeq) {
|
|
411
|
+
if (!ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0)
|
|
412
|
+
return;
|
|
413
|
+
this.pendingForwardAcks.set(ns, Math.max(this.pendingForwardAcks.get(ns) ?? 0, ackSeq));
|
|
414
|
+
}
|
|
415
|
+
clearForwardAck(ns, confirmedSeq) {
|
|
416
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
417
|
+
if (pending > 0 && confirmedSeq >= pending)
|
|
418
|
+
this.pendingForwardAcks.delete(ns);
|
|
419
|
+
}
|
|
341
420
|
get bootstrapCache() {
|
|
342
421
|
const client = this.client;
|
|
343
422
|
if (!(client._v2BootstrapCache instanceof Map)) {
|
|
@@ -759,7 +838,8 @@ export class V2E2EECoordinator {
|
|
|
759
838
|
createdAt: Date.now(),
|
|
760
839
|
});
|
|
761
840
|
client._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || '-'} group=${groupId || '<p2p>'} pending=${client._v2SenderIKPending.size}`);
|
|
762
|
-
|
|
841
|
+
if (args.startFetch !== false)
|
|
842
|
+
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
|
|
763
843
|
}
|
|
764
844
|
scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
|
|
765
845
|
const client = this.client;
|
|
@@ -810,8 +890,9 @@ export class V2E2EECoordinator {
|
|
|
810
890
|
const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
|
|
811
891
|
for (const [key, entry] of pendingItems) {
|
|
812
892
|
let plaintext = null;
|
|
893
|
+
const retryStatus = {};
|
|
813
894
|
try {
|
|
814
|
-
plaintext = await client._decryptV2Message(entry.msg, false);
|
|
895
|
+
plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
|
|
815
896
|
}
|
|
816
897
|
catch (exc) {
|
|
817
898
|
client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatE2EEError(exc))}`);
|
|
@@ -819,6 +900,10 @@ export class V2E2EECoordinator {
|
|
|
819
900
|
continue;
|
|
820
901
|
}
|
|
821
902
|
if (plaintext === null) {
|
|
903
|
+
if (retryStatus.deferred) {
|
|
904
|
+
client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
822
907
|
client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
|
|
823
908
|
const isGroup = Boolean(entry.groupId);
|
|
824
909
|
const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
|
|
@@ -907,7 +992,7 @@ export class V2E2EECoordinator {
|
|
|
907
992
|
history_error: { code: 'decrypt_failed', message: reason },
|
|
908
993
|
};
|
|
909
994
|
}
|
|
910
|
-
async decodeP2PWindowMessage(msg, publish, history, source) {
|
|
995
|
+
async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
|
|
911
996
|
const client = this.client;
|
|
912
997
|
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
913
998
|
let plaintext = null;
|
|
@@ -939,16 +1024,27 @@ export class V2E2EECoordinator {
|
|
|
939
1024
|
}
|
|
940
1025
|
if (publish) {
|
|
941
1026
|
const event = client._delivery.p2pAppEventForMessage(plaintext);
|
|
942
|
-
|
|
1027
|
+
if (orderedPublish) {
|
|
1028
|
+
await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
await client._publishAppEvent(event.event, event.payload, source);
|
|
1032
|
+
}
|
|
943
1033
|
}
|
|
944
1034
|
return plaintext;
|
|
945
1035
|
}
|
|
946
|
-
async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
|
|
1036
|
+
async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
|
|
947
1037
|
const client = this.client;
|
|
948
1038
|
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
949
1039
|
let plaintext = null;
|
|
950
1040
|
if (String(msg.version ?? '') === 'v1') {
|
|
951
1041
|
const payload = msg.payload;
|
|
1042
|
+
if (payload === undefined || payload === null) {
|
|
1043
|
+
if (history)
|
|
1044
|
+
return this.historyDecryptFailure(msg, 'legacy payload is missing');
|
|
1045
|
+
client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
952
1048
|
if (isJsonObject(payload)
|
|
953
1049
|
&& ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
|
|
954
1050
|
if (history)
|
|
@@ -985,8 +1081,14 @@ export class V2E2EECoordinator {
|
|
|
985
1081
|
client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
986
1082
|
return null;
|
|
987
1083
|
}
|
|
988
|
-
if (publish)
|
|
989
|
-
|
|
1084
|
+
if (publish) {
|
|
1085
|
+
if (orderedPublish) {
|
|
1086
|
+
await client._delivery.publishOrderedMessage('group.message_created', `group:${groupId}`, seq, plaintext);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
await client._publishAppEvent('group.message_created', plaintext, source);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
990
1092
|
return plaintext;
|
|
991
1093
|
}
|
|
992
1094
|
async decodeInlineP2PMessage(msg) {
|
|
@@ -1003,7 +1105,7 @@ export class V2E2EECoordinator {
|
|
|
1003
1105
|
client._persistSeq(ns);
|
|
1004
1106
|
await client._saveSeqTrackerState?.();
|
|
1005
1107
|
}
|
|
1006
|
-
async pullV2TailInternal(params) {
|
|
1108
|
+
async pullV2TailInternal(params, deferPublish = false) {
|
|
1007
1109
|
const client = this.client;
|
|
1008
1110
|
if (params.window_mode !== 'tail')
|
|
1009
1111
|
throw new ValidationError('window_mode must equal tail');
|
|
@@ -1016,7 +1118,7 @@ export class V2E2EECoordinator {
|
|
|
1016
1118
|
const floor = Number(result.retention_floor_seq ?? 0);
|
|
1017
1119
|
const previous = ns ? client._seqTracker.getSyncState(ns) : null;
|
|
1018
1120
|
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1019
|
-
const page = validateTailPage(result, limit);
|
|
1121
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1020
1122
|
if (ns) {
|
|
1021
1123
|
client._seqTracker.commitTailWindow(ns, {
|
|
1022
1124
|
windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
|
|
@@ -1026,7 +1128,7 @@ export class V2E2EECoordinator {
|
|
|
1026
1128
|
}
|
|
1027
1129
|
const decoded = [];
|
|
1028
1130
|
for (const msg of page.messages) {
|
|
1029
|
-
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
|
|
1131
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
|
|
1030
1132
|
if (plaintext)
|
|
1031
1133
|
decoded.push(plaintext);
|
|
1032
1134
|
}
|
|
@@ -1053,14 +1155,39 @@ export class V2E2EECoordinator {
|
|
|
1053
1155
|
await client._ensureV2SessionReady('message.pull');
|
|
1054
1156
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1055
1157
|
if (opts?.windowMode === 'tail') {
|
|
1158
|
+
if (!opts.gateLocked) {
|
|
1159
|
+
const key = pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1160
|
+
window_mode: 'tail', after_seq: afterSeq, limit,
|
|
1161
|
+
}, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
|
|
1162
|
+
return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
|
|
1163
|
+
...(opts ?? {}),
|
|
1164
|
+
gateLocked: true,
|
|
1165
|
+
}), false);
|
|
1166
|
+
}
|
|
1056
1167
|
const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
|
|
1057
1168
|
return Array.isArray(result.messages) ? result.messages : [];
|
|
1058
1169
|
}
|
|
1170
|
+
if (ns && !opts?.gateLocked) {
|
|
1171
|
+
const key = pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1172
|
+
after_seq: afterSeq, force: opts?.force === true, limit,
|
|
1173
|
+
}, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1174
|
+
return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
|
|
1175
|
+
...(opts ?? {}),
|
|
1176
|
+
gateLocked: true,
|
|
1177
|
+
}), true);
|
|
1178
|
+
}
|
|
1059
1179
|
const decrypted = [];
|
|
1060
1180
|
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
1181
|
+
let pullGateKey = ns
|
|
1182
|
+
? pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1183
|
+
after_seq: afterSeq, force: opts?.force === true, limit,
|
|
1184
|
+
}, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`)
|
|
1185
|
+
: '';
|
|
1186
|
+
const deferredServerCursor = ns ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1187
|
+
const deferredForwardAck = ns ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1061
1188
|
let pageCount = 0;
|
|
1062
1189
|
let lastAutoAckSeq = 0;
|
|
1063
|
-
let pendingAckSeq =
|
|
1190
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1064
1191
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1065
1192
|
while (pageCount < maxPages) {
|
|
1066
1193
|
pageCount += 1;
|
|
@@ -1075,24 +1202,29 @@ export class V2E2EECoordinator {
|
|
|
1075
1202
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1076
1203
|
});
|
|
1077
1204
|
if (ackUpToSeq > 0) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
if (
|
|
1081
|
-
actualAckSeq = typeof result.server_ack_seq === 'number'
|
|
1082
|
-
&& Number.isSafeInteger(result.server_ack_seq) && result.server_ack_seq >= 0
|
|
1083
|
-
? result.server_ack_seq
|
|
1084
|
-
: 0;
|
|
1085
|
-
}
|
|
1086
|
-
if (actualAckSeq > 0) {
|
|
1205
|
+
const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
|
|
1206
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1207
|
+
if (pendingInlineGeneration !== null) {
|
|
1087
1208
|
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
|
|
1088
1209
|
}
|
|
1210
|
+
pendingAckSeq = 0;
|
|
1211
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1212
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1216
|
+
await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
|
|
1217
|
+
pendingAckSeq = 0;
|
|
1218
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1089
1219
|
}
|
|
1090
|
-
pendingAckSeq = 0;
|
|
1091
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1092
1220
|
}
|
|
1093
1221
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1094
1222
|
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1095
|
-
|
|
1223
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1224
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1225
|
+
const hasMore = result.has_more === true;
|
|
1226
|
+
const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
|
|
1227
|
+
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 ?? '')}`);
|
|
1096
1228
|
const seqs = messages
|
|
1097
1229
|
.map((msg) => Number(msg.seq ?? 0))
|
|
1098
1230
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
@@ -1101,6 +1233,8 @@ export class V2E2EECoordinator {
|
|
|
1101
1233
|
? client._seqTracker.snapshotNamespace(ns)
|
|
1102
1234
|
: null;
|
|
1103
1235
|
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
1236
|
+
const deferredKeyFetches = new Map();
|
|
1237
|
+
let blockedSeq = 0;
|
|
1104
1238
|
for (const msg of messages) {
|
|
1105
1239
|
const seq = Number(msg.seq ?? 0);
|
|
1106
1240
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -1127,7 +1261,7 @@ export class V2E2EECoordinator {
|
|
|
1127
1261
|
attachGatewayProximity(v1Msg, msg);
|
|
1128
1262
|
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
1129
1263
|
if (ns)
|
|
1130
|
-
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
|
|
1264
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1131
1265
|
else
|
|
1132
1266
|
await client._publishAppEvent(appEvent.event, appEvent.payload);
|
|
1133
1267
|
decrypted.push(v1Msg);
|
|
@@ -1145,11 +1279,23 @@ export class V2E2EECoordinator {
|
|
|
1145
1279
|
if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
|
|
1146
1280
|
client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
|
|
1147
1281
|
}
|
|
1148
|
-
const
|
|
1282
|
+
const deferStatus = {};
|
|
1283
|
+
const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1284
|
+
if (deferStatus.deferred) {
|
|
1285
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1286
|
+
}
|
|
1287
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1288
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1289
|
+
deferredKeyFetches.set(key, {
|
|
1290
|
+
fromAid: deferStatus.fromAid,
|
|
1291
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1292
|
+
groupId: deferStatus.groupId ?? '',
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1149
1295
|
if (plaintext === null)
|
|
1150
1296
|
continue;
|
|
1151
1297
|
if (ns) {
|
|
1152
|
-
await client._publishPulledMessage('message.received', ns, seq, plaintext);
|
|
1298
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
1153
1299
|
decrypted.push(plaintext);
|
|
1154
1300
|
}
|
|
1155
1301
|
else {
|
|
@@ -1158,10 +1304,11 @@ export class V2E2EECoordinator {
|
|
|
1158
1304
|
}
|
|
1159
1305
|
}
|
|
1160
1306
|
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
1161
|
-
const
|
|
1307
|
+
const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
|
|
1308
|
+
const serverAckSeq = parsedServerAckSeq ?? 0;
|
|
1162
1309
|
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
1163
1310
|
if (ns) {
|
|
1164
|
-
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1311
|
+
const commitTarget = blockedSeq > 0 ? pageContigBefore : Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1165
1312
|
let ackSeq = pageContigBefore;
|
|
1166
1313
|
let contigAdvanced = false;
|
|
1167
1314
|
try {
|
|
@@ -1170,6 +1317,9 @@ export class V2E2EECoordinator {
|
|
|
1170
1317
|
if (retentionFloor > pageContigBefore) {
|
|
1171
1318
|
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
|
|
1172
1319
|
}
|
|
1320
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1321
|
+
client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
|
|
1322
|
+
}
|
|
1173
1323
|
if (seqs.length > 0) {
|
|
1174
1324
|
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1175
1325
|
}
|
|
@@ -1177,10 +1327,9 @@ export class V2E2EECoordinator {
|
|
|
1177
1327
|
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1178
1328
|
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1179
1329
|
if (contigAdvanced) {
|
|
1180
|
-
await client._drainOrderedMessages(ns);
|
|
1181
|
-
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1182
|
-
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1183
1330
|
await client._commitSeqTrackerState(ns);
|
|
1331
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1332
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1184
1333
|
}
|
|
1185
1334
|
}
|
|
1186
1335
|
catch (exc) {
|
|
@@ -1190,11 +1339,26 @@ export class V2E2EECoordinator {
|
|
|
1190
1339
|
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1191
1340
|
throw exc;
|
|
1192
1341
|
}
|
|
1193
|
-
|
|
1342
|
+
if (contigAdvanced)
|
|
1343
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1344
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1345
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1346
|
+
}
|
|
1347
|
+
if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
|
|
1348
|
+
this.recordForwardCursor(ns, serverAckSeq, ackSeq);
|
|
1349
|
+
}
|
|
1350
|
+
// 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
|
|
1351
|
+
// 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
|
|
1352
|
+
const knownServerAckSeq = hasServerAckSeq
|
|
1353
|
+
? serverAckSeq
|
|
1354
|
+
: (pageCount === 1 ? deferredServerCursor : 0);
|
|
1355
|
+
const ackNeeded = blockedSeq <= 0
|
|
1194
1356
|
&& ackSeq > 0
|
|
1195
1357
|
&& ackSeq > lastAutoAckSeq
|
|
1196
|
-
&& (
|
|
1358
|
+
&& ((hasServerAckSeq && ackSeq > serverAckSeq)
|
|
1359
|
+
|| (contigAdvanced && ackSeq > knownServerAckSeq));
|
|
1197
1360
|
if (ackNeeded) {
|
|
1361
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1198
1362
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1199
1363
|
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
|
|
1200
1364
|
if (canContinuePage) {
|
|
@@ -1202,18 +1366,34 @@ export class V2E2EECoordinator {
|
|
|
1202
1366
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1203
1367
|
}
|
|
1204
1368
|
else {
|
|
1205
|
-
await this.
|
|
1369
|
+
await this.confirmForwardP2PAck(ackSeq);
|
|
1206
1370
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1207
1371
|
}
|
|
1208
1372
|
}
|
|
1209
1373
|
}
|
|
1210
1374
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1211
1375
|
const rawCount = messages.length;
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const shouldContinue =
|
|
1376
|
+
if (blockedSeq > 0)
|
|
1377
|
+
break;
|
|
1378
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1379
|
+
rawCount,
|
|
1380
|
+
nextAfterSeq,
|
|
1381
|
+
nextAfter,
|
|
1382
|
+
limit,
|
|
1383
|
+
hasMoreField,
|
|
1384
|
+
hasMore,
|
|
1385
|
+
serverHead,
|
|
1386
|
+
pendingAckSeq,
|
|
1387
|
+
});
|
|
1215
1388
|
if (!shouldContinue)
|
|
1216
1389
|
break;
|
|
1390
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1391
|
+
const nextKey = pullGateKeyForClient(client, 'message.v2.pull', {
|
|
1392
|
+
after_seq: nextAfter, force: opts?.force === true, limit,
|
|
1393
|
+
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1394
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1395
|
+
pullGateKey = nextKey;
|
|
1396
|
+
}
|
|
1217
1397
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1218
1398
|
if (tailDelayMs > 0)
|
|
1219
1399
|
await v2Sleep(tailDelayMs);
|
|
@@ -1222,13 +1402,34 @@ export class V2E2EECoordinator {
|
|
|
1222
1402
|
if (pageCount >= maxPages) {
|
|
1223
1403
|
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
1224
1404
|
if (pendingAckSeq > 0) {
|
|
1225
|
-
await this.
|
|
1405
|
+
await this.confirmForwardP2PAck(pendingAckSeq);
|
|
1226
1406
|
pendingAckSeq = 0;
|
|
1227
1407
|
}
|
|
1228
1408
|
}
|
|
1229
1409
|
return decrypted;
|
|
1230
1410
|
}
|
|
1411
|
+
async confirmForwardP2PAck(upToSeq, inlineGeneration) {
|
|
1412
|
+
const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
|
|
1413
|
+
try {
|
|
1414
|
+
const result = await this.ackV2ForForward(upToSeq, inlineGeneration);
|
|
1415
|
+
const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
|
|
1416
|
+
if (actualAckSeq < upToSeq) {
|
|
1417
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
1418
|
+
}
|
|
1419
|
+
this.clearForwardAck(ns, upToSeq);
|
|
1420
|
+
}
|
|
1421
|
+
catch (exc) {
|
|
1422
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1423
|
+
throw exc;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1231
1426
|
async ackV2(upToSeq, inlineGeneration) {
|
|
1427
|
+
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false);
|
|
1428
|
+
}
|
|
1429
|
+
async ackV2ForForward(upToSeq, inlineGeneration) {
|
|
1430
|
+
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, true);
|
|
1431
|
+
}
|
|
1432
|
+
async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark) {
|
|
1232
1433
|
const client = this.client;
|
|
1233
1434
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1234
1435
|
let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
|
|
@@ -1237,23 +1438,33 @@ export class V2E2EECoordinator {
|
|
|
1237
1438
|
seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
|
|
1238
1439
|
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
1239
1440
|
return { acked: 0 };
|
|
1441
|
+
// ACK 发出前绑定当时的 inline pending;迟到响应不得结算重置后新建的 pending。
|
|
1442
|
+
const ackGeneration = inlineGeneration ?? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null;
|
|
1443
|
+
const v2SessionAtStart = client._v2Session;
|
|
1444
|
+
const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
|
|
1240
1445
|
const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
|
|
1241
1446
|
const result = isJsonObject(raw)
|
|
1242
1447
|
? { ...raw }
|
|
1243
1448
|
: { result: raw };
|
|
1244
|
-
const actualAckSeq =
|
|
1449
|
+
const actualAckSeq = requireServerWatermark
|
|
1450
|
+
? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
|
|
1451
|
+
: (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq);
|
|
1452
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
1453
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1454
|
+
}
|
|
1245
1455
|
result.ack_seq = actualAckSeq;
|
|
1246
1456
|
result.success = true;
|
|
1247
|
-
if (
|
|
1248
|
-
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq);
|
|
1249
|
-
}
|
|
1250
|
-
else {
|
|
1251
|
-
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
|
|
1457
|
+
if (ackGeneration !== null) {
|
|
1458
|
+
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
|
|
1252
1459
|
}
|
|
1253
|
-
if (
|
|
1460
|
+
if (v2SessionAtStart
|
|
1461
|
+
&& client._v2Session === v2SessionAtStart
|
|
1462
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
|
|
1254
1463
|
try {
|
|
1255
|
-
const destroyed = await
|
|
1256
|
-
if (
|
|
1464
|
+
const destroyed = await v2SessionAtStart.maybeDestroyOldSPKs(actualAckSeq);
|
|
1465
|
+
if (client._v2Session === v2SessionAtStart
|
|
1466
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart
|
|
1467
|
+
&& destroyed.length > 0) {
|
|
1257
1468
|
client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3)} (PFS)`);
|
|
1258
1469
|
}
|
|
1259
1470
|
}
|
|
@@ -1334,7 +1545,7 @@ export class V2E2EECoordinator {
|
|
|
1334
1545
|
throw exc;
|
|
1335
1546
|
}
|
|
1336
1547
|
}
|
|
1337
|
-
async pullGroupV2TailInternal(params) {
|
|
1548
|
+
async pullGroupV2TailInternal(params, deferPublish = false) {
|
|
1338
1549
|
const client = this.client;
|
|
1339
1550
|
if (params.window_mode !== 'tail')
|
|
1340
1551
|
throw new ValidationError('window_mode must equal tail');
|
|
@@ -1356,7 +1567,7 @@ export class V2E2EECoordinator {
|
|
|
1356
1567
|
const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
|
|
1357
1568
|
const previous = client._seqTracker.getSyncState(ns);
|
|
1358
1569
|
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1359
|
-
const page = validateTailPage(result, limit);
|
|
1570
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1360
1571
|
client._seqTracker.commitTailWindow(ns, {
|
|
1361
1572
|
windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
|
|
1362
1573
|
});
|
|
@@ -1364,7 +1575,8 @@ export class V2E2EECoordinator {
|
|
|
1364
1575
|
await client._saveSeqTrackerState?.();
|
|
1365
1576
|
const decoded = [];
|
|
1366
1577
|
for (const msg of page.messages) {
|
|
1367
|
-
|
|
1578
|
+
client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
1579
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
|
|
1368
1580
|
if (plaintext)
|
|
1369
1581
|
decoded.push(plaintext);
|
|
1370
1582
|
}
|
|
@@ -1411,22 +1623,52 @@ export class V2E2EECoordinator {
|
|
|
1411
1623
|
let groupAid = String(opts?.groupAid ?? '').trim();
|
|
1412
1624
|
const ns = `group:${gid}`;
|
|
1413
1625
|
if (opts?.windowMode === 'tail') {
|
|
1626
|
+
if (!opts.gateLocked) {
|
|
1627
|
+
const key = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
1628
|
+
group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
|
|
1629
|
+
_group_cursor_params: opts?.cursorParams,
|
|
1630
|
+
}, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
|
|
1631
|
+
return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1632
|
+
...(opts ?? {}),
|
|
1633
|
+
gateLocked: true,
|
|
1634
|
+
}), false);
|
|
1635
|
+
}
|
|
1414
1636
|
const result = await this.pullGroupV2TailInternal({
|
|
1415
1637
|
group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
|
|
1416
1638
|
window_mode: 'tail', after_seq: afterSeq, limit,
|
|
1417
1639
|
});
|
|
1418
1640
|
return Array.isArray(result.messages) ? result.messages : [];
|
|
1419
1641
|
}
|
|
1642
|
+
if (!opts?.gateLocked) {
|
|
1643
|
+
const key = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
1644
|
+
group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
|
|
1645
|
+
_group_cursor_params: opts?.cursorParams,
|
|
1646
|
+
}, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1647
|
+
return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1648
|
+
...(opts ?? {}),
|
|
1649
|
+
gateLocked: true,
|
|
1650
|
+
}), true);
|
|
1651
|
+
}
|
|
1420
1652
|
const decrypted = [];
|
|
1421
1653
|
const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
|
|
1422
1654
|
const cursorParams = opts?.cursorParams ?? {};
|
|
1423
1655
|
const ownsCursor = opts?.ownsCursor !== false;
|
|
1656
|
+
let pullGateKey = ownsCursor
|
|
1657
|
+
? pullGateKeyForClient(client, 'group.v2.pull', {
|
|
1658
|
+
group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
|
|
1659
|
+
_group_cursor_params: cursorParams,
|
|
1660
|
+
}, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`)
|
|
1661
|
+
: '';
|
|
1424
1662
|
let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
|
|
1425
1663
|
? afterSeq
|
|
1426
1664
|
: (afterSeq || client._seqTracker.getContiguousSeq(ns));
|
|
1665
|
+
const deferredServerCursor = ownsCursor
|
|
1666
|
+
? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns))
|
|
1667
|
+
: 0;
|
|
1668
|
+
const deferredForwardAck = ownsCursor ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1427
1669
|
let pageCount = 0;
|
|
1428
1670
|
let lastAutoAckSeq = 0;
|
|
1429
|
-
let pendingAckSeq =
|
|
1671
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1430
1672
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1431
1673
|
while (pageCount < maxPages) {
|
|
1432
1674
|
pageCount += 1;
|
|
@@ -1443,23 +1685,21 @@ export class V2E2EECoordinator {
|
|
|
1443
1685
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1444
1686
|
}, groupAid));
|
|
1445
1687
|
if (ackUpToSeq > 0) {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
? result.cursor
|
|
1450
|
-
: null;
|
|
1451
|
-
if (ackCursor && Object.prototype.hasOwnProperty.call(ackCursor, 'current_seq')) {
|
|
1452
|
-
actualAckSeq = typeof ackCursor.current_seq === 'number'
|
|
1453
|
-
&& Number.isSafeInteger(ackCursor.current_seq) && ackCursor.current_seq >= 0
|
|
1454
|
-
? ackCursor.current_seq
|
|
1455
|
-
: 0;
|
|
1456
|
-
}
|
|
1457
|
-
if (actualAckSeq > 0) {
|
|
1688
|
+
const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
|
|
1689
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1690
|
+
if (pendingInlineGeneration !== null) {
|
|
1458
1691
|
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
|
|
1459
1692
|
}
|
|
1693
|
+
pendingAckSeq = 0;
|
|
1694
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1695
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1696
|
+
}
|
|
1697
|
+
else {
|
|
1698
|
+
client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1699
|
+
await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
|
|
1700
|
+
pendingAckSeq = 0;
|
|
1701
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1460
1702
|
}
|
|
1461
|
-
pendingAckSeq = 0;
|
|
1462
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1463
1703
|
}
|
|
1464
1704
|
const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1465
1705
|
if (resultGroupAid)
|
|
@@ -1467,6 +1707,10 @@ export class V2E2EECoordinator {
|
|
|
1467
1707
|
const eventGroupAid = resultGroupAid || groupAid || gid;
|
|
1468
1708
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1469
1709
|
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1710
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1711
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1712
|
+
const hasMore = result.has_more === true;
|
|
1713
|
+
const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
|
|
1470
1714
|
const seqs = messages
|
|
1471
1715
|
.map((msg) => Number(msg.seq ?? 0))
|
|
1472
1716
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
@@ -1475,6 +1719,8 @@ export class V2E2EECoordinator {
|
|
|
1475
1719
|
? client._seqTracker.snapshotNamespace(ns)
|
|
1476
1720
|
: null;
|
|
1477
1721
|
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
1722
|
+
const deferredKeyFetches = new Map();
|
|
1723
|
+
let blockedSeq = 0;
|
|
1478
1724
|
for (const msg of messages) {
|
|
1479
1725
|
const seq = Number(msg.seq ?? 0);
|
|
1480
1726
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -1510,7 +1756,7 @@ export class V2E2EECoordinator {
|
|
|
1510
1756
|
};
|
|
1511
1757
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1512
1758
|
attachGatewayProximity(v1Msg, msg);
|
|
1513
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1759
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1514
1760
|
decrypted.push(v1Msg);
|
|
1515
1761
|
continue;
|
|
1516
1762
|
}
|
|
@@ -1529,7 +1775,7 @@ export class V2E2EECoordinator {
|
|
|
1529
1775
|
};
|
|
1530
1776
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1531
1777
|
attachGatewayProximity(v1Msg, msg);
|
|
1532
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1778
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1533
1779
|
decrypted.push(v1Msg);
|
|
1534
1780
|
continue;
|
|
1535
1781
|
}
|
|
@@ -1540,56 +1786,87 @@ export class V2E2EECoordinator {
|
|
|
1540
1786
|
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1541
1787
|
continue;
|
|
1542
1788
|
}
|
|
1543
|
-
|
|
1789
|
+
const deferStatus = {};
|
|
1790
|
+
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1791
|
+
if (deferStatus.deferred) {
|
|
1792
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1793
|
+
}
|
|
1794
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1795
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1796
|
+
deferredKeyFetches.set(key, {
|
|
1797
|
+
fromAid: deferStatus.fromAid,
|
|
1798
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1799
|
+
groupId: deferStatus.groupId ?? '',
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1544
1802
|
if (plaintext === null)
|
|
1545
1803
|
continue;
|
|
1546
1804
|
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1547
1805
|
plaintext.group_id = gid;
|
|
1548
1806
|
plaintext.group_aid = eventGroupAid;
|
|
1549
|
-
await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
|
|
1807
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
1550
1808
|
decrypted.push(plaintext);
|
|
1551
1809
|
}
|
|
1552
|
-
const
|
|
1553
|
-
const
|
|
1810
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
1811
|
+
const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
|
|
1812
|
+
const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
|
|
1554
1813
|
const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
|
|
1555
1814
|
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
1556
1815
|
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
1557
|
-
const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1816
|
+
const commitTarget = blockedSeq > 0 ? pageContigBefore : Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1558
1817
|
let ackSeq = pageContigBefore;
|
|
1559
1818
|
let contigAdvanced = false;
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1819
|
+
if (ownsCursor) {
|
|
1820
|
+
try {
|
|
1821
|
+
if (commitTarget > pageContigBefore) {
|
|
1822
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1823
|
+
if (effectiveFloor > pageContigBefore) {
|
|
1824
|
+
client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
|
|
1825
|
+
}
|
|
1826
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1827
|
+
client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
|
|
1828
|
+
}
|
|
1829
|
+
if (seqs.length > 0) {
|
|
1830
|
+
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1831
|
+
}
|
|
1568
1832
|
}
|
|
1569
|
-
}
|
|
1570
|
-
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1571
|
-
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1572
|
-
if (contigAdvanced) {
|
|
1573
|
-
await client._drainOrderedMessages(ns);
|
|
1574
1833
|
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1575
1834
|
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1576
|
-
|
|
1835
|
+
if (contigAdvanced) {
|
|
1836
|
+
await client._commitSeqTrackerState(ns);
|
|
1837
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1838
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1839
|
+
}
|
|
1577
1840
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1841
|
+
catch (exc) {
|
|
1842
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1843
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1844
|
+
}
|
|
1845
|
+
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1846
|
+
throw exc;
|
|
1582
1847
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1848
|
+
if (contigAdvanced)
|
|
1849
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1585
1850
|
}
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1851
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1852
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1853
|
+
}
|
|
1854
|
+
if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
|
|
1855
|
+
this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
|
|
1856
|
+
}
|
|
1857
|
+
// 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
|
|
1858
|
+
// 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
|
|
1859
|
+
const knownServerCursorSeq = hasServerCursor
|
|
1860
|
+
? cursorCurrentSeq
|
|
1861
|
+
: (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
|
|
1862
|
+
const ackNeeded = blockedSeq <= 0
|
|
1588
1863
|
&& ackSeq > 0
|
|
1589
1864
|
&& ackSeq > lastAutoAckSeq
|
|
1590
1865
|
&& ownsCursor
|
|
1591
|
-
&& (
|
|
1866
|
+
&& ((hasServerCursor && ackSeq > cursorCurrentSeq)
|
|
1867
|
+
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
1592
1868
|
if (ackNeeded) {
|
|
1869
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1593
1870
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1594
1871
|
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
|
|
1595
1872
|
if (canContinuePage) {
|
|
@@ -1597,7 +1874,7 @@ export class V2E2EECoordinator {
|
|
|
1597
1874
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1598
1875
|
}
|
|
1599
1876
|
else {
|
|
1600
|
-
await this.
|
|
1877
|
+
await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
|
|
1601
1878
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1602
1879
|
}
|
|
1603
1880
|
}
|
|
@@ -1605,11 +1882,28 @@ export class V2E2EECoordinator {
|
|
|
1605
1882
|
if (!ownsCursor)
|
|
1606
1883
|
break;
|
|
1607
1884
|
const rawCount = messages.length;
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
const shouldContinue =
|
|
1885
|
+
if (blockedSeq > 0)
|
|
1886
|
+
break;
|
|
1887
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1888
|
+
rawCount,
|
|
1889
|
+
nextAfterSeq,
|
|
1890
|
+
nextAfter,
|
|
1891
|
+
limit,
|
|
1892
|
+
hasMoreField,
|
|
1893
|
+
hasMore,
|
|
1894
|
+
serverHead,
|
|
1895
|
+
pendingAckSeq,
|
|
1896
|
+
});
|
|
1611
1897
|
if (!shouldContinue)
|
|
1612
1898
|
break;
|
|
1899
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1900
|
+
const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
1901
|
+
group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
|
|
1902
|
+
_group_cursor_params: cursorParams,
|
|
1903
|
+
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
1904
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1905
|
+
pullGateKey = nextKey;
|
|
1906
|
+
}
|
|
1613
1907
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1614
1908
|
if (tailDelayMs > 0)
|
|
1615
1909
|
await v2Sleep(tailDelayMs);
|
|
@@ -1618,13 +1912,34 @@ export class V2E2EECoordinator {
|
|
|
1618
1912
|
if (pageCount >= maxPages) {
|
|
1619
1913
|
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
1620
1914
|
if (pendingAckSeq > 0) {
|
|
1621
|
-
await this.
|
|
1915
|
+
await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
|
|
1622
1916
|
pendingAckSeq = 0;
|
|
1623
1917
|
}
|
|
1624
1918
|
}
|
|
1625
1919
|
return decrypted;
|
|
1626
1920
|
}
|
|
1921
|
+
async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
|
|
1922
|
+
const ns = `group:${String(groupId ?? '').trim()}`;
|
|
1923
|
+
try {
|
|
1924
|
+
const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
|
|
1925
|
+
const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
|
|
1926
|
+
if (actualAckSeq < upToSeq) {
|
|
1927
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
1928
|
+
}
|
|
1929
|
+
this.clearForwardAck(ns, upToSeq);
|
|
1930
|
+
}
|
|
1931
|
+
catch (exc) {
|
|
1932
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1933
|
+
throw exc;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1627
1936
|
async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
|
|
1937
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, false);
|
|
1938
|
+
}
|
|
1939
|
+
async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
|
|
1940
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, undefined, true);
|
|
1941
|
+
}
|
|
1942
|
+
async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark) {
|
|
1628
1943
|
const client = this.client;
|
|
1629
1944
|
const gid = typeof groupId === 'string' ? groupId.trim() : '';
|
|
1630
1945
|
if (!gid)
|
|
@@ -1644,13 +1959,17 @@ export class V2E2EECoordinator {
|
|
|
1644
1959
|
seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
|
|
1645
1960
|
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
1646
1961
|
return { acked: 0 };
|
|
1962
|
+
// 与 P2P 一致,必须将 ACK 响应限定到请求开始时对应的 inline generation。
|
|
1963
|
+
const ackGeneration = inlineGeneration ?? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null;
|
|
1647
1964
|
const raw = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
|
|
1648
|
-
const actualAckSeq =
|
|
1649
|
-
|
|
1650
|
-
client._delivery?.
|
|
1965
|
+
const actualAckSeq = requireServerWatermark
|
|
1966
|
+
? (client._delivery?.resolveForwardAckSeq?.(raw, seq) ?? 0)
|
|
1967
|
+
: (client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq);
|
|
1968
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
1969
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1651
1970
|
}
|
|
1652
|
-
|
|
1653
|
-
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq,
|
|
1971
|
+
if (ackGeneration !== null) {
|
|
1972
|
+
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
|
|
1654
1973
|
}
|
|
1655
1974
|
return raw;
|
|
1656
1975
|
}
|
|
@@ -1830,7 +2149,7 @@ export class V2E2EECoordinator {
|
|
|
1830
2149
|
let e2ee;
|
|
1831
2150
|
if (payload?.type === 'e2ee.group_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1832
2151
|
e2ee = v2E2eeMeta(payload);
|
|
1833
|
-
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
2152
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
|
|
1834
2153
|
if (plain === null) {
|
|
1835
2154
|
decryptFailed = true;
|
|
1836
2155
|
client._clientLog.debug(`group.thought.get decrypt returned null: group=${groupId}, thought_id=${thoughtId}, from=${fromAid}`);
|
|
@@ -1900,7 +2219,7 @@ export class V2E2EECoordinator {
|
|
|
1900
2219
|
let e2ee;
|
|
1901
2220
|
if (payload?.type === 'e2ee.p2p_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1902
2221
|
e2ee = v2E2eeMeta(payload);
|
|
1903
|
-
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
2222
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
|
|
1904
2223
|
if (plain === null) {
|
|
1905
2224
|
decryptFailed = true;
|
|
1906
2225
|
client._clientLog.debug(`message.thought.get decrypt returned null: thought_id=${thoughtId}, from=${fromAid}, to=${toAid}`);
|
|
@@ -1987,7 +2306,10 @@ export class V2E2EECoordinator {
|
|
|
1987
2306
|
const fromAid = String(opts.fromAid || aad.from || '').trim();
|
|
1988
2307
|
const senderDeviceId = String(aad.from_device ?? '');
|
|
1989
2308
|
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
1990
|
-
|
|
2309
|
+
let senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
2310
|
+
if (!senderPubDer && opts.fetchSenderIK) {
|
|
2311
|
+
senderPubDer = await this.fetchSenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
|
|
2312
|
+
}
|
|
1991
2313
|
if (!senderPubDer) {
|
|
1992
2314
|
client._clientLog.warn(`V2 thought decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
1993
2315
|
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupIdForKeys);
|