@agentunion/fastaun 0.5.19 → 0.5.21
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 +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +13 -1
- package/dist/agent-md-schema.js +63 -33
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +30 -3
- package/dist/agent-md.js +293 -152
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +42 -0
- package/dist/aid-store.js +224 -44
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +18 -13
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +78 -2
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +44 -1
- package/dist/cert-utils.js +157 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +68 -1
- package/dist/client/delivery.js +1196 -135
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +5 -1
- package/dist/client/rpc-pipeline.js +240 -111
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +12 -2
- package/dist/client/v2-e2ee.js +842 -379
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +26 -2
- package/dist/client.js +420 -75
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +23 -4
- package/dist/events.js +171 -15
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +43 -1
- package/dist/facades.js +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +7 -2
- package/dist/keystore/aid-db.js +194 -43
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +15 -1
- package/dist/keystore/local-token-store.d.ts +10 -2
- package/dist/keystore/local-token-store.js +81 -16
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.js +829 -22
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +40 -9
- package/dist/ownership-v21.js +174 -79
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +22 -0
- package/dist/ownership-workflow.js +861 -66
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/secret-store/file-store.js +84 -0
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +2 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +67 -3
- package/dist/tools/cross-sdk-agent.js +575 -42
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.js +108 -44
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/v2/e2ee/encrypt-group.js +1 -0
- package/dist/v2/e2ee/encrypt-group.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -0
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/e2ee/types.d.ts +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/client/v2-e2ee.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
|
+
import { _INTERNAL_MESSAGE_FLOW_LIMITS } from '../config.js';
|
|
2
3
|
import { E2EEError, StateError, ValidationError } from '../errors.js';
|
|
3
4
|
import { decryptMessage, encryptGroupMessage, encryptP2PMessage, } from '../v2/e2ee/index.js';
|
|
4
5
|
import { V2Session } from '../v2/session/index.js';
|
|
@@ -113,13 +114,13 @@ function deduplicateForwardPageMessages(value, afterSeq) {
|
|
|
113
114
|
function contiguousPullSeq(afterSeq, seqs, proof) {
|
|
114
115
|
const pulledMax = seqs.length > 0 ? Math.max(...seqs) : 0;
|
|
115
116
|
const serverProof = safeNonNegativeSeq(proof);
|
|
116
|
-
return Math.max(afterSeq, pulledMax, serverProof ?? 0);
|
|
117
|
+
return Math.max(afterSeq, pulledMax, pulledMax > 0 ? 0 : serverProof ?? 0);
|
|
117
118
|
}
|
|
118
119
|
function shouldContinueForwardPage({ rawCount, nextAfterSeq, nextAfter, limit, hasMoreField, hasMore, serverHead, observedUpper, pendingAckSeq, }) {
|
|
119
120
|
// 无新 seq 或游标未前进时必须停止,避免错误元数据造成循环。
|
|
120
121
|
if (rawCount <= 0 || nextAfter <= nextAfterSeq)
|
|
121
122
|
return false;
|
|
122
|
-
if (hasMore || serverHead > nextAfter || observedUpper > nextAfter
|
|
123
|
+
if (hasMore || serverHead > nextAfter || observedUpper > nextAfter)
|
|
123
124
|
return true;
|
|
124
125
|
// 仅旧服务缺失 has_more 时,以满页保留兼容续拉。
|
|
125
126
|
return !hasMoreField && limit > 0 && rawCount >= limit;
|
|
@@ -349,6 +350,13 @@ export class V2E2EECoordinator {
|
|
|
349
350
|
get client() {
|
|
350
351
|
return this.runtime.client;
|
|
351
352
|
}
|
|
353
|
+
async withApplicationPage(ns, source, operation, reservation, handlers) {
|
|
354
|
+
const delivery = this.client._delivery;
|
|
355
|
+
return typeof delivery.runApplicationPage === 'function'
|
|
356
|
+
? await (handlers ? delivery.runApplicationPage(ns, source, operation, reservation, handlers)
|
|
357
|
+
: reservation ? delivery.runApplicationPage(ns, source, operation, reservation) : delivery.runApplicationPage(ns, source, operation))
|
|
358
|
+
: await operation();
|
|
359
|
+
}
|
|
352
360
|
resetForwardCursorState() {
|
|
353
361
|
this.pendingForwardCursors.clear();
|
|
354
362
|
this.pendingForwardAcks.clear();
|
|
@@ -621,11 +629,13 @@ export class V2E2EECoordinator {
|
|
|
621
629
|
return null;
|
|
622
630
|
}
|
|
623
631
|
}
|
|
624
|
-
pendingSenderIKMessageKey(msg, groupId) {
|
|
632
|
+
pendingSenderIKMessageKey(msg, groupId, fromAid = '') {
|
|
625
633
|
const client = this.client;
|
|
626
634
|
const messageId = String(msg.message_id ?? '').trim();
|
|
627
635
|
const seq = String(msg.seq ?? '').trim();
|
|
628
|
-
const prefix = groupId ? `group:${groupId}` :
|
|
636
|
+
const prefix = groupId ? `group:${groupId}` : client._p2pPeerPullEnabled
|
|
637
|
+
? client._delivery.peerNamespace(fromAid || String(msg.sender_aid ?? msg.from_aid ?? msg.from ?? ''))
|
|
638
|
+
: `p2p:${client._aid ?? ''}`;
|
|
629
639
|
return `${prefix}:${messageId || seq || Math.random().toString(36).slice(2)}`;
|
|
630
640
|
}
|
|
631
641
|
pendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
|
|
@@ -717,7 +727,7 @@ export class V2E2EECoordinator {
|
|
|
717
727
|
return;
|
|
718
728
|
const senderDeviceId = String(args.senderDeviceId ?? '');
|
|
719
729
|
const groupId = String(args.groupId ?? '').trim();
|
|
720
|
-
const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId);
|
|
730
|
+
const messageKey = this.pendingSenderIKMessageKey(args.msg, groupId, fromAid);
|
|
721
731
|
client._v2SenderIKPending.set(messageKey, {
|
|
722
732
|
msg: { ...args.msg },
|
|
723
733
|
fromAid,
|
|
@@ -833,10 +843,19 @@ export class V2E2EECoordinator {
|
|
|
833
843
|
}
|
|
834
844
|
plaintext = normalizeGroupMentionMode(plaintext, entry.msg, entry.msg.envelope_json);
|
|
835
845
|
plaintext.group_id = entry.groupId;
|
|
846
|
+
plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
|
|
836
847
|
await client._publishPulledMessage('group.message_created', `group:${entry.groupId}`, seq, plaintext, false, 'pending_retry');
|
|
837
848
|
}
|
|
838
849
|
else {
|
|
839
|
-
|
|
850
|
+
const ns = client._p2pPeerPullEnabled
|
|
851
|
+
? await client._delivery.initializePeer(entry.fromAid) : `p2p:${client._aid ?? ''}`;
|
|
852
|
+
if (!generationIsCurrent())
|
|
853
|
+
return;
|
|
854
|
+
plaintext.sender_aid = entry.fromAid;
|
|
855
|
+
plaintext.need_render = typeof entry.msg.need_render === 'boolean' ? entry.msg.need_render : plaintext.need_render !== false;
|
|
856
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false, 'pending_retry');
|
|
857
|
+
if (client._p2pPeerPullEnabled && generationIsCurrent())
|
|
858
|
+
client._delivery.schedulePeerRecovery(entry.fromAid, seq);
|
|
840
859
|
}
|
|
841
860
|
if (generationIsCurrent())
|
|
842
861
|
pending.delete(key);
|
|
@@ -852,6 +871,8 @@ export class V2E2EECoordinator {
|
|
|
852
871
|
}
|
|
853
872
|
async publishPendingV2VolatileMessage(rawMessage, plaintext, source) {
|
|
854
873
|
const delivery = this.client._delivery;
|
|
874
|
+
const generation = Number(this.client._v2RuntimeGeneration ?? 0);
|
|
875
|
+
const context = JSON.stringify([this.client._aid, this.client._deviceId, this.client._slotId]);
|
|
855
876
|
const claim = delivery.beginVolatileDelivery(rawMessage);
|
|
856
877
|
if (claim === 'inflight')
|
|
857
878
|
return;
|
|
@@ -865,8 +886,14 @@ export class V2E2EECoordinator {
|
|
|
865
886
|
if (Object.prototype.hasOwnProperty.call(rawMessage, key))
|
|
866
887
|
publicMessage[key] = rawMessage[key];
|
|
867
888
|
}
|
|
889
|
+
publicMessage.need_render = typeof rawMessage.need_render === 'boolean' ? rawMessage.need_render : plaintext.need_render !== false;
|
|
868
890
|
const appEvent = delivery.p2pAppEventForMessage(publicMessage);
|
|
869
891
|
await this.client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
892
|
+
if (this.client._p2pPeerPullEnabled) {
|
|
893
|
+
if (context !== JSON.stringify([this.client._aid, this.client._deviceId, this.client._slotId]))
|
|
894
|
+
throw new StateError('delivery invalidated');
|
|
895
|
+
this.ensureDeliveryCurrent(delivery.peerNamespace(String(rawMessage.sender_aid ?? rawMessage.from_aid ?? rawMessage.from ?? '')), generation);
|
|
896
|
+
}
|
|
870
897
|
delivery.markVolatileSeen(rawMessage);
|
|
871
898
|
}
|
|
872
899
|
finally {
|
|
@@ -917,10 +944,14 @@ export class V2E2EECoordinator {
|
|
|
917
944
|
});
|
|
918
945
|
}
|
|
919
946
|
}
|
|
947
|
+
const targetDeviceId = String(opts.deviceId ?? '').trim();
|
|
948
|
+
if (useCache && targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
|
|
949
|
+
this.deleteBootstrapCacheEntry(to);
|
|
950
|
+
return this.buildV2P2PEnvelope({ ...opts, useCache: false });
|
|
951
|
+
}
|
|
920
952
|
if (peerDevices.length === 0) {
|
|
921
953
|
throw new E2EEError(`V2 bootstrap: no devices found for ${to}`);
|
|
922
954
|
}
|
|
923
|
-
const targetDeviceId = String(opts.deviceId ?? '').trim();
|
|
924
955
|
if (targetDeviceId && !peerDevices.some((dev) => String(dev.device_id ?? '').trim() === targetDeviceId)) {
|
|
925
956
|
throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
|
|
926
957
|
}
|
|
@@ -1007,6 +1038,10 @@ export class V2E2EECoordinator {
|
|
|
1007
1038
|
targetSet = this.cloneP2PTargetSet(targetSet);
|
|
1008
1039
|
}
|
|
1009
1040
|
if (targetSet.targets.length === 0) {
|
|
1041
|
+
if (useCache && targetDeviceId) {
|
|
1042
|
+
this.deleteBootstrapCacheEntry(to);
|
|
1043
|
+
return this.buildV2P2PEnvelope({ ...opts, useCache: false });
|
|
1044
|
+
}
|
|
1010
1045
|
throw new E2EEError(`V2 bootstrap: no usable devices found for ${to}`);
|
|
1011
1046
|
}
|
|
1012
1047
|
if (targetDeviceId) {
|
|
@@ -1020,6 +1055,10 @@ export class V2E2EECoordinator {
|
|
|
1020
1055
|
return isPeerTarget || target.role === 'self_sync';
|
|
1021
1056
|
});
|
|
1022
1057
|
if (!peerTargetFound) {
|
|
1058
|
+
if (useCache) {
|
|
1059
|
+
this.deleteBootstrapCacheEntry(to);
|
|
1060
|
+
return this.buildV2P2PEnvelope({ ...opts, useCache: false });
|
|
1061
|
+
}
|
|
1023
1062
|
throw new E2EEError(`V2 bootstrap: target device ${targetDeviceId} not found for ${to}`);
|
|
1024
1063
|
}
|
|
1025
1064
|
}
|
|
@@ -1028,6 +1067,7 @@ export class V2E2EECoordinator {
|
|
|
1028
1067
|
timestamp: opts.timestamp,
|
|
1029
1068
|
protectedHeaders: opts.protectedHeaders,
|
|
1030
1069
|
context: opts.context,
|
|
1070
|
+
needRender: opts.needRender,
|
|
1031
1071
|
});
|
|
1032
1072
|
client._logMessageDebug('send-envelope', 'message.send.v2', 'message.send', {
|
|
1033
1073
|
message_id: envelope.message_id,
|
|
@@ -1072,6 +1112,7 @@ export class V2E2EECoordinator {
|
|
|
1072
1112
|
deviceId: targetDeviceId || undefined,
|
|
1073
1113
|
protectedHeaders: opts?.protectedHeaders,
|
|
1074
1114
|
context: opts?.context,
|
|
1115
|
+
needRender: opts?.needRender,
|
|
1075
1116
|
useCache,
|
|
1076
1117
|
});
|
|
1077
1118
|
const result = await client.call('message.send', {
|
|
@@ -1081,6 +1122,7 @@ export class V2E2EECoordinator {
|
|
|
1081
1122
|
...(targetDeviceId ? { device_id: targetDeviceId } : {}),
|
|
1082
1123
|
...(opts?.ttl !== undefined ? { ttl: opts.ttl } : {}),
|
|
1083
1124
|
encrypt: false,
|
|
1125
|
+
need_render: opts?.needRender ?? true,
|
|
1084
1126
|
_skip_send_result_envelope: true,
|
|
1085
1127
|
});
|
|
1086
1128
|
client._clientLog.debug(`message.v2.send ok: to=${toAid}, use_cache=${useCache}, seq=${String((isJsonObject(result) ? result.seq : '') ?? '')}`);
|
|
@@ -1094,6 +1136,7 @@ export class V2E2EECoordinator {
|
|
|
1094
1136
|
message_id: messageId,
|
|
1095
1137
|
timestamp: opts?.timestamp,
|
|
1096
1138
|
ttl: opts?.ttl,
|
|
1139
|
+
need_render: opts?.needRender ?? true,
|
|
1097
1140
|
protected_headers: opts?.protectedHeaders,
|
|
1098
1141
|
context: opts?.context,
|
|
1099
1142
|
}, result, true);
|
|
@@ -1110,6 +1153,7 @@ export class V2E2EECoordinator {
|
|
|
1110
1153
|
message_id: messageId,
|
|
1111
1154
|
timestamp: opts?.timestamp,
|
|
1112
1155
|
ttl: opts?.ttl,
|
|
1156
|
+
need_render: opts?.needRender ?? true,
|
|
1113
1157
|
protected_headers: opts?.protectedHeaders,
|
|
1114
1158
|
context: opts?.context,
|
|
1115
1159
|
}, result, true);
|
|
@@ -1125,12 +1169,13 @@ export class V2E2EECoordinator {
|
|
|
1125
1169
|
history_error: { code: 'decrypt_failed', message: reason },
|
|
1126
1170
|
};
|
|
1127
1171
|
}
|
|
1128
|
-
ensureDeliveryCurrent(ns, generation) {
|
|
1172
|
+
ensureDeliveryCurrent(ns, generation, inlineGeneration) {
|
|
1129
1173
|
if (Number(this.client._v2RuntimeGeneration ?? 0) !== generation
|
|
1174
|
+
|| (inlineGeneration !== undefined && this.client._delivery.captureInlineAckGeneration() !== inlineGeneration)
|
|
1130
1175
|
|| this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) === false)
|
|
1131
1176
|
throw new StateError('pull invalidated');
|
|
1132
1177
|
}
|
|
1133
|
-
async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false) {
|
|
1178
|
+
async decodeP2PWindowMessage(msg, publish, history, source, orderedPublish = false, deferredEffects) {
|
|
1134
1179
|
const client = this.client;
|
|
1135
1180
|
const seq = strictWindowSeq(msg.seq, 'messages[].seq', true);
|
|
1136
1181
|
let plaintext = null;
|
|
@@ -1161,11 +1206,11 @@ export class V2E2EECoordinator {
|
|
|
1161
1206
|
else if (isJsonObject(payload)
|
|
1162
1207
|
&& ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
|
|
1163
1208
|
msg = { ...msg, envelope_json: JSON.stringify(payload) };
|
|
1164
|
-
plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
|
|
1209
|
+
plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source, ...(deferredEffects ? [deferredEffects] : []));
|
|
1165
1210
|
}
|
|
1166
1211
|
}
|
|
1167
1212
|
else {
|
|
1168
|
-
plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source);
|
|
1213
|
+
plaintext = await client._decryptV2Message(msg, false, !history, true, true, undefined, false, source, ...(deferredEffects ? [deferredEffects] : []));
|
|
1169
1214
|
}
|
|
1170
1215
|
if (!plaintext) {
|
|
1171
1216
|
if (history)
|
|
@@ -1248,6 +1293,7 @@ export class V2E2EECoordinator {
|
|
|
1248
1293
|
client._clientLog.warn(`Group Tail 单条消息解密失败,连续性证明仍保留: group=${groupId}, seq=${seq}`);
|
|
1249
1294
|
return null;
|
|
1250
1295
|
}
|
|
1296
|
+
plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
|
|
1251
1297
|
if (publish && !history && client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
1252
1298
|
return plaintext;
|
|
1253
1299
|
}
|
|
@@ -1256,6 +1302,363 @@ export class V2E2EECoordinator {
|
|
|
1256
1302
|
}
|
|
1257
1303
|
return plaintext;
|
|
1258
1304
|
}
|
|
1305
|
+
async pullPeerV2(senderAid, opts = {}) {
|
|
1306
|
+
const client = this.client;
|
|
1307
|
+
const generation = Number(client._v2RuntimeGeneration ?? 0);
|
|
1308
|
+
const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
|
|
1309
|
+
await client._ensureV2SessionReady('message.pull');
|
|
1310
|
+
this.ensureDeliveryCurrent(client._delivery.peerNamespace(senderAid), generation, inlineGeneration);
|
|
1311
|
+
const ns = await client._delivery.initializePeer(senderAid);
|
|
1312
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1313
|
+
const limit = Number(opts.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
|
|
1314
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
|
|
1315
|
+
throw new ValidationError('peer limit must be between 1 and 200');
|
|
1316
|
+
const mode = opts.window_mode === 'tail' ? 'tail' : 'forward';
|
|
1317
|
+
const source = mode === 'tail' ? 'tail' : 'pull';
|
|
1318
|
+
if (mode === 'tail' && (opts.force === true || 'ack_up_to_seq' in opts))
|
|
1319
|
+
throw new ValidationError('Tail does not accept force or ACK');
|
|
1320
|
+
const budget = opts.budget ?? { remaining: Math.max(1, Number(opts.max_pages ?? 100)) };
|
|
1321
|
+
let after = opts.after_seq === undefined ? client._seqTracker.getContiguousSeq(ns) : strictWindowSeq(opts.after_seq, 'after_seq');
|
|
1322
|
+
if (opts.force !== true && after > client._seqTracker.getContiguousSeq(ns))
|
|
1323
|
+
throw new ValidationError('after_seq cannot exceed the peer delivery cursor');
|
|
1324
|
+
let volatileCursor = String(opts.after_volatile_cursor ?? client._delivery.peerVolatilePullCursor(senderAid));
|
|
1325
|
+
const decoded = [];
|
|
1326
|
+
let complete = false;
|
|
1327
|
+
let head = after;
|
|
1328
|
+
let pages = 0;
|
|
1329
|
+
let durableAck = 0;
|
|
1330
|
+
let initializedAck = false;
|
|
1331
|
+
let confirmedAck = 0;
|
|
1332
|
+
const confirmAck = (seq) => {
|
|
1333
|
+
confirmedAck = Math.max(confirmedAck, Math.min(seq, durableAck));
|
|
1334
|
+
this.clearForwardAck(ns, confirmedAck);
|
|
1335
|
+
};
|
|
1336
|
+
const prefetched = [];
|
|
1337
|
+
const stopping = new AbortController();
|
|
1338
|
+
const validatePage = (response, requestAfter, committedAck, preparing = false) => {
|
|
1339
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1340
|
+
this.validatePeerResponse(response, senderAid);
|
|
1341
|
+
const rows = Array.isArray(response.messages) ? response.messages : null;
|
|
1342
|
+
if (!rows || rows.length > limit)
|
|
1343
|
+
throw new ValidationError('invalid peer messages page');
|
|
1344
|
+
const persistent = rows.filter((row) => row.storage !== 'volatile');
|
|
1345
|
+
const volatile = rows.filter((row) => row.storage === 'volatile');
|
|
1346
|
+
const end = strictWindowSeq(response.covered_through_seq, 'covered_through_seq');
|
|
1347
|
+
const pageHead = strictWindowSeq(response.head_seq, 'head_seq');
|
|
1348
|
+
let start;
|
|
1349
|
+
if (mode === 'tail') {
|
|
1350
|
+
if (response.window_mode !== 'tail')
|
|
1351
|
+
throw new ValidationError('peer Tail response mode mismatch');
|
|
1352
|
+
start = strictWindowSeq(response.window_start_seq, 'window_start_seq');
|
|
1353
|
+
if (start > end || end !== pageHead || (end > requestAfter && start < requestAfter + 1))
|
|
1354
|
+
throw new ValidationError('invalid peer Tail proof');
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
start = strictWindowSeq(response.scan_after_seq, 'scan_after_seq');
|
|
1358
|
+
const retention = safeNonNegativeSeq(response.retention_floor_seq) ?? 0;
|
|
1359
|
+
if (start < requestAfter || (!preparing && start > Math.max(requestAfter, retention, opts.force === true ? 0 : committedAck))
|
|
1360
|
+
|| end < start || end > pageHead || response.next_after_seq !== end)
|
|
1361
|
+
throw new ValidationError('invalid peer Forward proof');
|
|
1362
|
+
if (typeof response.persistent_has_more !== 'boolean' || typeof response.volatile_has_more !== 'boolean'
|
|
1363
|
+
|| response.has_more !== (response.persistent_has_more || response.volatile_has_more)
|
|
1364
|
+
|| (!response.persistent_has_more && end < pageHead))
|
|
1365
|
+
throw new ValidationError('invalid peer continuation proof');
|
|
1366
|
+
}
|
|
1367
|
+
let previous = mode === 'tail' ? start - 1 : start;
|
|
1368
|
+
for (const row of persistent) {
|
|
1369
|
+
const seq = strictWindowSeq(row.seq, 'messages[].seq', true);
|
|
1370
|
+
if (seq <= previous || seq > end || String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
|
|
1371
|
+
throw new ValidationError('peer message is outside proof or sender');
|
|
1372
|
+
previous = seq;
|
|
1373
|
+
}
|
|
1374
|
+
if (mode === 'forward' && opts.force !== true)
|
|
1375
|
+
confirmAck(safeNonNegativeSeq(response.server_ack_seq) ?? 0);
|
|
1376
|
+
return { persistent, volatile, end, pageHead, start };
|
|
1377
|
+
};
|
|
1378
|
+
const decodeRows = async (rows) => {
|
|
1379
|
+
const prepared = { messages: [], effects: [] };
|
|
1380
|
+
try {
|
|
1381
|
+
for (const row of rows) {
|
|
1382
|
+
if (stopping.signal.aborted)
|
|
1383
|
+
throw new StateError('pull invalidated');
|
|
1384
|
+
const plaintext = await this.decodeP2PWindowMessage(row, false, false, source, false, prepared.effects);
|
|
1385
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1386
|
+
if (!plaintext)
|
|
1387
|
+
throw new E2EEError('peer message could not be decrypted');
|
|
1388
|
+
plaintext.sender_aid = senderAid;
|
|
1389
|
+
plaintext.need_render = typeof row.need_render === 'boolean' ? row.need_render : plaintext.need_render !== false;
|
|
1390
|
+
prepared.messages.push(plaintext);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
catch (error) {
|
|
1394
|
+
prepared.error = error;
|
|
1395
|
+
}
|
|
1396
|
+
return prepared;
|
|
1397
|
+
};
|
|
1398
|
+
const fetchPage = async (requestAfter, requestCursor) => {
|
|
1399
|
+
const key = `${ns}|mode=${mode}|after=${requestAfter}|volatile=${requestCursor}|force=${Boolean(opts.force)}|limit=${limit}`;
|
|
1400
|
+
const request = (cursor) => client._runPullSerialized(key, () => client._callRawV2Rpc('message.v2.pull', {
|
|
1401
|
+
cursor_scope: 'peer', sender_aid: senderAid, device_id: client._deviceId, slot_id: client._slotId,
|
|
1402
|
+
after_seq: requestAfter, limit,
|
|
1403
|
+
...(mode === 'forward' && opts.force !== true && durableAck > 0 ? { ack_up_to_seq: durableAck } : {}),
|
|
1404
|
+
...(mode === 'tail' ? { window_mode: 'tail' } : {}),
|
|
1405
|
+
...(opts.force === true ? { force: true } : {}),
|
|
1406
|
+
...(cursor ? { after_volatile_cursor: cursor } : {}),
|
|
1407
|
+
...(opts.background ? { _rpc_background: true } : { _rpc_foreground: true }),
|
|
1408
|
+
}, key), Boolean(opts.background));
|
|
1409
|
+
const reservation = await client._dispatcher?._reserveMessageCapacity?.(limit, stopping.signal);
|
|
1410
|
+
try {
|
|
1411
|
+
if (stopping.signal.aborted)
|
|
1412
|
+
throw new StateError('pull invalidated');
|
|
1413
|
+
try {
|
|
1414
|
+
return { response: await request(requestCursor), reservation };
|
|
1415
|
+
}
|
|
1416
|
+
catch (error) {
|
|
1417
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1418
|
+
if (!requestCursor || !String(error instanceof Error ? error.message : error).toLowerCase().includes('volatile cursor epoch mismatch'))
|
|
1419
|
+
throw error;
|
|
1420
|
+
client._delivery.setPeerVolatilePullCursor(senderAid, '');
|
|
1421
|
+
return { response: await request(''), reservation };
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
catch (error) {
|
|
1425
|
+
reservation?.release();
|
|
1426
|
+
throw error;
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
const prefetchPage = (requestAfter, cursor) => {
|
|
1430
|
+
const existing = prefetched.find((page) => page.after === requestAfter && page.cursor === cursor);
|
|
1431
|
+
if (existing)
|
|
1432
|
+
return existing;
|
|
1433
|
+
if (prefetched.length >= 2 || budget.remaining <= 0 || stopping.signal.aborted)
|
|
1434
|
+
return undefined;
|
|
1435
|
+
budget.remaining -= 1;
|
|
1436
|
+
const page = { after: requestAfter, cursor, response: fetchPage(requestAfter, cursor) };
|
|
1437
|
+
void page.response.catch(() => undefined);
|
|
1438
|
+
prefetched.push(page);
|
|
1439
|
+
return page;
|
|
1440
|
+
};
|
|
1441
|
+
const preparePage = (page) => {
|
|
1442
|
+
if (page.prepared)
|
|
1443
|
+
return;
|
|
1444
|
+
page.prepared = page.response.then(async (fetched) => {
|
|
1445
|
+
try {
|
|
1446
|
+
const proof = validatePage(fetched.response, page.after, durableAck, true);
|
|
1447
|
+
const handlers = client._delivery.snapshotApplicationHandlers?.();
|
|
1448
|
+
if (proof.volatile.length === 0 && proof.end > page.after && fetched.response.persistent_has_more === true) {
|
|
1449
|
+
prefetchPage(proof.end, page.cursor);
|
|
1450
|
+
}
|
|
1451
|
+
return { ...fetched, handlers, prepared: await decodeRows(proof.persistent) };
|
|
1452
|
+
}
|
|
1453
|
+
catch (error) {
|
|
1454
|
+
fetched.reservation?.release();
|
|
1455
|
+
throw error;
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
void page.prepared.catch(() => undefined);
|
|
1459
|
+
};
|
|
1460
|
+
client._delivery.syncStarted(ns);
|
|
1461
|
+
let failed = false;
|
|
1462
|
+
try {
|
|
1463
|
+
while (budget.remaining > 0 || prefetched.length > 0) {
|
|
1464
|
+
const cached = prefetched.shift();
|
|
1465
|
+
if (!cached)
|
|
1466
|
+
budget.remaining -= 1;
|
|
1467
|
+
pages += 1;
|
|
1468
|
+
let requestedAfter = cached ? cached.after : after;
|
|
1469
|
+
const requestedVolatile = cached ? cached.cursor : volatileCursor;
|
|
1470
|
+
const page = await client._delivery.runPeerPageSerialized(ns, async () => {
|
|
1471
|
+
const committedAck = client._delivery.peerCommittedAck(ns);
|
|
1472
|
+
if (!initializedAck) {
|
|
1473
|
+
durableAck = committedAck;
|
|
1474
|
+
initializedAck = true;
|
|
1475
|
+
}
|
|
1476
|
+
if (!cached && mode === 'forward' && opts.force !== true)
|
|
1477
|
+
requestedAfter = Math.max(requestedAfter, committedAck);
|
|
1478
|
+
if (!cached)
|
|
1479
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1480
|
+
const fetched = await (cached ? cached.prepared ?? cached.response : fetchPage(requestedAfter, requestedVolatile));
|
|
1481
|
+
const { response, reservation } = fetched;
|
|
1482
|
+
try {
|
|
1483
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1484
|
+
const { persistent, volatile, end, pageHead, start } = validatePage(response, requestedAfter, committedAck);
|
|
1485
|
+
const handlers = fetched.handlers ?? client._delivery.snapshotApplicationHandlers?.();
|
|
1486
|
+
if (mode === 'forward' && opts.force !== true && volatile.length === 0 && end > requestedAfter
|
|
1487
|
+
&& (response.persistent_has_more === true || response.has_more === true)) {
|
|
1488
|
+
const pending = prefetchPage(end, requestedVolatile);
|
|
1489
|
+
if (pending)
|
|
1490
|
+
preparePage(pending);
|
|
1491
|
+
}
|
|
1492
|
+
const messages = [];
|
|
1493
|
+
const prepared = fetched.prepared ?? await decodeRows(persistent);
|
|
1494
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1495
|
+
for (const effect of prepared.effects)
|
|
1496
|
+
effect();
|
|
1497
|
+
if ('error' in prepared)
|
|
1498
|
+
throw prepared.error;
|
|
1499
|
+
await this.withApplicationPage(ns, source, async () => {
|
|
1500
|
+
for (let index = 0; index < persistent.length; index += 1) {
|
|
1501
|
+
const row = persistent[index], plaintext = prepared.messages[index];
|
|
1502
|
+
const event = client._delivery.p2pAppEventForMessage(plaintext);
|
|
1503
|
+
await client._delivery.publishPulledMessage(event.event, ns, row.seq, event.payload, false, source);
|
|
1504
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1505
|
+
messages.push(plaintext);
|
|
1506
|
+
}
|
|
1507
|
+
if (volatile.length > 0) {
|
|
1508
|
+
for (const row of volatile)
|
|
1509
|
+
if (String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== senderAid)
|
|
1510
|
+
throw new ValidationError('volatile peer sender mismatch');
|
|
1511
|
+
messages.push(...await this.publishV2VolatileMessages(volatile, source, true));
|
|
1512
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1513
|
+
}
|
|
1514
|
+
}, reservation, handlers);
|
|
1515
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1516
|
+
if (!volatile.every((row) => client._delivery.isVolatileSeen(row)))
|
|
1517
|
+
throw new E2EEError('volatile peer delivery remains pending');
|
|
1518
|
+
const before = client._seqTracker.snapshotNamespace(ns);
|
|
1519
|
+
try {
|
|
1520
|
+
if (mode === 'tail') {
|
|
1521
|
+
client._seqTracker.commitTailWindow(ns, { windowStartSeq: start, headSeq: pageHead, coveredThroughSeq: end, pageLimit: limit, sparse: true });
|
|
1522
|
+
}
|
|
1523
|
+
else if (opts.force !== true) {
|
|
1524
|
+
client._seqTracker.onPullResult(ns, persistent, requestedAfter, end);
|
|
1525
|
+
}
|
|
1526
|
+
if (opts.force !== true) {
|
|
1527
|
+
await client._commitSeqTrackerState(ns);
|
|
1528
|
+
durableAck = Math.max(durableAck, client._seqTracker.getContiguousSeq(ns));
|
|
1529
|
+
}
|
|
1530
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1531
|
+
}
|
|
1532
|
+
catch (error) {
|
|
1533
|
+
if (Number(client._v2RuntimeGeneration ?? 0) === generation
|
|
1534
|
+
&& (inlineGeneration === undefined || client._delivery.captureInlineAckGeneration() === inlineGeneration)
|
|
1535
|
+
&& client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
|
|
1536
|
+
client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, before);
|
|
1537
|
+
throw error;
|
|
1538
|
+
}
|
|
1539
|
+
const ack = client._seqTracker.getContiguousSeq(ns);
|
|
1540
|
+
if (mode === 'forward' && opts.force !== true) {
|
|
1541
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1542
|
+
const next = safeNonNegativeSeq(response.next_pending_seq);
|
|
1543
|
+
client._delivery.consumePeerPullHint(ns, end, response.persistent_has_more === true && next !== null && next > end && next <= pageHead ? next : undefined);
|
|
1544
|
+
confirmAck(Math.max(confirmedAck, safeNonNegativeSeq(response.server_ack_seq) ?? 0));
|
|
1545
|
+
if (ack > confirmedAck)
|
|
1546
|
+
this.recordForwardAck(ns, ack);
|
|
1547
|
+
}
|
|
1548
|
+
return { response, messages, end, head: pageHead };
|
|
1549
|
+
}
|
|
1550
|
+
finally {
|
|
1551
|
+
reservation?.release();
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1555
|
+
if (opts.retainMessages !== false)
|
|
1556
|
+
decoded.push(...page.messages);
|
|
1557
|
+
head = page.head;
|
|
1558
|
+
volatileCursor = client._delivery.peerVolatilePullCursor(senderAid);
|
|
1559
|
+
const more = page.response.persistent_has_more === true || page.response.volatile_has_more === true || page.response.has_more === true;
|
|
1560
|
+
complete = !more;
|
|
1561
|
+
client._delivery.syncProgress(ns, { rawCount: page.response.messages.length, pulledCount: page.messages.length, hasMore: more,
|
|
1562
|
+
currentSeq: client._seqTracker.getContiguousSeq(ns), targetSeq: head });
|
|
1563
|
+
after = page.end;
|
|
1564
|
+
if (mode === 'tail' || complete || (after <= requestedAfter && volatileCursor === requestedVolatile))
|
|
1565
|
+
break;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
failed = true;
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
finally {
|
|
1573
|
+
stopping.abort();
|
|
1574
|
+
for (const pending of prefetched) {
|
|
1575
|
+
try {
|
|
1576
|
+
(await (pending.prepared ?? pending.response)).reservation?.release();
|
|
1577
|
+
}
|
|
1578
|
+
catch { }
|
|
1579
|
+
}
|
|
1580
|
+
if (mode === 'forward' && opts.force !== true) {
|
|
1581
|
+
try {
|
|
1582
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1583
|
+
const pending = this.pendingForwardAck(ns, durableAck);
|
|
1584
|
+
if (pending > 0) {
|
|
1585
|
+
await this.ackPeerV2(senderAid, pending);
|
|
1586
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1587
|
+
confirmAck(pending);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
catch (error) {
|
|
1591
|
+
if (!failed)
|
|
1592
|
+
throw error;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const ack = client._seqTracker.getContiguousSeq(ns);
|
|
1597
|
+
if (!opts.background && opts.force !== true && (!complete || (mode === 'tail' && ack < head)))
|
|
1598
|
+
client._delivery.schedulePeerRecovery(senderAid, head, true);
|
|
1599
|
+
return { sender_aid: senderAid, messages: decoded, complete: mode === 'tail' ? ack >= head : complete,
|
|
1600
|
+
ack_seq: ack, head_seq: head, pages };
|
|
1601
|
+
}
|
|
1602
|
+
validatePeerResponse(response, senderAid) {
|
|
1603
|
+
const client = this.client;
|
|
1604
|
+
if (!response || response.cursor_scope !== 'peer' || response.sender_aid !== senderAid
|
|
1605
|
+
|| (response.device_id !== undefined && response.device_id !== client._deviceId)
|
|
1606
|
+
|| (response.slot_id !== undefined && response.slot_id !== client._slotId))
|
|
1607
|
+
throw new ValidationError('peer response scope mismatch');
|
|
1608
|
+
}
|
|
1609
|
+
async ackPeerV2(senderAid, upToSeq, volatileCursors = [], volatileIds = []) {
|
|
1610
|
+
const client = this.client;
|
|
1611
|
+
const generation = Number(client._v2RuntimeGeneration ?? 0);
|
|
1612
|
+
const inlineGeneration = client._delivery.captureInlineAckGeneration?.();
|
|
1613
|
+
const ns = await client._delivery.initializePeer(senderAid);
|
|
1614
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1615
|
+
const localAck = client._delivery.peerCommittedAck(ns);
|
|
1616
|
+
const requested = upToSeq === undefined ? (volatileCursors.length || volatileIds.length ? 0 : localAck) : strictWindowSeq(upToSeq, 'up_to_seq');
|
|
1617
|
+
const seq = Math.min(requested, localAck);
|
|
1618
|
+
if (!seq && !volatileCursors.length && !volatileIds.length)
|
|
1619
|
+
return { cursor_scope: 'peer', sender_aid: senderAid, peer_ack_seq: 0 };
|
|
1620
|
+
const result = await client._callRawV2Rpc('message.v2.ack', { cursor_scope: 'peer', sender_aid: senderAid,
|
|
1621
|
+
device_id: client._deviceId, slot_id: client._slotId,
|
|
1622
|
+
...(seq ? { up_to_seq: seq } : {}), ...(volatileCursors.length ? { volatile_cursors: volatileCursors } : {}),
|
|
1623
|
+
...(volatileIds.length ? { volatile_ids: volatileIds } : {}), _rpc_background: true });
|
|
1624
|
+
this.ensureDeliveryCurrent(ns, generation, inlineGeneration);
|
|
1625
|
+
this.validatePeerResponse(result, senderAid);
|
|
1626
|
+
const actual = safeNonNegativeSeq(result.peer_ack_seq ?? result.effective_ack_seq ?? result.up_to_seq);
|
|
1627
|
+
if (seq > 0 && (actual === null || actual < seq))
|
|
1628
|
+
throw new E2EEError('peer ACK watermark is below requested');
|
|
1629
|
+
client._delivery.peerAckChanged();
|
|
1630
|
+
return result;
|
|
1631
|
+
}
|
|
1632
|
+
async peerHistoryV2(params) {
|
|
1633
|
+
const sender = String(params.sender_aid ?? '').trim();
|
|
1634
|
+
if (!sender)
|
|
1635
|
+
throw new ValidationError('peer History requires sender_aid');
|
|
1636
|
+
const client = this.client;
|
|
1637
|
+
const ns = await client._delivery.initializePeer(sender);
|
|
1638
|
+
const generation = Number(client._v2RuntimeGeneration ?? 0);
|
|
1639
|
+
const before = strictWindowSeq(params.before_seq, 'before_seq', true);
|
|
1640
|
+
const limit = Number(params.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize);
|
|
1641
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200)
|
|
1642
|
+
throw new ValidationError('peer limit must be between 1 and 200');
|
|
1643
|
+
const key = pullGateKeyForClient(client, 'message.history', { ...params, sender_aid: sender, before_seq: before, limit }, `${ns}|history|before=${before}|limit=${limit}`);
|
|
1644
|
+
const result = await client._runPullSerialized(key, () => client._callRawV2Rpc('message.history', { ...params, cursor_scope: 'peer', sender_aid: sender,
|
|
1645
|
+
before_seq: before, limit, device_id: client._deviceId, slot_id: client._slotId, _rpc_foreground: true }, key), false);
|
|
1646
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1647
|
+
this.validatePeerResponse(result, sender);
|
|
1648
|
+
const rows = validateHistoryPage(result, before);
|
|
1649
|
+
if (rows.length > limit)
|
|
1650
|
+
throw new ValidationError('peer History exceeds limit');
|
|
1651
|
+
const messages = [];
|
|
1652
|
+
for (const row of rows) {
|
|
1653
|
+
if (String(row.sender_aid ?? row.from_aid ?? row.from ?? '') !== sender)
|
|
1654
|
+
throw new ValidationError('peer History sender mismatch');
|
|
1655
|
+
const plaintext = await this.decodeP2PWindowMessage(row, false, true, 'history');
|
|
1656
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1657
|
+
if (plaintext)
|
|
1658
|
+
messages.push({ ...plaintext, need_render: typeof row.need_render === 'boolean' ? row.need_render : plaintext.need_render !== false });
|
|
1659
|
+
}
|
|
1660
|
+
return { ...result, messages };
|
|
1661
|
+
}
|
|
1259
1662
|
async pullV2TailInternal(params, deferPublish = false) {
|
|
1260
1663
|
const client = this.client;
|
|
1261
1664
|
const generation = Number(client._v2RuntimeGeneration ?? 0);
|
|
@@ -1274,12 +1677,14 @@ export class V2E2EECoordinator {
|
|
|
1274
1677
|
const result = await client._callRawV2Rpc('message.v2.pull', { window_mode: 'tail', after_seq: afterSeq, limit, _rpc_foreground: true }, pullGateKey);
|
|
1275
1678
|
const page = validateTailPage(result, afterSeq, limit);
|
|
1276
1679
|
const decoded = [];
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1680
|
+
await this.withApplicationPage(ns, 'tail', async () => {
|
|
1681
|
+
for (const msg of page.messages) {
|
|
1682
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1683
|
+
const plaintext = await this.decodeP2PWindowMessage(msg, true, false, 'tail', deferPublish);
|
|
1684
|
+
if (plaintext)
|
|
1685
|
+
decoded.push(plaintext);
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1283
1688
|
this.ensureDeliveryCurrent(ns, generation);
|
|
1284
1689
|
if (ns) {
|
|
1285
1690
|
client._seqTracker.commitTailWindow(ns, {
|
|
@@ -1361,10 +1766,9 @@ export class V2E2EECoordinator {
|
|
|
1361
1766
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
1362
1767
|
let pipelineRunning = false;
|
|
1363
1768
|
let pipelineError = null;
|
|
1364
|
-
|
|
1769
|
+
let pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
|
|
1365
1770
|
let pipelineProducer = null;
|
|
1366
1771
|
let pipelineStopped = false;
|
|
1367
|
-
const pipelineResume = { current: null };
|
|
1368
1772
|
const startPipeline = (startAfterSeq, startVolatileCursor) => {
|
|
1369
1773
|
if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT)
|
|
1370
1774
|
return;
|
|
@@ -1375,30 +1779,31 @@ export class V2E2EECoordinator {
|
|
|
1375
1779
|
for (let produced = pageCount; produced < maxPages; produced += 1) {
|
|
1376
1780
|
if (pipelineStopped)
|
|
1377
1781
|
break;
|
|
1782
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1378
1783
|
const requestedVolatileCursor = producerVolatileCursor;
|
|
1784
|
+
const ackUpToSeq = pendingAckSeq;
|
|
1785
|
+
const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery.captureP2PInlineAckGeneration() : null;
|
|
1379
1786
|
const result = await client._callRawV2Rpc('message.v2.pull', {
|
|
1380
1787
|
after_seq: producerAfterSeq,
|
|
1381
1788
|
limit,
|
|
1789
|
+
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
1382
1790
|
...(producerVolatileCursor ? { after_volatile_cursor: producerVolatileCursor } : {}),
|
|
1383
1791
|
...(opts?.force ? { force: true } : {}),
|
|
1384
1792
|
}, pullGateKey);
|
|
1385
1793
|
if (pipelineStopped)
|
|
1386
1794
|
break;
|
|
1795
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1387
1796
|
const rawPageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1388
|
-
const completed = new Promise((resolve) => { pipelineResume.current = resolve; });
|
|
1389
1797
|
const enqueued = await pipelineQueue.put({
|
|
1390
1798
|
result,
|
|
1391
1799
|
afterSeq: producerAfterSeq,
|
|
1392
|
-
ackUpToSeq
|
|
1393
|
-
inlineAckGeneration
|
|
1800
|
+
ackUpToSeq,
|
|
1801
|
+
inlineAckGeneration,
|
|
1394
1802
|
requestedVolatileCursor,
|
|
1395
1803
|
weight: rawPageMessages.length,
|
|
1396
1804
|
}, rawPageMessages.length);
|
|
1397
1805
|
if (!enqueued || pipelineStopped)
|
|
1398
1806
|
break;
|
|
1399
|
-
await completed;
|
|
1400
|
-
if (pipelineStopped)
|
|
1401
|
-
break;
|
|
1402
1807
|
const volatileMessages = client._delivery.volatileMessages?.(rawPageMessages) ?? [];
|
|
1403
1808
|
if (volatileMessages.length > 0)
|
|
1404
1809
|
break;
|
|
@@ -1406,7 +1811,7 @@ export class V2E2EECoordinator {
|
|
|
1406
1811
|
const seqs = messages
|
|
1407
1812
|
.map((msg) => Number(msg.seq ?? 0))
|
|
1408
1813
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
1409
|
-
const nextAfter = Math.max(
|
|
1814
|
+
const nextAfter = Math.max(producerAfterSeq, ...seqs);
|
|
1410
1815
|
const cursor = isJsonObject(result.cursor)
|
|
1411
1816
|
? result.cursor : null;
|
|
1412
1817
|
const shouldContinue = shouldContinueForwardPage({
|
|
@@ -1433,8 +1838,6 @@ export class V2E2EECoordinator {
|
|
|
1433
1838
|
};
|
|
1434
1839
|
const stopPipeline = async () => {
|
|
1435
1840
|
pipelineStopped = true;
|
|
1436
|
-
pipelineResume.current?.();
|
|
1437
|
-
pipelineResume.current = null;
|
|
1438
1841
|
pipelineQueue.close();
|
|
1439
1842
|
if (pipelineProducer) {
|
|
1440
1843
|
const producer = pipelineProducer;
|
|
@@ -1458,7 +1861,7 @@ export class V2E2EECoordinator {
|
|
|
1458
1861
|
pageCount += 1;
|
|
1459
1862
|
const requestedVolatileCursor = pipelinePage?.requestedVolatileCursor ?? volatileCursor;
|
|
1460
1863
|
const ackUpToSeq = pipelinePage?.ackUpToSeq ?? pendingAckSeq;
|
|
1461
|
-
const inlineAckGeneration = pipelinePage
|
|
1864
|
+
const inlineAckGeneration = pipelinePage ? pipelinePage.inlineAckGeneration : (ackUpToSeq > 0
|
|
1462
1865
|
? client._delivery.captureP2PInlineAckGeneration()
|
|
1463
1866
|
: null);
|
|
1464
1867
|
client._clientLog.debug(`message.v2.pull page request: page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns || '<none>'}`);
|
|
@@ -1487,28 +1890,30 @@ export class V2E2EECoordinator {
|
|
|
1487
1890
|
continue;
|
|
1488
1891
|
}
|
|
1489
1892
|
}
|
|
1893
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1490
1894
|
if (ackUpToSeq > 0) {
|
|
1491
1895
|
const actualAckSeq = client._delivery.resolveP2PPullAckSeq(result, ackUpToSeq);
|
|
1492
1896
|
if (actualAckSeq >= ackUpToSeq) {
|
|
1493
1897
|
if (inlineAckGeneration !== null) {
|
|
1494
1898
|
client._delivery.onP2PV2PullAckResult(result, ackUpToSeq, inlineAckGeneration);
|
|
1495
1899
|
}
|
|
1496
|
-
pendingAckSeq
|
|
1900
|
+
if (pendingAckSeq <= ackUpToSeq)
|
|
1901
|
+
pendingAckSeq = 0;
|
|
1497
1902
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1498
1903
|
this.clearForwardAck(ns, ackUpToSeq);
|
|
1499
1904
|
}
|
|
1500
1905
|
else {
|
|
1501
1906
|
client._clientLog.warn(`message.v2.pull piggyback ACK below target: requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
1502
|
-
await this.confirmForwardP2PAck(ackUpToSeq);
|
|
1503
|
-
pendingAckSeq = 0;
|
|
1504
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
1505
1907
|
}
|
|
1506
1908
|
}
|
|
1507
1909
|
const pageMessages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
1508
1910
|
const volatileMessages = client._delivery.volatileMessages?.(pageMessages) ?? [];
|
|
1509
|
-
if (volatileMessages.length > 0) {
|
|
1510
|
-
|
|
1511
|
-
|
|
1911
|
+
if (pipelinePage && volatileMessages.length > 0) {
|
|
1912
|
+
if (pipelineProducer)
|
|
1913
|
+
await pipelineProducer;
|
|
1914
|
+
pipelineProducer = null;
|
|
1915
|
+
pipelineRunning = false;
|
|
1916
|
+
pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
|
|
1512
1917
|
}
|
|
1513
1918
|
const messages = deduplicateForwardPageMessages(pageMessages, nextAfterSeq);
|
|
1514
1919
|
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
@@ -1531,118 +1936,128 @@ export class V2E2EECoordinator {
|
|
|
1531
1936
|
pageMaxSeq = Math.max(...seqs);
|
|
1532
1937
|
}
|
|
1533
1938
|
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
1939
|
+
if (!pipelineRunning && volatileMessages.length === 0 && shouldContinueForwardPage({
|
|
1940
|
+
rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
|
|
1941
|
+
hasMoreField, hasMore, serverHead,
|
|
1942
|
+
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._pendingP2pPullUpper?.get?.(ns) ?? 0)),
|
|
1943
|
+
pendingAckSeq: 0,
|
|
1944
|
+
}))
|
|
1945
|
+
startPipeline(pageMaxSeq, volatileCursor);
|
|
1534
1946
|
const deferredKeyFetches = new Map();
|
|
1535
1947
|
let blockedSeq = 0;
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1948
|
+
let pageDecryptedBefore = decrypted.length;
|
|
1949
|
+
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
1950
|
+
const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
|
|
1951
|
+
const serverAckSeq = parsedServerAckSeq ?? 0;
|
|
1952
|
+
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
1953
|
+
let commitTarget = pageContigBefore;
|
|
1954
|
+
await this.withApplicationPage(ns, 'pull', async () => {
|
|
1955
|
+
if (volatileMessages.length > 0) {
|
|
1956
|
+
decrypted.push(...await this.publishV2VolatileMessages(volatileMessages, 'pull', true));
|
|
1957
|
+
pageDecryptedBefore = decrypted.length;
|
|
1958
|
+
}
|
|
1959
|
+
for (const msg of messages) {
|
|
1960
|
+
const seq = Number(msg.seq ?? 0);
|
|
1961
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
1962
|
+
continue;
|
|
1963
|
+
const version = String(msg.version ?? 'v2');
|
|
1964
|
+
if (version === 'v1') {
|
|
1965
|
+
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
1966
|
+
const legacyPayload = legacy.payload !== undefined ? legacy.payload : msg.payload;
|
|
1967
|
+
const payloadType = isJsonObject(legacyPayload)
|
|
1968
|
+
? String(legacyPayload.type ?? '').trim()
|
|
1969
|
+
: '';
|
|
1970
|
+
if (legacyPayload !== undefined && legacyPayload !== null
|
|
1971
|
+
&& !['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
|
|
1972
|
+
const v1Msg = {
|
|
1973
|
+
message_id: String(msg.message_id ?? ''),
|
|
1974
|
+
from: String(msg.from_aid ?? ''),
|
|
1975
|
+
to: String(legacy.to ?? client._aid ?? ''),
|
|
1976
|
+
seq: msg.seq,
|
|
1977
|
+
type: String(msg.type ?? ''),
|
|
1978
|
+
timestamp: msg.t_server,
|
|
1979
|
+
payload: legacyPayload,
|
|
1980
|
+
encrypted: false,
|
|
1981
|
+
};
|
|
1982
|
+
const direction = String(msg.direction ?? '').trim();
|
|
1983
|
+
if (direction)
|
|
1984
|
+
v1Msg.direction = direction;
|
|
1985
|
+
else if (String(msg.from_aid ?? '').trim() && String(msg.from_aid ?? '').trim() !== String(client._aid ?? '').trim()) {
|
|
1986
|
+
v1Msg.direction = 'inbound';
|
|
1987
|
+
}
|
|
1988
|
+
if (msg.sender_device_id !== undefined)
|
|
1989
|
+
v1Msg.sender_device_id = msg.sender_device_id;
|
|
1990
|
+
attachGatewayProximity(v1Msg, msg);
|
|
1991
|
+
const appEvent = client._delivery.p2pAppEventForMessage(v1Msg);
|
|
1992
|
+
if (ns) {
|
|
1993
|
+
await client._publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false);
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
await client._publishAppEvent(appEvent.event, appEvent.payload, 'pull');
|
|
1997
|
+
}
|
|
1998
|
+
decrypted.push(v1Msg);
|
|
1999
|
+
client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
|
|
1565
2000
|
}
|
|
1566
|
-
if (
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
2001
|
+
else if (isJsonObject(legacyPayload)
|
|
2002
|
+
&& ['e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
|
|
2003
|
+
const deferStatus = {};
|
|
2004
|
+
const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(legacyPayload) }, true, true, true, true, deferStatus, true, 'pull');
|
|
2005
|
+
if (deferStatus.deferred) {
|
|
2006
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
2007
|
+
if (deferStatus.fromAid) {
|
|
2008
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
2009
|
+
deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
if (plaintext) {
|
|
2013
|
+
if (ns)
|
|
2014
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
2015
|
+
else
|
|
2016
|
+
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
2017
|
+
decrypted.push(plaintext);
|
|
2018
|
+
}
|
|
1572
2019
|
}
|
|
1573
2020
|
else {
|
|
1574
|
-
|
|
2021
|
+
client._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || '<none>'} (V1 E2EE removed)`);
|
|
1575
2022
|
}
|
|
1576
|
-
|
|
1577
|
-
client._clientLog.debug(`message.v2.pull plaintext V1 delivered: seq=${seq}, ns=${ns || '<none>'}`);
|
|
2023
|
+
continue;
|
|
1578
2024
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2025
|
+
if (version !== 'v2') {
|
|
2026
|
+
client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
const spkId = String(msg.spk_id ?? '');
|
|
2030
|
+
if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
|
|
2031
|
+
client._v2Session.trackOldSPKMaxSeq(spkId, seq);
|
|
2032
|
+
}
|
|
2033
|
+
const deferStatus = {};
|
|
2034
|
+
const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
|
|
2035
|
+
if (deferStatus.deferred) {
|
|
2036
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
2037
|
+
}
|
|
2038
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
2039
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
2040
|
+
deferredKeyFetches.set(key, {
|
|
2041
|
+
fromAid: deferStatus.fromAid,
|
|
2042
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
2043
|
+
groupId: deferStatus.groupId ?? '',
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
if (plaintext === null) {
|
|
2047
|
+
client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
if (ns) {
|
|
2051
|
+
await client._publishPulledMessage('message.received', ns, seq, plaintext, false);
|
|
1597
2052
|
}
|
|
1598
2053
|
else {
|
|
1599
|
-
client.
|
|
2054
|
+
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
1600
2055
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
if (version !== 'v2') {
|
|
1604
|
-
client._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? '')}`);
|
|
1605
|
-
continue;
|
|
1606
|
-
}
|
|
1607
|
-
const spkId = String(msg.spk_id ?? '');
|
|
1608
|
-
if (spkId && client._v2Session && !client._v2Session.isCurrentSPK(spkId)) {
|
|
1609
|
-
client._v2Session.trackOldSPKMaxSeq(spkId, seq);
|
|
1610
|
-
}
|
|
1611
|
-
const deferStatus = {};
|
|
1612
|
-
const plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
|
|
1613
|
-
if (deferStatus.deferred) {
|
|
1614
|
-
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
1615
|
-
}
|
|
1616
|
-
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
1617
|
-
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
1618
|
-
deferredKeyFetches.set(key, {
|
|
1619
|
-
fromAid: deferStatus.fromAid,
|
|
1620
|
-
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
1621
|
-
groupId: deferStatus.groupId ?? '',
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
|
-
if (plaintext === null) {
|
|
1625
|
-
client._clientLog.debug(`message.v2.pull decrypt returned null: seq=${seq}, ns=${ns || '<none>'}`);
|
|
1626
|
-
continue;
|
|
2056
|
+
decrypted.push(plaintext);
|
|
2057
|
+
client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
|
|
1627
2058
|
}
|
|
1628
2059
|
if (ns) {
|
|
1629
|
-
|
|
1630
|
-
}
|
|
1631
|
-
else {
|
|
1632
|
-
await client._publishAppEvent('message.received', plaintext, 'pull');
|
|
1633
|
-
}
|
|
1634
|
-
decrypted.push(plaintext);
|
|
1635
|
-
client._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
|
|
1636
|
-
}
|
|
1637
|
-
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
1638
|
-
const parsedServerAckSeq = hasServerAckSeq ? safeNonNegativeSeq(result.server_ack_seq) : null;
|
|
1639
|
-
const serverAckSeq = parsedServerAckSeq ?? 0;
|
|
1640
|
-
const retentionFloor = Math.max(0, Number(result.retention_floor_seq ?? 0));
|
|
1641
|
-
if (ns) {
|
|
1642
|
-
const commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
|
|
1643
|
-
let ackSeq = pageContigBefore;
|
|
1644
|
-
let contigAdvanced = false;
|
|
1645
|
-
try {
|
|
2060
|
+
commitTarget = Math.max(pageContigBefore, Number.isFinite(retentionFloor) ? retentionFloor : 0, pageCount === 1 ? deferredServerCursor : 0, serverAckSeq, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
|
|
1646
2061
|
this.ensureDeliveryCurrent(ns, generation);
|
|
1647
2062
|
const window = client._seqTracker.getSyncState?.(ns);
|
|
1648
2063
|
const deliveryTarget = window && commitTarget >= window.tail - 1
|
|
@@ -1651,6 +2066,15 @@ export class V2E2EECoordinator {
|
|
|
1651
2066
|
await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
|
|
1652
2067
|
}
|
|
1653
2068
|
this.ensureDeliveryCurrent(ns, generation);
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
if (volatileMessages.length > 0)
|
|
2072
|
+
volatileCursor = String(client._delivery.volatilePullCursor?.() ?? '').trim();
|
|
2073
|
+
if (ns) {
|
|
2074
|
+
let ackSeq = pageContigBefore;
|
|
2075
|
+
let contigAdvanced = false;
|
|
2076
|
+
try {
|
|
2077
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1654
2078
|
pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
|
|
1655
2079
|
if (commitTarget > pageContigBefore) {
|
|
1656
2080
|
trackerMutated = true;
|
|
@@ -1701,18 +2125,8 @@ export class V2E2EECoordinator {
|
|
|
1701
2125
|
|| (contigAdvanced && ackSeq > knownServerAckSeq));
|
|
1702
2126
|
if (ackNeeded) {
|
|
1703
2127
|
this.recordForwardAck(ns, ackSeq);
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
if (canContinuePage && !pipelineRunning) {
|
|
1707
|
-
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
1708
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1709
|
-
client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
|
|
1710
|
-
}
|
|
1711
|
-
else {
|
|
1712
|
-
client._clientLog.debug(`message.v2.pull sending terminal auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
1713
|
-
await this.confirmForwardP2PAck(ackSeq);
|
|
1714
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
1715
|
-
}
|
|
2128
|
+
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
2129
|
+
client._clientLog.debug(`message.v2.pull queued piggyback auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
|
|
1716
2130
|
}
|
|
1717
2131
|
}
|
|
1718
2132
|
if (!ns) {
|
|
@@ -1729,7 +2143,7 @@ export class V2E2EECoordinator {
|
|
|
1729
2143
|
currentSeq: client._seqTracker.getContiguousSeq(ns),
|
|
1730
2144
|
targetSeq: serverHead,
|
|
1731
2145
|
});
|
|
1732
|
-
const nextAfter = Math.max(
|
|
2146
|
+
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
1733
2147
|
const rawCount = messages.length;
|
|
1734
2148
|
if (blockedSeq > 0)
|
|
1735
2149
|
break;
|
|
@@ -1761,11 +2175,7 @@ export class V2E2EECoordinator {
|
|
|
1761
2175
|
&& volatileCursor !== requestedVolatileCursor;
|
|
1762
2176
|
if (!shouldContinuePersistent && !shouldContinueVolatile)
|
|
1763
2177
|
break;
|
|
1764
|
-
if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
|
|
1765
|
-
if (pendingAckSeq > 0) {
|
|
1766
|
-
await this.confirmForwardP2PAck(pendingAckSeq);
|
|
1767
|
-
pendingAckSeq = 0;
|
|
1768
|
-
}
|
|
2178
|
+
if (shouldContinueForPipeline && volatileMessages.length === 0 && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
|
|
1769
2179
|
startPipeline(nextAfter, volatileCursor);
|
|
1770
2180
|
}
|
|
1771
2181
|
if (!pipelineRunning && pullGateKey && opts?.gateLocked && client._rpcPipeline?.yieldPullGate) {
|
|
@@ -1775,14 +2185,10 @@ export class V2E2EECoordinator {
|
|
|
1775
2185
|
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
1776
2186
|
pullGateKey = nextKey;
|
|
1777
2187
|
}
|
|
1778
|
-
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
2188
|
+
const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
|
|
1779
2189
|
if (tailDelayMs > 0)
|
|
1780
2190
|
await v2Sleep(tailDelayMs);
|
|
1781
2191
|
nextAfterSeq = nextAfter;
|
|
1782
|
-
if (pipelinePage) {
|
|
1783
|
-
pipelineResume.current?.();
|
|
1784
|
-
pipelineResume.current = null;
|
|
1785
|
-
}
|
|
1786
2192
|
}
|
|
1787
2193
|
}
|
|
1788
2194
|
finally {
|
|
@@ -1790,11 +2196,14 @@ export class V2E2EECoordinator {
|
|
|
1790
2196
|
}
|
|
1791
2197
|
if (pageCount >= maxPages) {
|
|
1792
2198
|
client._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2199
|
+
}
|
|
2200
|
+
if (pendingAckSeq > 0) {
|
|
2201
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2202
|
+
client._clientLog.debug(`message.v2.pull flushing pending auto-ack: ns=${ns}, ack_seq=${pendingAckSeq}`);
|
|
2203
|
+
const ackToFlush = pendingAckSeq;
|
|
2204
|
+
await this.confirmForwardP2PAck(ackToFlush);
|
|
2205
|
+
if (pendingAckSeq <= ackToFlush)
|
|
1796
2206
|
pendingAckSeq = 0;
|
|
1797
|
-
}
|
|
1798
2207
|
}
|
|
1799
2208
|
client._clientLog.debug(`message.v2.pull done: requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns || '<none>'}`);
|
|
1800
2209
|
return decrypted;
|
|
@@ -1816,11 +2225,27 @@ export class V2E2EECoordinator {
|
|
|
1816
2225
|
const client = this.client;
|
|
1817
2226
|
if (!client._v2Session)
|
|
1818
2227
|
return [];
|
|
2228
|
+
const generation = Number(client._v2RuntimeGeneration ?? 0);
|
|
2229
|
+
const context = JSON.stringify([client._aid, client._deviceId, client._slotId]);
|
|
2230
|
+
const ensureCurrent = () => {
|
|
2231
|
+
if (!client._p2pPeerPullEnabled)
|
|
2232
|
+
return;
|
|
2233
|
+
if (context !== JSON.stringify([client._aid, client._deviceId, client._slotId]))
|
|
2234
|
+
throw new StateError('delivery invalidated');
|
|
2235
|
+
const sender = String(messages[0]?.sender_aid ?? messages[0]?.from_aid ?? messages[0]?.from ?? '').trim();
|
|
2236
|
+
if (sender)
|
|
2237
|
+
this.ensureDeliveryCurrent(client._delivery.peerNamespace(sender), generation);
|
|
2238
|
+
};
|
|
1819
2239
|
const processed = [];
|
|
1820
2240
|
const plaintextMessages = [];
|
|
1821
2241
|
let complete = true;
|
|
1822
2242
|
for (const rawMessage of messages) {
|
|
1823
|
-
|
|
2243
|
+
let claim = client._delivery.beginVolatileDelivery(rawMessage);
|
|
2244
|
+
if (claim === 'inflight' && typeof client._delivery.waitVolatileDelivery === 'function') {
|
|
2245
|
+
await client._delivery.waitVolatileDelivery(rawMessage);
|
|
2246
|
+
ensureCurrent();
|
|
2247
|
+
claim = client._delivery.beginVolatileDelivery(rawMessage);
|
|
2248
|
+
}
|
|
1824
2249
|
if (claim === 'seen') {
|
|
1825
2250
|
processed.push(rawMessage);
|
|
1826
2251
|
continue;
|
|
@@ -1859,13 +2284,16 @@ export class V2E2EECoordinator {
|
|
|
1859
2284
|
complete = false;
|
|
1860
2285
|
continue;
|
|
1861
2286
|
}
|
|
2287
|
+
ensureCurrent();
|
|
1862
2288
|
const publicMessage = client._delivery.publicVolatileMessage(plaintext);
|
|
1863
2289
|
for (const key of ['storage', 'volatile_cursor', 'created_at', 'expires_at', 'persist']) {
|
|
1864
2290
|
if (Object.prototype.hasOwnProperty.call(rawMessage, key))
|
|
1865
2291
|
publicMessage[key] = rawMessage[key];
|
|
1866
2292
|
}
|
|
2293
|
+
publicMessage.need_render = typeof rawMessage.need_render === 'boolean' ? rawMessage.need_render : publicMessage.need_render !== false;
|
|
1867
2294
|
const appEvent = client._delivery.p2pAppEventForMessage(publicMessage);
|
|
1868
2295
|
await client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
2296
|
+
ensureCurrent();
|
|
1869
2297
|
client._delivery.markVolatileSeen(rawMessage);
|
|
1870
2298
|
processed.push(rawMessage);
|
|
1871
2299
|
plaintextMessages.push(publicMessage);
|
|
@@ -1875,14 +2303,17 @@ export class V2E2EECoordinator {
|
|
|
1875
2303
|
}
|
|
1876
2304
|
}
|
|
1877
2305
|
if (processed.length > 0) {
|
|
2306
|
+
ensureCurrent();
|
|
1878
2307
|
await client._delivery.ackVolatileMessages(processed, true, advanceCursor && complete && processed.length === messages.length);
|
|
1879
2308
|
}
|
|
1880
2309
|
return plaintextMessages;
|
|
1881
2310
|
}
|
|
1882
2311
|
async confirmForwardP2PAck(upToSeq) {
|
|
1883
2312
|
const ns = this.client._aid ? `p2p:${this.client._aid}` : '';
|
|
2313
|
+
const generation = Number(this.client._v2RuntimeGeneration ?? 0);
|
|
1884
2314
|
try {
|
|
1885
2315
|
const result = await this.ackV2ForForward(upToSeq);
|
|
2316
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
1886
2317
|
const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
|
|
1887
2318
|
if (actualAckSeq < upToSeq) {
|
|
1888
2319
|
throw new E2EEError(`message.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
@@ -1890,7 +2321,9 @@ export class V2E2EECoordinator {
|
|
|
1890
2321
|
this.clearForwardAck(ns, upToSeq);
|
|
1891
2322
|
}
|
|
1892
2323
|
catch (exc) {
|
|
1893
|
-
this.
|
|
2324
|
+
if (Number(this.client._v2RuntimeGeneration ?? 0) === generation
|
|
2325
|
+
&& this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
|
|
2326
|
+
this.recordForwardAck(ns, upToSeq);
|
|
1894
2327
|
throw exc;
|
|
1895
2328
|
}
|
|
1896
2329
|
}
|
|
@@ -1985,6 +2418,7 @@ export class V2E2EECoordinator {
|
|
|
1985
2418
|
timestamp: opts?.timestamp,
|
|
1986
2419
|
protectedHeaders: opts?.protectedHeaders,
|
|
1987
2420
|
context: opts?.context,
|
|
2421
|
+
needRender: opts?.needRender,
|
|
1988
2422
|
useCache,
|
|
1989
2423
|
});
|
|
1990
2424
|
const result = await client.call('group.v2.send', withExplicitGroupAid({
|
|
@@ -2001,6 +2435,7 @@ export class V2E2EECoordinator {
|
|
|
2001
2435
|
payload,
|
|
2002
2436
|
message_id: messageId,
|
|
2003
2437
|
timestamp: opts?.timestamp,
|
|
2438
|
+
need_render: opts?.needRender ?? true,
|
|
2004
2439
|
protected_headers: opts?.protectedHeaders,
|
|
2005
2440
|
context: opts?.context,
|
|
2006
2441
|
}, groupAid), result, true);
|
|
@@ -2016,6 +2451,7 @@ export class V2E2EECoordinator {
|
|
|
2016
2451
|
payload,
|
|
2017
2452
|
message_id: messageId,
|
|
2018
2453
|
timestamp: opts?.timestamp,
|
|
2454
|
+
need_render: opts?.needRender ?? true,
|
|
2019
2455
|
protected_headers: opts?.protectedHeaders,
|
|
2020
2456
|
context: opts?.context,
|
|
2021
2457
|
}, groupAid), result, true);
|
|
@@ -2140,6 +2576,7 @@ export class V2E2EECoordinator {
|
|
|
2140
2576
|
timestamp: opts.timestamp,
|
|
2141
2577
|
protectedHeaders: opts.protectedHeaders,
|
|
2142
2578
|
context: opts.context,
|
|
2579
|
+
needRender: opts.needRender,
|
|
2143
2580
|
}, stateCommitment);
|
|
2144
2581
|
client._logMessageDebug('send-envelope', 'group.send.v2', 'group.send', {
|
|
2145
2582
|
group_id: groupId,
|
|
@@ -2161,7 +2598,7 @@ export class V2E2EECoordinator {
|
|
|
2161
2598
|
});
|
|
2162
2599
|
return envelope;
|
|
2163
2600
|
}
|
|
2164
|
-
async pullGroupV2TailInternal(params, deferPublish = false) {
|
|
2601
|
+
async pullGroupV2TailInternal(params, deferPublish = false, namespaceLocked = false) {
|
|
2165
2602
|
const client = this.client;
|
|
2166
2603
|
const generation = Number(client._v2RuntimeGeneration ?? 0);
|
|
2167
2604
|
if (params.window_mode !== 'tail')
|
|
@@ -2175,6 +2612,7 @@ export class V2E2EECoordinator {
|
|
|
2175
2612
|
const afterSeq = strictWindowSeq(params.after_seq ?? 0, 'after_seq');
|
|
2176
2613
|
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
2177
2614
|
const ns = `group:${groupId}`;
|
|
2615
|
+
const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
|
|
2178
2616
|
const explicitCursorParams = isJsonObject(params._group_cursor_params)
|
|
2179
2617
|
? params._group_cursor_params : {};
|
|
2180
2618
|
const cursorParams = Object.keys(explicitCursorParams).length > 0 ? explicitCursorParams : params;
|
|
@@ -2191,35 +2629,55 @@ export class V2E2EECoordinator {
|
|
|
2191
2629
|
limit,
|
|
2192
2630
|
_group_cursor_params: explicitCursorParams,
|
|
2193
2631
|
}, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
|
|
2194
|
-
|
|
2632
|
+
if (!namespaceLocked)
|
|
2633
|
+
return await client._rpcPipeline.runPullNamespaceSerialized(pullGateKey, () => this.pullGroupV2TailInternal(params, deferPublish, true), false);
|
|
2634
|
+
const result = await client._runPullSerialized(pullGateKey, () => client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
2195
2635
|
group_id: groupId,
|
|
2196
2636
|
window_mode: 'tail',
|
|
2197
2637
|
after_seq: afterSeq,
|
|
2198
2638
|
limit,
|
|
2199
2639
|
_group_cursor_params: explicitCursorParams,
|
|
2200
2640
|
_rpc_foreground: true,
|
|
2201
|
-
}, groupAid), pullGateKey);
|
|
2641
|
+
}, groupAid), pullGateKey), false);
|
|
2202
2642
|
const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
2203
2643
|
if (resultAid)
|
|
2204
2644
|
groupAid = resultAid;
|
|
2205
2645
|
const page = validateTailPage(result, afterSeq, limit);
|
|
2206
2646
|
const decoded = [];
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
client._seqTracker.commitTailWindow(ns, {
|
|
2216
|
-
windowStartSeq: page.windowStart,
|
|
2217
|
-
headSeq: page.head,
|
|
2218
|
-
coveredThroughSeq: page.head,
|
|
2219
|
-
pageLimit: limit,
|
|
2647
|
+
await this.withApplicationPage(ns, 'tail', async () => {
|
|
2648
|
+
for (const msg of page.messages) {
|
|
2649
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2650
|
+
client._logMessageDebug('tail-raw', 'group.v2.pull', 'group.message_created', msg);
|
|
2651
|
+
const plaintext = await this.decodeGroupWindowMessage(msg, groupId, groupAid, true, false, 'tail', deferPublish);
|
|
2652
|
+
if (plaintext)
|
|
2653
|
+
decoded.push(plaintext);
|
|
2654
|
+
}
|
|
2220
2655
|
});
|
|
2221
|
-
|
|
2222
|
-
|
|
2656
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2657
|
+
if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
|
|
2658
|
+
throw new Error('pull invalidated');
|
|
2659
|
+
client._delivery.rememberCommittedSeqState?.(ns);
|
|
2660
|
+
const snapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
|
|
2661
|
+
try {
|
|
2662
|
+
client._seqTracker.commitTailWindow(ns, {
|
|
2663
|
+
windowStartSeq: page.windowStart,
|
|
2664
|
+
headSeq: page.head,
|
|
2665
|
+
coveredThroughSeq: page.head,
|
|
2666
|
+
pageLimit: limit,
|
|
2667
|
+
});
|
|
2668
|
+
await client._commitSeqTrackerState(ns);
|
|
2669
|
+
if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
|
|
2670
|
+
throw new Error('pull invalidated');
|
|
2671
|
+
}
|
|
2672
|
+
catch (error) {
|
|
2673
|
+
if (Number(client._v2RuntimeGeneration ?? 0) === generation
|
|
2674
|
+
&& client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
|
|
2675
|
+
&& client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false) {
|
|
2676
|
+
(client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
|
|
2677
|
+
.call(client._seqTracker, ns, snapshot);
|
|
2678
|
+
}
|
|
2679
|
+
throw error;
|
|
2680
|
+
}
|
|
2223
2681
|
this.ensureDeliveryCurrent(ns, generation);
|
|
2224
2682
|
const ack = client._seqTracker.getContiguousSeq(ns);
|
|
2225
2683
|
if (ownsCursor)
|
|
@@ -2241,12 +2699,13 @@ export class V2E2EECoordinator {
|
|
|
2241
2699
|
let groupAid = String(params.group_aid ?? '').trim();
|
|
2242
2700
|
const beforeSeq = strictWindowSeq(params.before_seq, 'before_seq', true);
|
|
2243
2701
|
const limit = strictWindowLimit(params.limit ?? V2_PULL_DEFAULT_PAGE_LIMIT);
|
|
2244
|
-
const
|
|
2702
|
+
const key = pullGateKeyForClient(client, 'group.history', { group_id: groupId, before_seq: beforeSeq, limit }, `group:${groupId}|history|before=${beforeSeq}|limit=${limit}`);
|
|
2703
|
+
const result = await client._runPullSerialized(key, () => client._callRawV2Rpc('group.history', withExplicitGroupAid({
|
|
2245
2704
|
group_id: groupId,
|
|
2246
2705
|
before_seq: beforeSeq,
|
|
2247
2706
|
limit,
|
|
2248
2707
|
_rpc_foreground: true,
|
|
2249
|
-
}, groupAid));
|
|
2708
|
+
}, groupAid), key), false);
|
|
2250
2709
|
const resultAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
2251
2710
|
if (resultAid)
|
|
2252
2711
|
groupAid = resultAid;
|
|
@@ -2283,7 +2742,7 @@ export class V2E2EECoordinator {
|
|
|
2283
2742
|
group_id: gid, window_mode: 'tail', after_seq: afterSeq, limit,
|
|
2284
2743
|
_group_cursor_params: opts?.cursorParams,
|
|
2285
2744
|
}, `${ns}|tail|after=${afterSeq}|limit=${limit}`);
|
|
2286
|
-
return (await client.
|
|
2745
|
+
return (await client._rpcPipeline.runPullNamespaceSerialized(key, () => this.pullGroupV2Impl(gid, afterSeq, limit, {
|
|
2287
2746
|
...opts,
|
|
2288
2747
|
gateLocked: true,
|
|
2289
2748
|
}), false));
|
|
@@ -2296,7 +2755,7 @@ export class V2E2EECoordinator {
|
|
|
2296
2755
|
window_mode: 'tail',
|
|
2297
2756
|
after_seq: afterSeq,
|
|
2298
2757
|
limit,
|
|
2299
|
-
});
|
|
2758
|
+
}, false, true);
|
|
2300
2759
|
return (Array.isArray(result.messages) ? result.messages : []);
|
|
2301
2760
|
}
|
|
2302
2761
|
if (!opts?.gateLocked) {
|
|
@@ -2304,20 +2763,17 @@ export class V2E2EECoordinator {
|
|
|
2304
2763
|
group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
|
|
2305
2764
|
_group_cursor_params: opts?.cursorParams,
|
|
2306
2765
|
}, `${ns}|forward|after=${afterSeq}|force=${opts?.force === true}|limit=${limit}`);
|
|
2307
|
-
return await client.
|
|
2766
|
+
return await client._rpcPipeline.runPullNamespaceSerialized(key, async () => this.pullGroupV2Impl(gid, afterSeq, limit, {
|
|
2308
2767
|
...(opts ?? {}),
|
|
2309
2768
|
gateLocked: true,
|
|
2310
2769
|
scheduleFollowup: true,
|
|
2311
|
-
}),
|
|
2770
|
+
}), opts?.background !== false);
|
|
2312
2771
|
}
|
|
2313
2772
|
const decrypted = [];
|
|
2314
2773
|
const wireGroupId = String(opts?.wireGroupId ?? groupId ?? '').trim() || gid;
|
|
2315
2774
|
const cursorParams = opts?.cursorParams ?? {};
|
|
2316
2775
|
const ownsCursor = opts?.ownsCursor !== false;
|
|
2317
|
-
|
|
2318
|
-
group_id: gid, after_seq: afterSeq, force: opts?.force === true, limit,
|
|
2319
|
-
_group_cursor_params: cursorParams,
|
|
2320
|
-
}, `${ns}|forward|after=${afterSeq}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
2776
|
+
const background = opts?.background !== false;
|
|
2321
2777
|
let nextAfterSeq = (opts?.explicitAfterSeq || opts?.force)
|
|
2322
2778
|
? afterSeq
|
|
2323
2779
|
: (afterSeq || client._seqTracker.getContiguousSeq(ns));
|
|
@@ -2328,13 +2784,27 @@ export class V2E2EECoordinator {
|
|
|
2328
2784
|
let pageCount = 0;
|
|
2329
2785
|
let lastAutoAckSeq = 0;
|
|
2330
2786
|
let pendingAckSeq = deferredForwardAck;
|
|
2787
|
+
let committedAckSeq = ownsCursor ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
2788
|
+
const fetchPage = async (after, ack) => {
|
|
2789
|
+
const key = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
2790
|
+
group_id: gid, after_seq: after, force: opts?.force === true, limit, _group_cursor_params: cursorParams,
|
|
2791
|
+
}, `${ns}|forward|after=${after}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
2792
|
+
return await client._runPullSerialized(key, async () => {
|
|
2793
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2794
|
+
return await client._callRawV2Rpc('group.v2.pull', withExplicitGroupAid({
|
|
2795
|
+
group_id: wireGroupId, after_seq: after, limit, ...cursorParams,
|
|
2796
|
+
...(opts?.force ? { force: true } : {}),
|
|
2797
|
+
...(ack > 0 ? { ack_up_to_seq: Math.min(ack, committedAckSeq) } : {}),
|
|
2798
|
+
_rpc_background: background, _rpc_foreground: !background,
|
|
2799
|
+
}, groupAid), key);
|
|
2800
|
+
}, background);
|
|
2801
|
+
};
|
|
2331
2802
|
const maxPages = Math.max(1, Math.min(100, Math.trunc(Number(opts?.maxPages ?? 100) || 100)));
|
|
2332
2803
|
let pipelineRunning = false;
|
|
2333
2804
|
let pipelineError = null;
|
|
2334
2805
|
const pipelineQueue = new V2PullPageQueue(V2_PULL_PIPELINE_QUEUE_CAPACITY);
|
|
2335
2806
|
let pipelineProducer = null;
|
|
2336
2807
|
let pipelineStopped = false;
|
|
2337
|
-
const pipelineResume = { current: null };
|
|
2338
2808
|
const startPipeline = (startAfterSeq) => {
|
|
2339
2809
|
if (pipelineRunning || pageCount >= maxPages || limit < V2_PULL_MAX_PAGE_LIMIT || !ownsCursor)
|
|
2340
2810
|
return;
|
|
@@ -2344,35 +2814,29 @@ export class V2E2EECoordinator {
|
|
|
2344
2814
|
for (let produced = pageCount; produced < maxPages; produced += 1) {
|
|
2345
2815
|
if (pipelineStopped)
|
|
2346
2816
|
break;
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
...cursorParams,
|
|
2352
|
-
...(opts?.force ? { force: true } : {}),
|
|
2353
|
-
}, groupAid), pullGateKey);
|
|
2817
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2818
|
+
const ackUpToSeq = Math.min(pendingAckSeq, committedAckSeq);
|
|
2819
|
+
const inlineAckGeneration = ackUpToSeq > 0 ? client._delivery.captureGroupInlineAckGeneration() : null;
|
|
2820
|
+
const result = await fetchPage(producerAfterSeq, ackUpToSeq);
|
|
2354
2821
|
if (pipelineStopped)
|
|
2355
2822
|
break;
|
|
2823
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2356
2824
|
const pageMessages = (Array.isArray(result.messages) ? result.messages : []);
|
|
2357
|
-
const completed = new Promise((resolve) => { pipelineResume.current = resolve; });
|
|
2358
2825
|
const enqueued = await pipelineQueue.put({
|
|
2359
2826
|
result,
|
|
2360
2827
|
afterSeq: producerAfterSeq,
|
|
2361
|
-
ackUpToSeq
|
|
2362
|
-
inlineAckGeneration
|
|
2828
|
+
ackUpToSeq,
|
|
2829
|
+
inlineAckGeneration,
|
|
2363
2830
|
requestedVolatileCursor: '',
|
|
2364
2831
|
weight: pageMessages.length,
|
|
2365
2832
|
}, pageMessages.length);
|
|
2366
2833
|
if (!enqueued || pipelineStopped)
|
|
2367
2834
|
break;
|
|
2368
|
-
await completed;
|
|
2369
|
-
if (pipelineStopped)
|
|
2370
|
-
break;
|
|
2371
2835
|
const messages = deduplicateForwardPageMessages(pageMessages, producerAfterSeq);
|
|
2372
2836
|
const seqs = messages
|
|
2373
2837
|
.map((msg) => Number(msg.seq ?? 0))
|
|
2374
2838
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
2375
|
-
const nextAfter = Math.max(
|
|
2839
|
+
const nextAfter = Math.max(producerAfterSeq, ...seqs);
|
|
2376
2840
|
const cursor = isJsonObject(result.cursor)
|
|
2377
2841
|
? result.cursor : null;
|
|
2378
2842
|
const shouldContinue = shouldContinueForwardPage({
|
|
@@ -2398,8 +2862,6 @@ export class V2E2EECoordinator {
|
|
|
2398
2862
|
};
|
|
2399
2863
|
const stopPipeline = async () => {
|
|
2400
2864
|
pipelineStopped = true;
|
|
2401
|
-
pipelineResume.current?.();
|
|
2402
|
-
pipelineResume.current = null;
|
|
2403
2865
|
pipelineQueue.close();
|
|
2404
2866
|
if (pipelineProducer) {
|
|
2405
2867
|
const producer = pipelineProducer;
|
|
@@ -2422,7 +2884,7 @@ export class V2E2EECoordinator {
|
|
|
2422
2884
|
}
|
|
2423
2885
|
pageCount += 1;
|
|
2424
2886
|
const ackUpToSeq = ownsCursor ? (pipelinePage?.ackUpToSeq ?? pendingAckSeq) : 0;
|
|
2425
|
-
const inlineAckGeneration = pipelinePage
|
|
2887
|
+
const inlineAckGeneration = pipelinePage ? pipelinePage.inlineAckGeneration : (ackUpToSeq > 0
|
|
2426
2888
|
? client._delivery.captureGroupInlineAckGeneration()
|
|
2427
2889
|
: null);
|
|
2428
2890
|
client._clientLog.debug(`group.v2.pull page request: group=${gid}, page=${pageCount}, after_seq=${nextAfterSeq}, limit=${limit}, ack_up_to_seq=${ackUpToSeq}, ns=${ns}`);
|
|
@@ -2432,30 +2894,23 @@ export class V2E2EECoordinator {
|
|
|
2432
2894
|
result = pipelinePage.result;
|
|
2433
2895
|
}
|
|
2434
2896
|
else {
|
|
2435
|
-
result = await
|
|
2436
|
-
group_id: wireGroupId,
|
|
2437
|
-
after_seq: nextAfterSeq,
|
|
2438
|
-
limit,
|
|
2439
|
-
...cursorParams,
|
|
2440
|
-
...(opts?.force ? { force: true } : {}),
|
|
2441
|
-
...(ackUpToSeq > 0 ? { ack_up_to_seq: ackUpToSeq } : {}),
|
|
2442
|
-
}, groupAid), pullGateKey);
|
|
2897
|
+
result = await fetchPage(nextAfterSeq, ackUpToSeq);
|
|
2443
2898
|
}
|
|
2899
|
+
if (ownsCursor)
|
|
2900
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2444
2901
|
if (ackUpToSeq > 0) {
|
|
2445
2902
|
const actualAckSeq = client._delivery.resolveGroupPullAckSeq(result, ackUpToSeq);
|
|
2446
2903
|
if (actualAckSeq >= ackUpToSeq) {
|
|
2447
2904
|
if (inlineAckGeneration !== null) {
|
|
2448
2905
|
client._delivery.onGroupV2PullAckResult(gid, result, ackUpToSeq, inlineAckGeneration);
|
|
2449
2906
|
}
|
|
2450
|
-
pendingAckSeq
|
|
2907
|
+
if (pendingAckSeq <= ackUpToSeq)
|
|
2908
|
+
pendingAckSeq = 0;
|
|
2451
2909
|
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
2452
2910
|
this.clearForwardAck(ns, ackUpToSeq);
|
|
2453
2911
|
}
|
|
2454
2912
|
else {
|
|
2455
2913
|
client._clientLog.warn(`group.v2.pull piggyback ACK below target: group=${gid}, requested=${ackUpToSeq}, actual=${actualAckSeq}; fallback terminal ACK`);
|
|
2456
|
-
await this.confirmForwardGroupAck(gid, ackUpToSeq, groupAid);
|
|
2457
|
-
pendingAckSeq = 0;
|
|
2458
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackUpToSeq);
|
|
2459
2914
|
}
|
|
2460
2915
|
}
|
|
2461
2916
|
const resultGroupAid = String(result.group_aid ?? result.groupAid ?? '').trim();
|
|
@@ -2476,6 +2931,7 @@ export class V2E2EECoordinator {
|
|
|
2476
2931
|
.map((msg) => Number(msg.seq ?? 0))
|
|
2477
2932
|
.filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
2478
2933
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
2934
|
+
const namespaceGeneration = client._delivery.captureSeqNamespaceGeneration?.(ns);
|
|
2479
2935
|
let pageTrackerSnapshot = null;
|
|
2480
2936
|
let trackerMutated = false;
|
|
2481
2937
|
let pageMaxSeq = nextAfterSeq;
|
|
@@ -2483,33 +2939,76 @@ export class V2E2EECoordinator {
|
|
|
2483
2939
|
pageMaxSeq = Math.max(...seqs);
|
|
2484
2940
|
}
|
|
2485
2941
|
const pageContiguousSeq = contiguousPullSeq(pageContigBefore, seqs, result.contiguous_seq);
|
|
2942
|
+
if (!pipelineRunning && ownsCursor && shouldContinueForwardPage({
|
|
2943
|
+
rawCount: messages.length, nextAfterSeq, nextAfter: pageMaxSeq, limit,
|
|
2944
|
+
hasMoreField, hasMore, serverHead,
|
|
2945
|
+
observedUpper: Math.max(client._seqTracker.getMaxSeenSeq(ns), Number(client._delivery.pendingPullUpper?.(ns) ?? 0)),
|
|
2946
|
+
pendingAckSeq: 0,
|
|
2947
|
+
}))
|
|
2948
|
+
startPipeline(pageMaxSeq);
|
|
2486
2949
|
const deferredKeyFetches = new Map();
|
|
2487
2950
|
let blockedSeq = 0;
|
|
2488
2951
|
const pageDecryptedBefore = decrypted.length;
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
const
|
|
2499
|
-
|
|
2500
|
-
if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
|
|
2501
|
-
&& client._delivery.recallEventFromGroupMessage(msg)) {
|
|
2502
|
-
await client._delivery.publishGroupRecallTombstone(gid, seq, {
|
|
2503
|
-
...msg,
|
|
2504
|
-
group_id: gid,
|
|
2505
|
-
group_aid: eventGroupAid,
|
|
2506
|
-
}, 'pull');
|
|
2507
|
-
client._markPublishedSeq(ns, seq);
|
|
2508
|
-
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
2952
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
2953
|
+
const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
|
|
2954
|
+
const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
|
|
2955
|
+
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
|
|
2956
|
+
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
2957
|
+
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
2958
|
+
let commitTarget = pageContigBefore;
|
|
2959
|
+
await this.withApplicationPage(ns, 'pull', async () => {
|
|
2960
|
+
for (const msg of messages) {
|
|
2961
|
+
const seq = Number(msg.seq ?? 0);
|
|
2962
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
2509
2963
|
continue;
|
|
2510
|
-
|
|
2511
|
-
if (
|
|
2512
|
-
|
|
2964
|
+
const version = String(msg.version ?? 'v2');
|
|
2965
|
+
if (version === 'v1') {
|
|
2966
|
+
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
2967
|
+
const payload = msg.payload !== undefined ? msg.payload : legacy.payload;
|
|
2968
|
+
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
2969
|
+
const payloadType = payloadObj ? String(payloadObj.type ?? '').trim() : '';
|
|
2970
|
+
// 群撤回 tombstone(占位 / 通知):归一化为 group.message_recalled 事件,仍占 seq。
|
|
2971
|
+
if (typeof client._delivery?.recallEventFromGroupMessage === 'function'
|
|
2972
|
+
&& client._delivery.recallEventFromGroupMessage(msg)) {
|
|
2973
|
+
await client._delivery.publishGroupRecallTombstone(gid, seq, {
|
|
2974
|
+
...msg,
|
|
2975
|
+
group_id: gid,
|
|
2976
|
+
group_aid: eventGroupAid,
|
|
2977
|
+
}, 'pull');
|
|
2978
|
+
client._markPublishedSeq(ns, seq);
|
|
2979
|
+
client._clientLog.debug(`group.v2.pull recall tombstone delivered: group=${gid}, seq=${seq}`);
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
if (payloadObj) {
|
|
2983
|
+
if (!['e2ee.encrypted', 'e2ee.group_encrypted', 'e2ee.p2p_encrypted'].includes(payloadType)) {
|
|
2984
|
+
let v1Msg = {
|
|
2985
|
+
message_id: String(msg.message_id ?? ''),
|
|
2986
|
+
from: String(msg.from_aid ?? ''),
|
|
2987
|
+
group_id: gid,
|
|
2988
|
+
group_aid: eventGroupAid,
|
|
2989
|
+
seq: msg.seq,
|
|
2990
|
+
type: String(msg.type ?? ''),
|
|
2991
|
+
timestamp: msg.t_server,
|
|
2992
|
+
payload,
|
|
2993
|
+
encrypted: false,
|
|
2994
|
+
need_render: msg.need_render !== false,
|
|
2995
|
+
};
|
|
2996
|
+
if (msg.sender_device_id !== undefined)
|
|
2997
|
+
v1Msg.sender_device_id = msg.sender_device_id;
|
|
2998
|
+
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
2999
|
+
attachGatewayProximity(v1Msg, msg);
|
|
3000
|
+
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
3001
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
3002
|
+
}
|
|
3003
|
+
else {
|
|
3004
|
+
client._markPublishedSeq(ns, seq);
|
|
3005
|
+
}
|
|
3006
|
+
decrypted.push(v1Msg);
|
|
3007
|
+
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
else if (payload !== undefined && payload !== null) {
|
|
2513
3012
|
let v1Msg = {
|
|
2514
3013
|
message_id: String(msg.message_id ?? ''),
|
|
2515
3014
|
from: String(msg.from_aid ?? ''),
|
|
@@ -2520,114 +3019,81 @@ export class V2E2EECoordinator {
|
|
|
2520
3019
|
timestamp: msg.t_server,
|
|
2521
3020
|
payload,
|
|
2522
3021
|
encrypted: false,
|
|
3022
|
+
need_render: msg.need_render !== false,
|
|
2523
3023
|
};
|
|
2524
3024
|
if (msg.sender_device_id !== undefined)
|
|
2525
3025
|
v1Msg.sender_device_id = msg.sender_device_id;
|
|
2526
3026
|
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
2527
3027
|
attachGatewayProximity(v1Msg, msg);
|
|
2528
|
-
|
|
2529
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
2530
|
-
}
|
|
2531
|
-
else {
|
|
2532
|
-
client._markPublishedSeq(ns, seq);
|
|
2533
|
-
}
|
|
3028
|
+
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
2534
3029
|
decrypted.push(v1Msg);
|
|
2535
3030
|
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
2536
3031
|
continue;
|
|
2537
3032
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
timestamp: msg.t_server,
|
|
2548
|
-
payload,
|
|
2549
|
-
encrypted: false,
|
|
2550
|
-
};
|
|
2551
|
-
if (msg.sender_device_id !== undefined)
|
|
2552
|
-
v1Msg.sender_device_id = msg.sender_device_id;
|
|
2553
|
-
v1Msg = normalizeGroupMentionMode(v1Msg, msg, msg.envelope_json);
|
|
2554
|
-
attachGatewayProximity(v1Msg, msg);
|
|
2555
|
-
await client._publishPulledMessage('group.message_created', ns, seq, v1Msg, false);
|
|
2556
|
-
decrypted.push(v1Msg);
|
|
2557
|
-
client._clientLog.debug(`group.v2.pull plaintext V1 delivered: group=${gid}, seq=${seq}`);
|
|
2558
|
-
continue;
|
|
2559
|
-
}
|
|
2560
|
-
if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
|
|
2561
|
-
const deferStatus = {};
|
|
2562
|
-
const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
|
|
2563
|
-
if (deferStatus.deferred) {
|
|
2564
|
-
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
2565
|
-
if (deferStatus.fromAid) {
|
|
2566
|
-
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
2567
|
-
deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
|
|
3033
|
+
if (payloadObj && ['e2ee.group_encrypted', 'e2ee.p2p_encrypted', 'e2ee.encrypted'].includes(payloadType)) {
|
|
3034
|
+
const deferStatus = {};
|
|
3035
|
+
const plaintext = await client._decryptV2Message({ ...msg, envelope_json: JSON.stringify(payloadObj) }, true, true, true, true, deferStatus, true, 'pull');
|
|
3036
|
+
if (deferStatus.deferred) {
|
|
3037
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
3038
|
+
if (deferStatus.fromAid) {
|
|
3039
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
3040
|
+
deferredKeyFetches.set(key, { fromAid: deferStatus.fromAid, senderDeviceId: deferStatus.senderDeviceId ?? '', groupId: deferStatus.groupId ?? '' });
|
|
3041
|
+
}
|
|
2568
3042
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
3043
|
+
if (plaintext) {
|
|
3044
|
+
plaintext.group_id = gid;
|
|
3045
|
+
plaintext.group_aid = eventGroupAid;
|
|
3046
|
+
plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
|
|
3047
|
+
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
3048
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
3049
|
+
}
|
|
3050
|
+
else {
|
|
3051
|
+
client._markPublishedSeq(ns, seq);
|
|
3052
|
+
}
|
|
3053
|
+
decrypted.push(plaintext);
|
|
2575
3054
|
}
|
|
2576
|
-
else {
|
|
2577
|
-
client._markPublishedSeq(ns, seq);
|
|
2578
|
-
}
|
|
2579
|
-
decrypted.push(plaintext);
|
|
2580
3055
|
}
|
|
3056
|
+
else {
|
|
3057
|
+
client._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? '') : '<none>'} (V1 E2EE removed)`);
|
|
3058
|
+
}
|
|
3059
|
+
continue;
|
|
3060
|
+
}
|
|
3061
|
+
if (version !== 'v2') {
|
|
3062
|
+
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
const deferStatus = {};
|
|
3066
|
+
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
|
|
3067
|
+
if (deferStatus.deferred) {
|
|
3068
|
+
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
3069
|
+
}
|
|
3070
|
+
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
3071
|
+
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
3072
|
+
deferredKeyFetches.set(key, {
|
|
3073
|
+
fromAid: deferStatus.fromAid,
|
|
3074
|
+
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
3075
|
+
groupId: deferStatus.groupId ?? '',
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
if (plaintext === null) {
|
|
3079
|
+
client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
plaintext = normalizeGroupMentionMode(plaintext, msg, msg.envelope_json);
|
|
3083
|
+
plaintext.group_id = gid;
|
|
3084
|
+
plaintext.group_aid = eventGroupAid;
|
|
3085
|
+
plaintext.need_render = typeof msg.need_render === 'boolean' ? msg.need_render : plaintext.need_render !== false;
|
|
3086
|
+
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
3087
|
+
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
2581
3088
|
}
|
|
2582
3089
|
else {
|
|
2583
|
-
client.
|
|
3090
|
+
client._markPublishedSeq(ns, seq);
|
|
2584
3091
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
if (version !== 'v2') {
|
|
2588
|
-
client._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? '')}`);
|
|
2589
|
-
continue;
|
|
2590
|
-
}
|
|
2591
|
-
const deferStatus = {};
|
|
2592
|
-
let plaintext = await client._decryptV2Message(msg, true, true, true, true, deferStatus, true, 'pull');
|
|
2593
|
-
if (deferStatus.deferred) {
|
|
2594
|
-
blockedSeq = blockedSeq > 0 ? Math.min(blockedSeq, seq) : seq;
|
|
2595
|
-
}
|
|
2596
|
-
if (deferStatus.deferred && deferStatus.fromAid) {
|
|
2597
|
-
const key = `${deferStatus.fromAid}\u0000${deferStatus.senderDeviceId ?? ''}\u0000${deferStatus.groupId ?? ''}`;
|
|
2598
|
-
deferredKeyFetches.set(key, {
|
|
2599
|
-
fromAid: deferStatus.fromAid,
|
|
2600
|
-
senderDeviceId: deferStatus.senderDeviceId ?? '',
|
|
2601
|
-
groupId: deferStatus.groupId ?? '',
|
|
2602
|
-
});
|
|
2603
|
-
}
|
|
2604
|
-
if (plaintext === null) {
|
|
2605
|
-
client._clientLog.debug(`group.v2.pull decrypt returned null: group=${gid}, seq=${seq}`);
|
|
2606
|
-
continue;
|
|
3092
|
+
decrypted.push(plaintext);
|
|
3093
|
+
client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
2607
3094
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
plaintext.group_aid = eventGroupAid;
|
|
2611
|
-
if (!client._delivery.isSelfSentGroupMessage?.(msg)) {
|
|
2612
|
-
await client._publishPulledMessage('group.message_created', ns, seq, plaintext, false);
|
|
2613
|
-
}
|
|
2614
|
-
else {
|
|
2615
|
-
client._markPublishedSeq(ns, seq);
|
|
2616
|
-
}
|
|
2617
|
-
decrypted.push(plaintext);
|
|
2618
|
-
client._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
2619
|
-
}
|
|
2620
|
-
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
2621
|
-
const parsedCursorCurrentSeq = hasServerCursor ? safeNonNegativeSeq(cursor?.current_seq) : null;
|
|
2622
|
-
const cursorCurrentSeq = parsedCursorCurrentSeq ?? 0;
|
|
2623
|
-
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'));
|
|
2624
|
-
const visibilityFloor = Math.max(0, Math.max(0, Number(result.earliest_available_message_seq ?? 1) - 1), Number(cursor?.join_seq ?? 0));
|
|
2625
|
-
const effectiveFloor = Math.max(retentionFloor, visibilityFloor);
|
|
2626
|
-
const commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
|
|
2627
|
-
let ackSeq = pageContigBefore;
|
|
2628
|
-
let contigAdvanced = false;
|
|
2629
|
-
if (ownsCursor) {
|
|
2630
|
-
try {
|
|
3095
|
+
commitTarget = Math.max(pageContigBefore, effectiveFloor, ownsCursor && pageCount === 1 ? deferredServerCursor : 0, ownsCursor ? cursorCurrentSeq : 0, blockedSeq > 0 ? blockedSeq - 1 : pageContiguousSeq);
|
|
3096
|
+
if (ownsCursor) {
|
|
2631
3097
|
this.ensureDeliveryCurrent(ns, generation);
|
|
2632
3098
|
const window = client._seqTracker.getSyncState?.(ns);
|
|
2633
3099
|
const deliveryTarget = window && commitTarget >= window.tail - 1
|
|
@@ -2636,6 +3102,16 @@ export class V2E2EECoordinator {
|
|
|
2636
3102
|
await client._drainOrderedMessages(ns, undefined, false, false, 'pull', deliveryTarget);
|
|
2637
3103
|
}
|
|
2638
3104
|
this.ensureDeliveryCurrent(ns, generation);
|
|
3105
|
+
}
|
|
3106
|
+
});
|
|
3107
|
+
let ackSeq = pageContigBefore;
|
|
3108
|
+
let contigAdvanced = false;
|
|
3109
|
+
if (ownsCursor) {
|
|
3110
|
+
try {
|
|
3111
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
3112
|
+
if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
|
|
3113
|
+
throw new Error('pull invalidated');
|
|
3114
|
+
client._delivery.rememberCommittedSeqState?.(ns);
|
|
2639
3115
|
pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
|
|
2640
3116
|
if (commitTarget > pageContigBefore) {
|
|
2641
3117
|
trackerMutated = true;
|
|
@@ -2655,12 +3131,16 @@ export class V2E2EECoordinator {
|
|
|
2655
3131
|
if (contigAdvanced) {
|
|
2656
3132
|
await client._commitSeqTrackerState(ns);
|
|
2657
3133
|
this.ensureDeliveryCurrent(ns, generation);
|
|
3134
|
+
if (client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) === false)
|
|
3135
|
+
throw new Error('pull invalidated');
|
|
2658
3136
|
if (pageCount === 1 && deferredServerCursor > 0)
|
|
2659
3137
|
this.clearForwardCursor(ns, ackSeq);
|
|
2660
3138
|
}
|
|
3139
|
+
committedAckSeq = ackSeq;
|
|
2661
3140
|
}
|
|
2662
3141
|
catch (exc) {
|
|
2663
3142
|
if (trackerMutated && Number(client._v2RuntimeGeneration ?? 0) === generation
|
|
3143
|
+
&& client._delivery.isSeqNamespaceGenerationCurrent?.(ns, namespaceGeneration) !== false
|
|
2664
3144
|
&& client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false
|
|
2665
3145
|
&& typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
2666
3146
|
(client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
|
|
@@ -2688,18 +3168,8 @@ export class V2E2EECoordinator {
|
|
|
2688
3168
|
|| (contigAdvanced && ackSeq > knownServerCursorSeq));
|
|
2689
3169
|
if (ackNeeded) {
|
|
2690
3170
|
this.recordForwardAck(ns, ackSeq);
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
if (canContinuePage && !pipelineRunning) {
|
|
2694
|
-
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
2695
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
2696
|
-
client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
|
|
2697
|
-
}
|
|
2698
|
-
else {
|
|
2699
|
-
client._clientLog.debug(`group.v2.pull sending terminal auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
2700
|
-
await this.confirmForwardGroupAck(gid, ackSeq, groupAid);
|
|
2701
|
-
lastAutoAckSeq = Math.max(lastAutoAckSeq, ackSeq);
|
|
2702
|
-
}
|
|
3171
|
+
pendingAckSeq = Math.max(pendingAckSeq, ackSeq);
|
|
3172
|
+
client._clientLog.debug(`group.v2.pull queued piggyback auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}, raw_count=${messages.length}`);
|
|
2703
3173
|
}
|
|
2704
3174
|
client._delivery.syncProgress(ns, {
|
|
2705
3175
|
rawCount: messages.length,
|
|
@@ -2709,7 +3179,7 @@ export class V2E2EECoordinator {
|
|
|
2709
3179
|
currentSeq: client._seqTracker.getContiguousSeq(ns),
|
|
2710
3180
|
targetSeq: serverHead,
|
|
2711
3181
|
});
|
|
2712
|
-
const nextAfter = Math.max(
|
|
3182
|
+
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
2713
3183
|
if (!ownsCursor)
|
|
2714
3184
|
break;
|
|
2715
3185
|
const rawCount = messages.length;
|
|
@@ -2740,28 +3210,12 @@ export class V2E2EECoordinator {
|
|
|
2740
3210
|
if (!shouldContinue)
|
|
2741
3211
|
break;
|
|
2742
3212
|
if (shouldContinueForPipeline && !pipelineRunning && limit >= V2_PULL_MAX_PAGE_LIMIT) {
|
|
2743
|
-
if (pendingAckSeq > 0) {
|
|
2744
|
-
await this.confirmForwardGroupAck(gid, pendingAckSeq, groupAid);
|
|
2745
|
-
pendingAckSeq = 0;
|
|
2746
|
-
}
|
|
2747
3213
|
startPipeline(nextAfter);
|
|
2748
3214
|
}
|
|
2749
|
-
|
|
2750
|
-
const nextKey = pullGateKeyForClient(client, 'group.v2.pull', {
|
|
2751
|
-
group_id: gid, after_seq: nextAfter, force: opts?.force === true, limit,
|
|
2752
|
-
_group_cursor_params: cursorParams,
|
|
2753
|
-
}, `${ns}|forward|after=${nextAfter}|force=${String(Boolean(opts?.force))}|limit=${limit}`);
|
|
2754
|
-
await client._rpcPipeline.yieldPullGate(pullGateKey, nextKey, true);
|
|
2755
|
-
pullGateKey = nextKey;
|
|
2756
|
-
}
|
|
2757
|
-
const tailDelayMs = v2PullTailDelayMs(rawCount, limit);
|
|
3215
|
+
const tailDelayMs = pipelineRunning ? 0 : v2PullTailDelayMs(rawCount, limit);
|
|
2758
3216
|
if (tailDelayMs > 0)
|
|
2759
3217
|
await v2Sleep(tailDelayMs);
|
|
2760
3218
|
nextAfterSeq = nextAfter;
|
|
2761
|
-
if (pipelinePage) {
|
|
2762
|
-
pipelineResume.current?.();
|
|
2763
|
-
pipelineResume.current = null;
|
|
2764
|
-
}
|
|
2765
3219
|
}
|
|
2766
3220
|
}
|
|
2767
3221
|
finally {
|
|
@@ -2769,11 +3223,14 @@ export class V2E2EECoordinator {
|
|
|
2769
3223
|
}
|
|
2770
3224
|
if (pageCount >= maxPages) {
|
|
2771
3225
|
client._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3226
|
+
}
|
|
3227
|
+
if (pendingAckSeq > 0) {
|
|
3228
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
3229
|
+
client._clientLog.debug(`group.v2.pull flushing pending auto-ack: group=${gid}, ns=${ns}, ack_seq=${pendingAckSeq}`);
|
|
3230
|
+
const ackToFlush = pendingAckSeq;
|
|
3231
|
+
await this.confirmForwardGroupAck(gid, ackToFlush, groupAid, background);
|
|
3232
|
+
if (pendingAckSeq <= ackToFlush)
|
|
2775
3233
|
pendingAckSeq = 0;
|
|
2776
|
-
}
|
|
2777
3234
|
}
|
|
2778
3235
|
client._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
|
|
2779
3236
|
return decrypted;
|
|
@@ -2792,10 +3249,12 @@ export class V2E2EECoordinator {
|
|
|
2792
3249
|
throw error;
|
|
2793
3250
|
}
|
|
2794
3251
|
}
|
|
2795
|
-
async confirmForwardGroupAck(groupId, upToSeq, groupAid) {
|
|
3252
|
+
async confirmForwardGroupAck(groupId, upToSeq, groupAid, background = false) {
|
|
2796
3253
|
const ns = `group:${String(groupId ?? '').trim()}`;
|
|
3254
|
+
const generation = Number(this.client._v2RuntimeGeneration ?? 0);
|
|
2797
3255
|
try {
|
|
2798
|
-
const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid);
|
|
3256
|
+
const result = await this.ackGroupV2ForForward(groupId, upToSeq, groupAid, background);
|
|
3257
|
+
this.ensureDeliveryCurrent(ns, generation);
|
|
2799
3258
|
const actualAckSeq = this.client._delivery.resolveForwardAckSeq(result, upToSeq);
|
|
2800
3259
|
if (actualAckSeq < upToSeq) {
|
|
2801
3260
|
throw new E2EEError(`group.v2.ack server ACK watermark ${actualAckSeq} is below requested ${upToSeq}`);
|
|
@@ -2803,17 +3262,19 @@ export class V2E2EECoordinator {
|
|
|
2803
3262
|
this.clearForwardAck(ns, upToSeq);
|
|
2804
3263
|
}
|
|
2805
3264
|
catch (exc) {
|
|
2806
|
-
this.
|
|
3265
|
+
if (Number(this.client._v2RuntimeGeneration ?? 0) === generation
|
|
3266
|
+
&& this.client._rpcPipeline?.isPullNamespaceCurrent?.(ns) !== false)
|
|
3267
|
+
this.recordForwardAck(ns, upToSeq);
|
|
2807
3268
|
throw exc;
|
|
2808
3269
|
}
|
|
2809
3270
|
}
|
|
2810
3271
|
async ackGroupV2(groupId, upToSeq, groupAid) {
|
|
2811
3272
|
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, false);
|
|
2812
3273
|
}
|
|
2813
|
-
async ackGroupV2ForForward(groupId, upToSeq, groupAid) {
|
|
2814
|
-
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true);
|
|
3274
|
+
async ackGroupV2ForForward(groupId, upToSeq, groupAid, background = false) {
|
|
3275
|
+
return this.ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, true, background);
|
|
2815
3276
|
}
|
|
2816
|
-
async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark) {
|
|
3277
|
+
async ackGroupV2WithConfirmation(groupId, upToSeq, groupAid, requireServerWatermark, background) {
|
|
2817
3278
|
const client = this.client;
|
|
2818
3279
|
const gid = String(groupId ?? '').trim();
|
|
2819
3280
|
if (!gid)
|
|
@@ -2831,7 +3292,9 @@ export class V2E2EECoordinator {
|
|
|
2831
3292
|
return { acked: 0 };
|
|
2832
3293
|
const inlineAckGeneration = client._delivery.captureGroupInlineAckGeneration();
|
|
2833
3294
|
client._clientLog.debug(`group.v2.ack send: group=${gid}, ns=${ns}, up_to_seq=${seq}`);
|
|
2834
|
-
const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq
|
|
3295
|
+
const result = await client._callRawV2Rpc('group.v2.ack', withExplicitGroupAid({ group_id: gid, up_to_seq: seq,
|
|
3296
|
+
...(background === undefined ? {} : { _rpc_background: background, _rpc_foreground: !background }),
|
|
3297
|
+
}, explicitGroupAid));
|
|
2835
3298
|
const actualAckSeq = requireServerWatermark
|
|
2836
3299
|
? client._delivery.resolveForwardAckSeq(result, seq)
|
|
2837
3300
|
: client._delivery.resolveActualAckSeq(result, seq);
|