@agentunion/fastaun-browser 0.5.6 → 0.5.8
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 +74 -0
- package/_packed_docs/CHANGELOG.md +74 -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/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/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
- 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 +229 -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 +26 -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 +29895 -21968
- 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 +33 -5
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +597 -220
- package/dist/client/delivery.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 +452 -151
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +17 -6
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +284 -115
- 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 +398 -114
- 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 +396 -104
- 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 +51 -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
|
@@ -21,6 +21,9 @@ function strictWindowSeq(value, field, positive = false) {
|
|
|
21
21
|
throw new ValidationError(`${field} must be >= ${minimum}`);
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
|
+
function safeNonNegativeSeq(value) {
|
|
25
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
26
|
+
}
|
|
24
27
|
function strictWindowLimit(value) {
|
|
25
28
|
const limit = strictWindowSeq(value, 'limit', true);
|
|
26
29
|
if (limit > V2_PULL_MAX_PAGE_LIMIT)
|
|
@@ -53,26 +56,50 @@ function deduplicateForwardPageMessages(value, afterSeq) {
|
|
|
53
56
|
if (!isJsonObject(raw))
|
|
54
57
|
continue;
|
|
55
58
|
const row = raw;
|
|
56
|
-
const seq =
|
|
57
|
-
if (
|
|
59
|
+
const seq = safeNonNegativeSeq(row.seq);
|
|
60
|
+
if (seq === null || seq <= 0 || seq <= afterSeq || bySeq.has(seq))
|
|
58
61
|
continue;
|
|
59
62
|
bySeq.set(seq, row);
|
|
60
63
|
}
|
|
61
64
|
return [...bySeq.entries()].sort(([a], [b]) => a - b).map(([, row]) => row);
|
|
62
65
|
}
|
|
63
|
-
function
|
|
66
|
+
function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, pendingAckSeq, }) {
|
|
67
|
+
// 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
|
|
68
|
+
if (rawCount <= 0 || nextAfter <= nextAfterSeq)
|
|
69
|
+
return false;
|
|
70
|
+
if (hasMore || serverHead > nextAfter || pendingAckSeq > 0)
|
|
71
|
+
return true;
|
|
72
|
+
// 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
|
|
73
|
+
return !hasMoreField && limit > 0 && rawCount >= limit;
|
|
74
|
+
}
|
|
75
|
+
function validateTailPage(result, afterSeq, pageLimit) {
|
|
64
76
|
if (!isJsonObject(result))
|
|
65
77
|
throw new ValidationError('tail response must be an object');
|
|
66
78
|
const row = result;
|
|
67
79
|
if (row.window_mode !== 'tail')
|
|
68
80
|
throw new ValidationError('tail response window_mode must equal tail');
|
|
81
|
+
strictWindowSeq(afterSeq, 'after_seq');
|
|
69
82
|
const windowStart = strictWindowSeq(row.window_start_seq, 'window_start_seq');
|
|
70
83
|
const head = strictWindowSeq(row.head_seq, 'head_seq');
|
|
71
84
|
const covered = strictWindowSeq(row.covered_through_seq, 'covered_through_seq');
|
|
72
85
|
if (windowStart > head || covered !== head || head - windowStart + 1 > pageLimit) {
|
|
73
86
|
throw new ValidationError('tail response proof is invalid');
|
|
74
87
|
}
|
|
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))
|
|
@@ -329,6 +356,9 @@ export class V2E2EECoordinator {
|
|
|
329
356
|
runtime;
|
|
330
357
|
targetSetCache = new Map();
|
|
331
358
|
senderIKSyncFetches = new Map();
|
|
359
|
+
/** 服务端 ACK cursor 高于本地 A 时,延迟到下一次 Forward Pull 再恢复。 */
|
|
360
|
+
pendingForwardCursors = new Map();
|
|
361
|
+
pendingForwardAcks = new Map();
|
|
332
362
|
v2SessionInitRetryTimer = null;
|
|
333
363
|
v2SessionInitRetryBackgroundSync = false;
|
|
334
364
|
v2SessionInitRetryIdentityKey = null;
|
|
@@ -338,6 +368,51 @@ export class V2E2EECoordinator {
|
|
|
338
368
|
get client() {
|
|
339
369
|
return this.runtime.client;
|
|
340
370
|
}
|
|
371
|
+
resetForwardCursorState() {
|
|
372
|
+
this.pendingForwardCursors.clear();
|
|
373
|
+
this.pendingForwardAcks.clear();
|
|
374
|
+
}
|
|
375
|
+
pendingForwardCursor(ns, localAck) {
|
|
376
|
+
if (!ns)
|
|
377
|
+
return 0;
|
|
378
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
379
|
+
if (pending <= localAck) {
|
|
380
|
+
if (pending > 0)
|
|
381
|
+
this.pendingForwardCursors.delete(ns);
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
return pending;
|
|
385
|
+
}
|
|
386
|
+
recordForwardCursor(ns, cursor, localAck) {
|
|
387
|
+
if (!ns || !Number.isSafeInteger(cursor) || cursor <= localAck)
|
|
388
|
+
return;
|
|
389
|
+
this.pendingForwardCursors.set(ns, Math.max(this.pendingForwardCursors.get(ns) ?? 0, cursor));
|
|
390
|
+
}
|
|
391
|
+
clearForwardCursor(ns, committedAck) {
|
|
392
|
+
const pending = this.pendingForwardCursors.get(ns) ?? 0;
|
|
393
|
+
if (pending > 0 && committedAck >= pending)
|
|
394
|
+
this.pendingForwardCursors.delete(ns);
|
|
395
|
+
}
|
|
396
|
+
pendingForwardAck(ns, localAck) {
|
|
397
|
+
if (!ns)
|
|
398
|
+
return 0;
|
|
399
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
400
|
+
if (!Number.isSafeInteger(pending) || pending <= 0 || localAck <= 0 || pending > localAck) {
|
|
401
|
+
this.pendingForwardAcks.delete(ns);
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
return pending;
|
|
405
|
+
}
|
|
406
|
+
recordForwardAck(ns, ackSeq) {
|
|
407
|
+
if (!ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0)
|
|
408
|
+
return;
|
|
409
|
+
this.pendingForwardAcks.set(ns, Math.max(this.pendingForwardAcks.get(ns) ?? 0, ackSeq));
|
|
410
|
+
}
|
|
411
|
+
clearForwardAck(ns, confirmedSeq) {
|
|
412
|
+
const pending = this.pendingForwardAcks.get(ns) ?? 0;
|
|
413
|
+
if (pending > 0 && confirmedSeq >= pending)
|
|
414
|
+
this.pendingForwardAcks.delete(ns);
|
|
415
|
+
}
|
|
341
416
|
get bootstrapCache() {
|
|
342
417
|
const client = this.client;
|
|
343
418
|
if (!(client._v2BootstrapCache instanceof Map)) {
|
|
@@ -759,7 +834,8 @@ export class V2E2EECoordinator {
|
|
|
759
834
|
createdAt: Date.now(),
|
|
760
835
|
});
|
|
761
836
|
client._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || '-'} group=${groupId || '<p2p>'} pending=${client._v2SenderIKPending.size}`);
|
|
762
|
-
|
|
837
|
+
if (args.startFetch !== false)
|
|
838
|
+
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupId);
|
|
763
839
|
}
|
|
764
840
|
scheduleSenderIKFetch(fromAid, senderDeviceId, groupId) {
|
|
765
841
|
const client = this.client;
|
|
@@ -810,8 +886,9 @@ export class V2E2EECoordinator {
|
|
|
810
886
|
const pendingItems = [...client._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
|
|
811
887
|
for (const [key, entry] of pendingItems) {
|
|
812
888
|
let plaintext = null;
|
|
889
|
+
const retryStatus = {};
|
|
813
890
|
try {
|
|
814
|
-
plaintext = await client._decryptV2Message(entry.msg, false);
|
|
891
|
+
plaintext = await client._decryptV2Message(entry.msg, false, true, true, true, retryStatus);
|
|
815
892
|
}
|
|
816
893
|
catch (exc) {
|
|
817
894
|
client._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatE2EEError(exc))}`);
|
|
@@ -819,6 +896,10 @@ export class V2E2EECoordinator {
|
|
|
819
896
|
continue;
|
|
820
897
|
}
|
|
821
898
|
if (plaintext === null) {
|
|
899
|
+
if (retryStatus.deferred) {
|
|
900
|
+
client._clientLog.warn(`V2 pending retry still missing key material: key=${key}`);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
822
903
|
client._clientLog.warn(`V2 sender IK pending retry still undecryptable: key=${key}`);
|
|
823
904
|
const isGroup = Boolean(entry.groupId);
|
|
824
905
|
const eventName = isGroup ? 'group.message_undecryptable' : 'message.undecryptable';
|
|
@@ -907,7 +988,7 @@ export class V2E2EECoordinator {
|
|
|
907
988
|
history_error: { code: 'decrypt_failed', message: reason },
|
|
908
989
|
};
|
|
909
990
|
}
|
|
910
|
-
async decodeP2PWindowMessage(msg, publish, history, source) {
|
|
991
|
+
async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
|
|
911
992
|
const client = this.client;
|
|
912
993
|
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
913
994
|
let plaintext = null;
|
|
@@ -939,16 +1020,27 @@ export class V2E2EECoordinator {
|
|
|
939
1020
|
}
|
|
940
1021
|
if (publish) {
|
|
941
1022
|
const event = client._delivery.p2pAppEventForMessage(plaintext);
|
|
942
|
-
|
|
1023
|
+
if (orderedPublish) {
|
|
1024
|
+
await client._delivery.publishOrderedMessage(event.event, client._aid ? `p2p:${client._aid}` : '', seq, event.payload);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
await client._publishAppEvent(event.event, event.payload, source);
|
|
1028
|
+
}
|
|
943
1029
|
}
|
|
944
1030
|
return plaintext;
|
|
945
1031
|
}
|
|
946
|
-
async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source) {
|
|
1032
|
+
async decodeGroupWindowMessage(msg, groupId, groupAid, publish, history, source, orderedPublish = false) {
|
|
947
1033
|
const client = this.client;
|
|
948
1034
|
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
949
1035
|
let plaintext = null;
|
|
950
1036
|
if (String(msg.version ?? '') === 'v1') {
|
|
951
1037
|
const payload = msg.payload;
|
|
1038
|
+
if (payload === undefined || payload === null) {
|
|
1039
|
+
if (history)
|
|
1040
|
+
return this.historyDecryptFailure(msg, 'legacy payload is missing');
|
|
1041
|
+
client._clientLog.warn(`Group Tail 缺少 payload 的 V1 行已跳过,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
952
1044
|
if (isJsonObject(payload)
|
|
953
1045
|
&& ['e2ee.encrypted', 'e2ee.group_encrypted'].includes(String(payload.type ?? ''))) {
|
|
954
1046
|
if (history)
|
|
@@ -985,8 +1077,14 @@ export class V2E2EECoordinator {
|
|
|
985
1077
|
client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
986
1078
|
return null;
|
|
987
1079
|
}
|
|
988
|
-
if (publish)
|
|
989
|
-
|
|
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
|
+
}
|
|
990
1088
|
return plaintext;
|
|
991
1089
|
}
|
|
992
1090
|
async decodeInlineP2PMessage(msg) {
|
|
@@ -1003,7 +1101,7 @@ export class V2E2EECoordinator {
|
|
|
1003
1101
|
client._persistSeq(ns);
|
|
1004
1102
|
await client._saveSeqTrackerState?.();
|
|
1005
1103
|
}
|
|
1006
|
-
async pullV2TailInternal(params) {
|
|
1104
|
+
async pullV2TailInternal(params, deferPublish = false) {
|
|
1007
1105
|
const client = this.client;
|
|
1008
1106
|
if (params.window_mode !== 'tail')
|
|
1009
1107
|
throw new ValidationError('window_mode must equal tail');
|
|
@@ -1016,7 +1114,7 @@ export class V2E2EECoordinator {
|
|
|
1016
1114
|
const floor = Number(result.retention_floor_seq ?? 0);
|
|
1017
1115
|
const previous = ns ? client._seqTracker.getSyncState(ns) : null;
|
|
1018
1116
|
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1019
|
-
const page = validateTailPage(result, limit);
|
|
1117
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1020
1118
|
if (ns) {
|
|
1021
1119
|
client._seqTracker.commitTailWindow(ns, {
|
|
1022
1120
|
windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
|
|
@@ -1026,7 +1124,7 @@ export class V2E2EECoordinator {
|
|
|
1026
1124
|
}
|
|
1027
1125
|
const decoded = [];
|
|
1028
1126
|
for (const msg of page.messages) {
|
|
1029
|
-
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail');
|
|
1127
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
|
|
1030
1128
|
if (plaintext)
|
|
1031
1129
|
decoded.push(plaintext);
|
|
1032
1130
|
}
|
|
@@ -1053,14 +1151,33 @@ export class V2E2EECoordinator {
|
|
|
1053
1151
|
await client._ensureV2SessionReady('message.pull');
|
|
1054
1152
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1055
1153
|
if (opts?.windowMode === 'tail') {
|
|
1154
|
+
if (!opts.gateLocked) {
|
|
1155
|
+
const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
|
|
1156
|
+
return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
|
|
1157
|
+
...(opts ?? {}),
|
|
1158
|
+
gateLocked: true,
|
|
1159
|
+
}), false);
|
|
1160
|
+
}
|
|
1056
1161
|
const result = await this.pullV2TailInternal({ window_mode: 'tail', after_seq: afterSeq, limit });
|
|
1057
1162
|
return Array.isArray(result.messages) ? result.messages : [];
|
|
1058
1163
|
}
|
|
1164
|
+
if (ns && !opts?.gateLocked) {
|
|
1165
|
+
const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1166
|
+
return await client._runPullSerialized(key, () => this.pullV2(afterSeq, limit, {
|
|
1167
|
+
...(opts ?? {}),
|
|
1168
|
+
gateLocked: true,
|
|
1169
|
+
}), true);
|
|
1170
|
+
}
|
|
1059
1171
|
const decrypted = [];
|
|
1060
1172
|
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? client._seqTracker.getContiguousSeq(ns) : 0));
|
|
1173
|
+
let pullGateKey = ns
|
|
1174
|
+
? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
|
|
1175
|
+
: '';
|
|
1176
|
+
const deferredServerCursor = ns ? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1177
|
+
const deferredForwardAck = ns ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1061
1178
|
let pageCount = 0;
|
|
1062
1179
|
let lastAutoAckSeq = 0;
|
|
1063
|
-
let pendingAckSeq =
|
|
1180
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1064
1181
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1065
1182
|
while (pageCount < maxPages) {
|
|
1066
1183
|
pageCount += 1;
|
|
@@ -1075,24 +1192,29 @@ export class V2E2EECoordinator {
|
|
|
1075
1192
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1076
1193
|
});
|
|
1077
1194
|
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) {
|
|
1195
|
+
const actualAckSeq = client._delivery?.resolveP2PPullAckSeq?.(result, ackUpToSeq) ?? 0;
|
|
1196
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1197
|
+
if (pendingInlineGeneration !== null) {
|
|
1087
1198
|
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
|
|
1088
1199
|
}
|
|
1200
|
+
pendingAckSeq = 0;
|
|
1201
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1202
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1206
|
+
await this.confirmForwardP2PAck(ackUpToSeq, pendingInlineGeneration ?? undefined);
|
|
1207
|
+
pendingAckSeq = 0;
|
|
1208
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1089
1209
|
}
|
|
1090
|
-
pendingAckSeq = 0;
|
|
1091
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1092
1210
|
}
|
|
1093
1211
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1094
1212
|
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1095
|
-
|
|
1213
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1214
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1215
|
+
const hasMore = result.has_more === true;
|
|
1216
|
+
const serverHead = safeNonNegativeSeq(cursor?.latest_seq) ?? 0;
|
|
1217
|
+
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
1218
|
const seqs = messages
|
|
1097
1219
|
.map((msg) => Number(msg.seq ?? 0))
|
|
1098
1220
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
@@ -1101,6 +1223,7 @@ export class V2E2EECoordinator {
|
|
|
1101
1223
|
? client._seqTracker.snapshotNamespace(ns)
|
|
1102
1224
|
: null;
|
|
1103
1225
|
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
1226
|
+
const deferredKeyFetches = new Map();
|
|
1104
1227
|
for (const msg of messages) {
|
|
1105
1228
|
const seq = Number(msg.seq ?? 0);
|
|
1106
1229
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -1127,7 +1250,7 @@ export class V2E2EECoordinator {
|
|
|
1127
1250
|
attachGatewayProximity(v1Msg, msg);
|
|
1128
1251
|
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
1129
1252
|
if (ns)
|
|
1130
|
-
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload);
|
|
1253
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1131
1254
|
else
|
|
1132
1255
|
await client._publishAppEvent(appEvent.event, appEvent.payload);
|
|
1133
1256
|
decrypted.push(v1Msg);
|
|
@@ -1145,11 +1268,20 @@ export class V2E2EECoordinator {
|
|
|
1145
1268
|
if (msgSpkId && client._v2Session && !client._v2Session.isCurrentSPK(msgSpkId)) {
|
|
1146
1269
|
client._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
|
|
1147
1270
|
}
|
|
1148
|
-
const
|
|
1271
|
+
const deferStatus = {};
|
|
1272
|
+
const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1273
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1274
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1275
|
+
deferredKeyFetches.set(key, {
|
|
1276
|
+
fromAid: deferStatus.fromAid,
|
|
1277
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1278
|
+
groupId: deferStatus.groupId ?? '',
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1149
1281
|
if (plaintext === null)
|
|
1150
1282
|
continue;
|
|
1151
1283
|
if (ns) {
|
|
1152
|
-
await client._publishPulledMessage('message.received', ns, seq, plaintext);
|
|
1284
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
1153
1285
|
decrypted.push(plaintext);
|
|
1154
1286
|
}
|
|
1155
1287
|
else {
|
|
@@ -1158,10 +1290,11 @@ export class V2E2EECoordinator {
|
|
|
1158
1290
|
}
|
|
1159
1291
|
}
|
|
1160
1292
|
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
1161
|
-
const
|
|
1293
|
+
const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
|
|
1294
|
+
const serverAckSeq = parsedServerAckSeq ?? 0;
|
|
1162
1295
|
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
1163
1296
|
if (ns) {
|
|
1164
|
-
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1297
|
+
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1165
1298
|
let ackSeq = pageContigBefore;
|
|
1166
1299
|
let contigAdvanced = false;
|
|
1167
1300
|
try {
|
|
@@ -1170,6 +1303,9 @@ export class V2E2EECoordinator {
|
|
|
1170
1303
|
if (retentionFloor > pageContigBefore) {
|
|
1171
1304
|
client._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${pageContigBefore} -> retention_floor_seq=${retentionFloor}`);
|
|
1172
1305
|
}
|
|
1306
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1307
|
+
client._clientLog.info(`message.v2.pull restore local A from deferred server ACK cursor: ns=${ns}, contiguous=${pageContigBefore} -> server_ack_seq=${deferredServerCursor}`);
|
|
1308
|
+
}
|
|
1173
1309
|
if (seqs.length > 0) {
|
|
1174
1310
|
client._clientLog.debug(`message.v2.pull force contiguous: ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1175
1311
|
}
|
|
@@ -1177,10 +1313,9 @@ export class V2E2EECoordinator {
|
|
|
1177
1313
|
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1178
1314
|
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1179
1315
|
if (contigAdvanced) {
|
|
1180
|
-
await client._drainOrderedMessages(ns);
|
|
1181
|
-
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1182
|
-
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1183
1316
|
await client._commitSeqTrackerState(ns);
|
|
1317
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1318
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1184
1319
|
}
|
|
1185
1320
|
}
|
|
1186
1321
|
catch (exc) {
|
|
@@ -1190,11 +1325,25 @@ export class V2E2EECoordinator {
|
|
|
1190
1325
|
client._clientLog.error?.(`message.v2.pull Forward commit rolled back: ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1191
1326
|
throw exc;
|
|
1192
1327
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1328
|
+
if (contigAdvanced)
|
|
1329
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1330
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1331
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1332
|
+
}
|
|
1333
|
+
if (parsedServerAckSeq !== null && serverAckSeq > ackSeq) {
|
|
1334
|
+
this.recordForwardCursor(ns, serverAckSeq, ackSeq);
|
|
1335
|
+
}
|
|
1336
|
+
// 无本页服务端水位时,只有本页成功提交了比已知服务端更高的 A
|
|
1337
|
+
// 才新增 ACK 义务。空页不能仅因本地已有 A 而重复 ACK。
|
|
1338
|
+
const knownServerAckSeq = hasServerAckSeq
|
|
1339
|
+
? serverAckSeq
|
|
1340
|
+
: (pageCount === 1 ? deferredServerCursor : 0);
|
|
1341
|
+
const ackNeeded = ackSeq > 0
|
|
1195
1342
|
&& ackSeq > lastAutoAckSeq
|
|
1196
|
-
&& (
|
|
1343
|
+
&& ((hasServerAckSeq && ackSeq > serverAckSeq)
|
|
1344
|
+
|| (contigAdvanced && ackSeq > knownServerAckSeq));
|
|
1197
1345
|
if (ackNeeded) {
|
|
1346
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1198
1347
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1199
1348
|
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq;
|
|
1200
1349
|
if (canContinuePage) {
|
|
@@ -1202,18 +1351,30 @@ export class V2E2EECoordinator {
|
|
|
1202
1351
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1203
1352
|
}
|
|
1204
1353
|
else {
|
|
1205
|
-
await this.
|
|
1354
|
+
await this.confirmForwardP2PAck(ackSeq);
|
|
1206
1355
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1207
1356
|
}
|
|
1208
1357
|
}
|
|
1209
1358
|
}
|
|
1210
1359
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1211
1360
|
const rawCount = messages.length;
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1361
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1362
|
+
rawCount,
|
|
1363
|
+
nextAfterSeq,
|
|
1364
|
+
nextAfter,
|
|
1365
|
+
limit,
|
|
1366
|
+
hasMoreField,
|
|
1367
|
+
hasMore,
|
|
1368
|
+
serverHead,
|
|
1369
|
+
pendingAckSeq,
|
|
1370
|
+
});
|
|
1215
1371
|
if (!shouldContinue)
|
|
1216
1372
|
break;
|
|
1373
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1374
|
+
const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1375
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1376
|
+
pullGateKey = nextKey;
|
|
1377
|
+
}
|
|
1217
1378
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1218
1379
|
if (tailDelayMs > 0)
|
|
1219
1380
|
await v2Sleep(tailDelayMs);
|
|
@@ -1222,13 +1383,34 @@ export class V2E2EECoordinator {
|
|
|
1222
1383
|
if (pageCount >= maxPages) {
|
|
1223
1384
|
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
1224
1385
|
if (pendingAckSeq > 0) {
|
|
1225
|
-
await this.
|
|
1386
|
+
await this.confirmForwardP2PAck(pendingAckSeq);
|
|
1226
1387
|
pendingAckSeq = 0;
|
|
1227
1388
|
}
|
|
1228
1389
|
}
|
|
1229
1390
|
return decrypted;
|
|
1230
1391
|
}
|
|
1392
|
+
async confirmForwardP2PAck(upToSeq, inlineGeneration) {
|
|
1393
|
+
const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
|
|
1394
|
+
try {
|
|
1395
|
+
const result = await this.ackV2ForForward(upToSeq, inlineGeneration);
|
|
1396
|
+
const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
|
|
1397
|
+
if (actualAckSeq < upToSeq) {
|
|
1398
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
1399
|
+
}
|
|
1400
|
+
this.clearForwardAck(ns, upToSeq);
|
|
1401
|
+
}
|
|
1402
|
+
catch (exc) {
|
|
1403
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1404
|
+
throw exc;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1231
1407
|
async ackV2(upToSeq, inlineGeneration) {
|
|
1408
|
+
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, false);
|
|
1409
|
+
}
|
|
1410
|
+
async ackV2ForForward(upToSeq, inlineGeneration) {
|
|
1411
|
+
return this.ackV2WithConfirmation(upToSeq, inlineGeneration, true);
|
|
1412
|
+
}
|
|
1413
|
+
async ackV2WithConfirmation(upToSeq, inlineGeneration, requireServerWatermark) {
|
|
1232
1414
|
const client = this.client;
|
|
1233
1415
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1234
1416
|
let seq = upToSeq ?? (ns ? client._seqTracker.getContiguousSeq(ns) : 0);
|
|
@@ -1237,23 +1419,33 @@ export class V2E2EECoordinator {
|
|
|
1237
1419
|
seq = client._clampAckSeq('message.v2.ack', 'up_to_seq', ns, seq);
|
|
1238
1420
|
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
1239
1421
|
return { acked: 0 };
|
|
1422
|
+
// ACK 发出前绑定当时的 inline pending;迟到响应不得结算重置后新建的 pending。
|
|
1423
|
+
const ackGeneration = inlineGeneration ?? client._delivery?.pendingP2PInlineAckGeneration?.(ns) ?? null;
|
|
1424
|
+
const v2SessionAtStart = client._v2Session;
|
|
1425
|
+
const v2RuntimeGenerationAtStart = Number(client._v2RuntimeGeneration ?? 0);
|
|
1240
1426
|
const raw = await client._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
|
|
1241
1427
|
const result = isJsonObject(raw)
|
|
1242
1428
|
? { ...raw }
|
|
1243
1429
|
: { result: raw };
|
|
1244
|
-
const actualAckSeq =
|
|
1430
|
+
const actualAckSeq = requireServerWatermark
|
|
1431
|
+
? (client._delivery?.resolveForwardAckSeq?.(result, seq) ?? 0)
|
|
1432
|
+
: (client._delivery?.resolveActualAckSeq?.(result, seq) ?? seq);
|
|
1433
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
1434
|
+
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1435
|
+
}
|
|
1245
1436
|
result.ack_seq = actualAckSeq;
|
|
1246
1437
|
result.success = true;
|
|
1247
|
-
if (
|
|
1248
|
-
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq);
|
|
1249
|
-
}
|
|
1250
|
-
else {
|
|
1251
|
-
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, inlineGeneration);
|
|
1438
|
+
if (ackGeneration !== null) {
|
|
1439
|
+
client._delivery?.completePendingP2PInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
|
|
1252
1440
|
}
|
|
1253
|
-
if (
|
|
1441
|
+
if (v2SessionAtStart
|
|
1442
|
+
&& client._v2Session === v2SessionAtStart
|
|
1443
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart) {
|
|
1254
1444
|
try {
|
|
1255
|
-
const destroyed = await
|
|
1256
|
-
if (
|
|
1445
|
+
const destroyed = await v2SessionAtStart.maybeDestroyOldSPKs(actualAckSeq);
|
|
1446
|
+
if (client._v2Session === v2SessionAtStart
|
|
1447
|
+
&& Number(client._v2RuntimeGeneration ?? 0) === v2RuntimeGenerationAtStart
|
|
1448
|
+
&& destroyed.length > 0) {
|
|
1257
1449
|
client._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3)} (PFS)`);
|
|
1258
1450
|
}
|
|
1259
1451
|
}
|
|
@@ -1334,7 +1526,7 @@ export class V2E2EECoordinator {
|
|
|
1334
1526
|
throw exc;
|
|
1335
1527
|
}
|
|
1336
1528
|
}
|
|
1337
|
-
async pullGroupV2TailInternal(params) {
|
|
1529
|
+
async pullGroupV2TailInternal(params, deferPublish = false) {
|
|
1338
1530
|
const client = this.client;
|
|
1339
1531
|
if (params.window_mode !== 'tail')
|
|
1340
1532
|
throw new ValidationError('window_mode must equal tail');
|
|
@@ -1356,7 +1548,7 @@ export class V2E2EECoordinator {
|
|
|
1356
1548
|
const floor = Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0);
|
|
1357
1549
|
const previous = client._seqTracker.getSyncState(ns);
|
|
1358
1550
|
await this.applyTailFloor(ns, Number.isSafeInteger(floor) ? floor : 0);
|
|
1359
|
-
const page = validateTailPage(result, limit);
|
|
1551
|
+
const page = validateTailPage(result, afterSeq, limit);
|
|
1360
1552
|
client._seqTracker.commitTailWindow(ns, {
|
|
1361
1553
|
windowStartSeq: page.windowStart, headSeq: page.head, coveredThroughSeq: page.head, pageLimit: limit,
|
|
1362
1554
|
});
|
|
@@ -1364,7 +1556,8 @@ export class V2E2EECoordinator {
|
|
|
1364
1556
|
await client._saveSeqTrackerState?.();
|
|
1365
1557
|
const decoded = [];
|
|
1366
1558
|
for (const msg of page.messages) {
|
|
1367
|
-
|
|
1559
|
+
client._logMessageDebug?.('tail-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
1560
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
|
|
1368
1561
|
if (plaintext)
|
|
1369
1562
|
decoded.push(plaintext);
|
|
1370
1563
|
}
|
|
@@ -1411,22 +1604,43 @@ export class V2E2EECoordinator {
|
|
|
1411
1604
|
let groupAid = String(opts?.groupAid ?? '').trim();
|
|
1412
1605
|
const ns = `group:${gid}`;
|
|
1413
1606
|
if (opts?.windowMode === 'tail') {
|
|
1607
|
+
if (!opts.gateLocked) {
|
|
1608
|
+
const key = `${ns}|tail|after=${afterSeq}|limit=${limit}`;
|
|
1609
|
+
return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1610
|
+
...(opts ?? {}),
|
|
1611
|
+
gateLocked: true,
|
|
1612
|
+
}), false);
|
|
1613
|
+
}
|
|
1414
1614
|
const result = await this.pullGroupV2TailInternal({
|
|
1415
1615
|
group_id: String(opts.wireGroupId ?? gid), group_aid: groupAid || undefined,
|
|
1416
1616
|
window_mode: 'tail', after_seq: afterSeq, limit,
|
|
1417
1617
|
});
|
|
1418
1618
|
return Array.isArray(result.messages) ? result.messages : [];
|
|
1419
1619
|
}
|
|
1620
|
+
if (!opts?.gateLocked) {
|
|
1621
|
+
const key = `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1622
|
+
return await client._runPullSerialized(key, () => this.pullGroupV2(gid, afterSeq, limit, {
|
|
1623
|
+
...(opts ?? {}),
|
|
1624
|
+
gateLocked: true,
|
|
1625
|
+
}), true);
|
|
1626
|
+
}
|
|
1420
1627
|
const decrypted = [];
|
|
1421
1628
|
const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
|
|
1422
1629
|
const cursorParams = opts?.cursorParams ?? {};
|
|
1423
1630
|
const ownsCursor = opts?.ownsCursor !== false;
|
|
1631
|
+
let pullGateKey = ownsCursor
|
|
1632
|
+
? `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`
|
|
1633
|
+
: '';
|
|
1424
1634
|
let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
|
|
1425
1635
|
? afterSeq
|
|
1426
1636
|
: (afterSeq || client._seqTracker.getContiguousSeq(ns));
|
|
1637
|
+
const deferredServerCursor = ownsCursor
|
|
1638
|
+
? this.pendingForwardCursor(ns, client._seqTracker.getContiguousSeq(ns))
|
|
1639
|
+
: 0;
|
|
1640
|
+
const deferredForwardAck = ownsCursor ? this.pendingForwardAck(ns, client._seqTracker.getContiguousSeq(ns)) : 0;
|
|
1427
1641
|
let pageCount = 0;
|
|
1428
1642
|
let lastAutoAckSeq = 0;
|
|
1429
|
-
let pendingAckSeq =
|
|
1643
|
+
let pendingAckSeq = deferredForwardAck;
|
|
1430
1644
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1431
1645
|
while (pageCount < maxPages) {
|
|
1432
1646
|
pageCount += 1;
|
|
@@ -1443,23 +1657,21 @@ export class V2E2EECoordinator {
|
|
|
1443
1657
|
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1444
1658
|
}, groupAid));
|
|
1445
1659
|
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) {
|
|
1660
|
+
const actualAckSeq = client._delivery?.resolveGroupPullAckSeq?.(result, ackUpToSeq) ?? 0;
|
|
1661
|
+
if (actualAckSeq >= ackUpToSeq) {
|
|
1662
|
+
if (pendingInlineGeneration !== null) {
|
|
1458
1663
|
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, pendingInlineGeneration);
|
|
1459
1664
|
}
|
|
1665
|
+
pendingAckSeq = 0;
|
|
1666
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1667
|
+
this.clearForwardAck(ns, ackUpToSeq);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1671
|
+
await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
|
|
1672
|
+
pendingAckSeq = 0;
|
|
1673
|
+
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1460
1674
|
}
|
|
1461
|
-
pendingAckSeq = 0;
|
|
1462
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1463
1675
|
}
|
|
1464
1676
|
const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
1465
1677
|
if (resultGroupAid)
|
|
@@ -1467,6 +1679,10 @@ export class V2E2EECoordinator {
|
|
|
1467
1679
|
const eventGroupAid = resultGroupAid || groupAid || gid;
|
|
1468
1680
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1469
1681
|
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1682
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1683
|
+
const hasMoreField = Object.prototype.hasOwnProperty.call(result, 'has_more');
|
|
1684
|
+
const hasMore = result.has_more === true;
|
|
1685
|
+
const serverHead = Math.max(safeNonNegativeSeq(result.latest_message_seq) ?? 0, safeNonNegativeSeq(cursor?.latest_seq) ?? 0);
|
|
1470
1686
|
const seqs = messages
|
|
1471
1687
|
.map((msg) => Number(msg.seq ?? 0))
|
|
1472
1688
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
@@ -1475,6 +1691,7 @@ export class V2E2EECoordinator {
|
|
|
1475
1691
|
? client._seqTracker.snapshotNamespace(ns)
|
|
1476
1692
|
: null;
|
|
1477
1693
|
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
1694
|
+
const deferredKeyFetches = new Map();
|
|
1478
1695
|
for (const msg of messages) {
|
|
1479
1696
|
const seq = Number(msg.seq ?? 0);
|
|
1480
1697
|
if (!Number.isFinite(seq) || seq <= 0)
|
|
@@ -1510,7 +1727,7 @@ export class V2E2EECoordinator {
|
|
|
1510
1727
|
};
|
|
1511
1728
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1512
1729
|
attachGatewayProximity(v1Msg, msg);
|
|
1513
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1730
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1514
1731
|
decrypted.push(v1Msg);
|
|
1515
1732
|
continue;
|
|
1516
1733
|
}
|
|
@@ -1529,7 +1746,7 @@ export class V2E2EECoordinator {
|
|
|
1529
1746
|
};
|
|
1530
1747
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
1531
1748
|
attachGatewayProximity(v1Msg, msg);
|
|
1532
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg);
|
|
1749
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
1533
1750
|
decrypted.push(v1Msg);
|
|
1534
1751
|
continue;
|
|
1535
1752
|
}
|
|
@@ -1540,56 +1757,83 @@ export class V2E2EECoordinator {
|
|
|
1540
1757
|
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1541
1758
|
continue;
|
|
1542
1759
|
}
|
|
1543
|
-
|
|
1760
|
+
const deferStatus = {};
|
|
1761
|
+
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true);
|
|
1762
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1763
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1764
|
+
deferredKeyFetches.set(key, {
|
|
1765
|
+
fromAid: deferStatus.fromAid,
|
|
1766
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1767
|
+
groupId: deferStatus.groupId ?? '',
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1544
1770
|
if (plaintext === null)
|
|
1545
1771
|
continue;
|
|
1546
1772
|
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
1547
1773
|
plaintext.group_id = gid;
|
|
1548
1774
|
plaintext.group_aid = eventGroupAid;
|
|
1549
|
-
await client._publishPulledMessage('group.message_created', ns, seq, plaintext);
|
|
1775
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
1550
1776
|
decrypted.push(plaintext);
|
|
1551
1777
|
}
|
|
1552
|
-
const
|
|
1553
|
-
const
|
|
1778
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
1779
|
+
const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
|
|
1780
|
+
const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
|
|
1554
1781
|
const retentionFloor = Math.max(Number(result.retention_floor_message_seq ?? result.retention_floor_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
|
|
1555
1782
|
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
1556
1783
|
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
1557
|
-
const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1784
|
+
const commitTarget = Math.max(pageContigBefore, Number.isFinite(effectiveFloor) ? effectiveFloor : 0, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, seqs.length > 0 ? pageMaxSeq : pageContigBefore);
|
|
1558
1785
|
let ackSeq = pageContigBefore;
|
|
1559
1786
|
let contigAdvanced = false;
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1787
|
+
if (ownsCursor) {
|
|
1788
|
+
try {
|
|
1789
|
+
if (commitTarget > pageContigBefore) {
|
|
1790
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
1791
|
+
if (effectiveFloor > pageContigBefore) {
|
|
1792
|
+
client._clientLog.info(`group.v2.pull floor advance: ns=${ns} contiguous=${pageContigBefore} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} visibility_floor=${visibilityFloor}`);
|
|
1793
|
+
}
|
|
1794
|
+
if (deferredServerCursor > pageContigBefore && pageCount === 1) {
|
|
1795
|
+
client._clientLog.info(`group.v2.pull restore local A from deferred server ACK cursor: group=${gid}, ns=${ns}, contiguous=${pageContigBefore} -> current_seq=${deferredServerCursor}`);
|
|
1796
|
+
}
|
|
1797
|
+
if (seqs.length > 0) {
|
|
1798
|
+
client._clientLog.debug(`group.v2.pull force contiguous: group=${gid}, ns=${ns}, page_max_seq=${pageMaxSeq}, previous=${pageContigBefore}`);
|
|
1799
|
+
}
|
|
1568
1800
|
}
|
|
1569
|
-
}
|
|
1570
|
-
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1571
|
-
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1572
|
-
if (contigAdvanced) {
|
|
1573
|
-
await client._drainOrderedMessages(ns);
|
|
1574
1801
|
ackSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1575
1802
|
contigAdvanced = ackSeq !== pageContigBefore;
|
|
1576
|
-
|
|
1803
|
+
if (contigAdvanced) {
|
|
1804
|
+
await client._commitSeqTrackerState(ns);
|
|
1805
|
+
if (pageCount === 1 && deferredServerCursor > 0)
|
|
1806
|
+
this.clearForwardCursor(ns, ackSeq);
|
|
1807
|
+
}
|
|
1577
1808
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1809
|
+
catch (exc) {
|
|
1810
|
+
if (typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1811
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
1812
|
+
}
|
|
1813
|
+
client._clientLog.error?.(`group.v2.pull Forward commit rolled back: group=${gid} ns=${ns} target=${commitTarget} error=${formatE2EEError(exc)}`);
|
|
1814
|
+
throw exc;
|
|
1582
1815
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1816
|
+
if (contigAdvanced)
|
|
1817
|
+
await client._drainOrderedMessages(ns, undefined, false, false);
|
|
1585
1818
|
}
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
|
|
1819
|
+
for (const fetch of deferredKeyFetches.values()) {
|
|
1820
|
+
this.scheduleSenderIKFetch(fetch.fromAid, fetch.senderDeviceId, fetch.groupId);
|
|
1821
|
+
}
|
|
1822
|
+
if (ownsCursor && parsedCursorCurrentSeq !== null && cursorCurrentSeq > ackSeq) {
|
|
1823
|
+
this.recordForwardCursor(ns, cursorCurrentSeq, ackSeq);
|
|
1824
|
+
}
|
|
1825
|
+
// 无本页 cursor.current_seq 时,只有本页成功提交了比已知 cursor
|
|
1826
|
+
// 更高的 A 才新增 ACK 义务;空页不得因已有本地 A 重复确认。
|
|
1827
|
+
const knownServerCursorSeq = hasServerCursor
|
|
1828
|
+
? cursorCurrentSeq
|
|
1829
|
+
: (ownsCursor && pageCount === 1 ? deferredServerCursor : 0);
|
|
1830
|
+
const ackNeeded = ackSeq > 0
|
|
1589
1831
|
&& ackSeq > lastAutoAckSeq
|
|
1590
1832
|
&& ownsCursor
|
|
1591
|
-
&& (
|
|
1833
|
+
&& ((hasServerCursor && ackSeq > cursorCurrentSeq)
|
|
1834
|
+
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
1592
1835
|
if (ackNeeded) {
|
|
1836
|
+
this.recordForwardAck(ns, ackSeq);
|
|
1593
1837
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1594
1838
|
const canContinuePage = messages.length > 0 && nextAfter > nextAfterSeq && ownsCursor;
|
|
1595
1839
|
if (canContinuePage) {
|
|
@@ -1597,7 +1841,7 @@ export class V2E2EECoordinator {
|
|
|
1597
1841
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1598
1842
|
}
|
|
1599
1843
|
else {
|
|
1600
|
-
await this.
|
|
1844
|
+
await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
|
|
1601
1845
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1602
1846
|
}
|
|
1603
1847
|
}
|
|
@@ -1605,11 +1849,23 @@ export class V2E2EECoordinator {
|
|
|
1605
1849
|
if (!ownsCursor)
|
|
1606
1850
|
break;
|
|
1607
1851
|
const rawCount = messages.length;
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1852
|
+
const shouldContinue = shouldContinueForwardPage({
|
|
1853
|
+
rawCount,
|
|
1854
|
+
nextAfterSeq,
|
|
1855
|
+
nextAfter,
|
|
1856
|
+
limit,
|
|
1857
|
+
hasMoreField,
|
|
1858
|
+
hasMore,
|
|
1859
|
+
serverHead,
|
|
1860
|
+
pendingAckSeq,
|
|
1861
|
+
});
|
|
1611
1862
|
if (!shouldContinue)
|
|
1612
1863
|
break;
|
|
1864
|
+
if (pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
1865
|
+
const nextKey = `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`;
|
|
1866
|
+
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1867
|
+
pullGateKey = nextKey;
|
|
1868
|
+
}
|
|
1613
1869
|
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
1614
1870
|
if (tailDelayMs > 0)
|
|
1615
1871
|
await v2Sleep(tailDelayMs);
|
|
@@ -1618,13 +1874,34 @@ export class V2E2EECoordinator {
|
|
|
1618
1874
|
if (pageCount >= maxPages) {
|
|
1619
1875
|
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
1620
1876
|
if (pendingAckSeq > 0) {
|
|
1621
|
-
await this.
|
|
1877
|
+
await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
|
|
1622
1878
|
pendingAckSeq = 0;
|
|
1623
1879
|
}
|
|
1624
1880
|
}
|
|
1625
1881
|
return decrypted;
|
|
1626
1882
|
}
|
|
1883
|
+
async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
|
|
1884
|
+
const ns = `group:${String(groupId ?? '').trim()}`;
|
|
1885
|
+
try {
|
|
1886
|
+
const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
|
|
1887
|
+
const actualAckSeq = this.client._delivery?.resolveForwardAckSeq?.(result, upToSeq) ?? 0;
|
|
1888
|
+
if (actualAckSeq < upToSeq) {
|
|
1889
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
1890
|
+
}
|
|
1891
|
+
this.clearForwardAck(ns, upToSeq);
|
|
1892
|
+
}
|
|
1893
|
+
catch (exc) {
|
|
1894
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1895
|
+
throw exc;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1627
1898
|
async ackGroupV2(groupId, upToSeq, groupAid, inlineGeneration) {
|
|
1899
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, false);
|
|
1900
|
+
}
|
|
1901
|
+
async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
|
|
1902
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, undefined, true);
|
|
1903
|
+
}
|
|
1904
|
+
async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, inlineGeneration, requireServerWatermark) {
|
|
1628
1905
|
const client = this.client;
|
|
1629
1906
|
const gid = typeof groupId === 'string' ? groupId.trim() : '';
|
|
1630
1907
|
if (!gid)
|
|
@@ -1644,13 +1921,17 @@ export class V2E2EECoordinator {
|
|
|
1644
1921
|
seq = client._clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
|
|
1645
1922
|
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
1646
1923
|
return { acked: 0 };
|
|
1924
|
+
// 与 P2P 一致,必须将 ACK 响应限定到请求开始时对应的 inline generation。
|
|
1925
|
+
const ackGeneration = inlineGeneration ?? client._delivery?.pendingGroupInlineAckGeneration?.(ns) ?? null;
|
|
1647
1926
|
const raw = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq }, explicitGroupAid));
|
|
1648
|
-
const actualAckSeq =
|
|
1649
|
-
|
|
1650
|
-
client._delivery?.
|
|
1927
|
+
const actualAckSeq = requireServerWatermark
|
|
1928
|
+
? (client._delivery?.resolveForwardAckSeq?.(raw, seq) ?? 0)
|
|
1929
|
+
: (client._delivery?.resolveActualAckSeq?.(raw, seq) ?? seq);
|
|
1930
|
+
if (requireServerWatermark && actualAckSeq < seq) {
|
|
1931
|
+
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${seq}`);
|
|
1651
1932
|
}
|
|
1652
|
-
|
|
1653
|
-
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq,
|
|
1933
|
+
if (ackGeneration !== null) {
|
|
1934
|
+
client._delivery?.completePendingGroupInlineAckUpTo?.(ns, actualAckSeq, ackGeneration);
|
|
1654
1935
|
}
|
|
1655
1936
|
return raw;
|
|
1656
1937
|
}
|
|
@@ -1830,7 +2111,7 @@ export class V2E2EECoordinator {
|
|
|
1830
2111
|
let e2ee;
|
|
1831
2112
|
if (payload?.type === 'e2ee.group_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1832
2113
|
e2ee = v2E2eeMeta(payload);
|
|
1833
|
-
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
2114
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
|
|
1834
2115
|
if (plain === null) {
|
|
1835
2116
|
decryptFailed = true;
|
|
1836
2117
|
client._clientLog.debug(`group.thought.get decrypt returned null: group=${groupId}, thought_id=${thoughtId}, from=${fromAid}`);
|
|
@@ -1900,7 +2181,7 @@ export class V2E2EECoordinator {
|
|
|
1900
2181
|
let e2ee;
|
|
1901
2182
|
if (payload?.type === 'e2ee.p2p_encrypted' && String(payload.version ?? '') === 'v2') {
|
|
1902
2183
|
e2ee = v2E2eeMeta(payload);
|
|
1903
|
-
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid });
|
|
2184
|
+
const plain = await client._decryptV2EnvelopeForThought({ envelope: payload, fromAid, fetchSenderIK: true });
|
|
1904
2185
|
if (plain === null) {
|
|
1905
2186
|
decryptFailed = true;
|
|
1906
2187
|
client._clientLog.debug(`message.thought.get decrypt returned null: thought_id=${thoughtId}, from=${fromAid}, to=${toAid}`);
|
|
@@ -1987,7 +2268,10 @@ export class V2E2EECoordinator {
|
|
|
1987
2268
|
const fromAid = String(opts.fromAid || aad.from || '').trim();
|
|
1988
2269
|
const senderDeviceId = String(aad.from_device ?? '');
|
|
1989
2270
|
const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
1990
|
-
|
|
2271
|
+
let senderPubDer = await this.getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
|
|
2272
|
+
if (!senderPubDer && opts.fetchSenderIK) {
|
|
2273
|
+
senderPubDer = await this.fetchSenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
|
|
2274
|
+
}
|
|
1991
2275
|
if (!senderPubDer) {
|
|
1992
2276
|
client._clientLog.warn(`V2 thought decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
1993
2277
|
this.scheduleSenderIKFetch(fromAid, senderDeviceId, groupIdForKeys);
|