@agentunion/fastaun-browser 0.5.4 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +177 -67
- package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/216/245/345/205/245/344/270/216/346/274/224/347/244/272/346/214/207/345/215/227.md +290 -0
- package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/346/236/266/346/236/204/344/270/216/350/257/246/347/273/206/345/256/236/347/216/260/350/256/241/345/210/222-codex.md +994 -0
- package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/350/277/220/347/273/264/344/270/216/345/217/221/345/270/203/346/214/207/345/215/227.md +144 -0
- package/_packed_docs/CHANGELOG.md +177 -67
- package/_packed_docs/INDEX.md +124 -65
- package/_packed_docs/KITE_DOCS_GUIDE.md +65 -30
- package/_packed_docs/agent.md/SCHEMA.md +83 -58
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
- package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +366 -0
- package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +234 -0
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1095 -0
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1199 -0
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
- package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
- package/_packed_docs/message-online-push-alignment.md +572 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
- package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
- package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
- package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +165 -421
- package/_packed_docs/protocol/README.md +1 -0
- package/_packed_docs/protocol/aun-docs-guide.md +7 -4
- package/_packed_docs/protocol/index.md +25 -19
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +63 -18
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +93 -84
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +73 -34
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +21 -9
- package/_packed_docs/sdk/09-group-rpc-manual.md +319 -285
- package/_packed_docs/sdk/09-message-rpc-manual.md +144 -103
- package/_packed_docs/sdk/09-payload-reference.md +1 -1
- package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
- package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +34 -23
- package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
- package/_packed_docs/sdk/INDEX.md +45 -37
- package/_packed_docs/sdk/README.md +11 -9
- package/_packed_docs//345/217/221/345/270/203/346/212/245/345/221/212-0.5.6.md +260 -0
- package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/344/273/243/347/240/201/345/256/241/346/237/245/351/227/256/351/242/230/346/270/205/345/215/225.md +229 -0
- package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/346/224/271/351/200/240/346/226/271/346/241/210.md +750 -0
- package/dist/agent-md-schema.d.ts +14 -0
- package/dist/agent-md-schema.d.ts.map +1 -0
- package/dist/agent-md-schema.js +103 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +11 -5
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +295 -53
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +7 -7
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +46 -18
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +29 -7
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +31 -11
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +29920 -19719
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +36 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +106 -11
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1925 -374
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts.map +1 -1
- package/dist/client/group-state.js +27 -24
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.d.ts +15 -1
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +477 -132
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/mention-mode.d.ts +7 -0
- package/dist/client/mention-mode.d.ts.map +1 -0
- package/dist/client/mention-mode.js +184 -0
- package/dist/client/mention-mode.js.map +1 -0
- package/dist/client/peers.d.ts +1 -1
- package/dist/client/peers.d.ts.map +1 -1
- package/dist/client/peers.js +26 -3
- package/dist/client/peers.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +34 -2
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +503 -99
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.d.ts.map +1 -1
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +43 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +931 -138
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +46 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +785 -304
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +26 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +142 -60
- package/dist/facades.js.map +1 -1
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +2 -17
- package/dist/group-id.js.map +1 -1
- package/dist/group-index.d.ts +6 -1
- package/dist/group-index.d.ts.map +1 -1
- package/dist/group-index.js +44 -25
- package/dist/group-index.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +10 -5
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +0 -12
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +0 -60
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +9 -5
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +3 -1
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +47 -2
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/register-flow.d.ts.map +1 -1
- package/dist/register-flow.js +28 -3
- package/dist/register-flow.js.map +1 -1
- package/dist/seq-tracker.d.ts +28 -8
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +224 -61
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.d.ts.map +1 -1
- package/dist/storage/vfs.js +26 -2
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +399 -11
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +12 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +226 -102
- package/dist/transport.js.map +1 -1
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +3 -4
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts.map +1 -1
- package/dist/v2/state/commitment.js +1 -2
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +8 -14
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -9
- package/dist/group-resources.d.ts +0 -98
- package/dist/group-resources.d.ts.map +0 -1
- package/dist/group-resources.js +0 -635
- package/dist/group-resources.js.map +0 -1
- package/dist/keystore/indexeddb.d.ts +0 -179
- package/dist/keystore/indexeddb.d.ts.map +0 -1
- package/dist/keystore/indexeddb.js +0 -2031
- package/dist/keystore/indexeddb.js.map +0 -1
- package/dist/namespaces/auth.d.ts +0 -98
- package/dist/namespaces/auth.d.ts.map +0 -1
- package/dist/namespaces/auth.js +0 -992
- package/dist/namespaces/auth.js.map +0 -1
- package/dist/namespaces/custody.d.ts +0 -51
- package/dist/namespaces/custody.d.ts.map +0 -1
- package/dist/namespaces/custody.js +0 -302
- package/dist/namespaces/custody.js.map +0 -1
- package/dist/namespaces/meta.d.ts +0 -109
- package/dist/namespaces/meta.d.ts.map +0 -1
- package/dist/namespaces/meta.js +0 -549
- package/dist/namespaces/meta.js.map +0 -1
package/dist/client/delivery.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { slotIsolationKey } from '../config.js';
|
|
2
|
-
import { normalizeGroupId } from '../group-id.js';
|
|
3
2
|
import { ConnectionState, isJsonObject } from '../types.js';
|
|
3
|
+
import { hasGroupMentionModeField, normalizeGroupMentionMode } from './mention-mode.js';
|
|
4
4
|
const PUSHED_SEQS_LIMIT = 50_000;
|
|
5
5
|
const PENDING_ORDERED_LIMIT = 50_000;
|
|
6
6
|
const MESSAGE_RECALL_SEEN_LIMIT = 10_000;
|
|
@@ -13,7 +13,7 @@ const APP_MESSAGE_ENVELOPE_KEYS = [
|
|
|
13
13
|
'module_id', 'message_type', 'type', 'kind', 'version',
|
|
14
14
|
'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
|
|
15
15
|
'timestamp', 'created_at', 'encrypted',
|
|
16
|
-
'context', 'protected_headers', 'headers', 'payload_type',
|
|
16
|
+
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
|
|
17
17
|
];
|
|
18
18
|
const RECALL_PAYLOAD_KEYS = [
|
|
19
19
|
'message_ids', 'target_message_seqs',
|
|
@@ -33,6 +33,7 @@ const APP_GROUP_EVENT_ENVELOPE_KEYS = [
|
|
|
33
33
|
'actor_aid', 'sender_aid', 'member_aid', 'target_aid', 'operator_aid',
|
|
34
34
|
'created_at', 'timestamp', 't_server', 'status', 'device_id', 'slot_id',
|
|
35
35
|
];
|
|
36
|
+
const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
|
|
36
37
|
function formatDeliveryError(error) {
|
|
37
38
|
return error instanceof Error ? error : String(error);
|
|
38
39
|
}
|
|
@@ -47,6 +48,34 @@ function isTransientGapFillError(error) {
|
|
|
47
48
|
|| text.includes('connection closed')
|
|
48
49
|
|| text.includes('socket closed');
|
|
49
50
|
}
|
|
51
|
+
function positiveSafeSequenceHint(value) {
|
|
52
|
+
if (typeof value === 'number') {
|
|
53
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value !== 'string' || !value || value.trim() !== value)
|
|
56
|
+
return 0;
|
|
57
|
+
const parsed = Number(value);
|
|
58
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
59
|
+
}
|
|
60
|
+
function nonNegativeSafeSequenceHint(value) {
|
|
61
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
62
|
+
return null;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function deduplicatePlainForwardPageMessages(value, afterSeq) {
|
|
66
|
+
if (!Array.isArray(value))
|
|
67
|
+
return [];
|
|
68
|
+
const bySeq = new Map();
|
|
69
|
+
for (const raw of value) {
|
|
70
|
+
if (!isJsonObject(raw))
|
|
71
|
+
continue;
|
|
72
|
+
const seq = positiveSafeSequenceHint(raw.seq);
|
|
73
|
+
if (seq <= afterSeq || bySeq.has(seq))
|
|
74
|
+
continue;
|
|
75
|
+
bySeq.set(seq, { ...raw, seq });
|
|
76
|
+
}
|
|
77
|
+
return [...bySeq.entries()].sort(([left], [right]) => left - right).map(([, message]) => message);
|
|
78
|
+
}
|
|
50
79
|
function p2pAppEventFromPlainPullMessage(message) {
|
|
51
80
|
const payload = message.payload;
|
|
52
81
|
const payloadType = isJsonObject(payload)
|
|
@@ -65,9 +94,146 @@ function p2pAppEventFromPlainPullMessage(message) {
|
|
|
65
94
|
}
|
|
66
95
|
export class MessageDeliveryEngine {
|
|
67
96
|
runtime;
|
|
97
|
+
realtimeTailResults = null;
|
|
98
|
+
realtimeSyncing = null;
|
|
99
|
+
pendingP2pPullUpper = null;
|
|
100
|
+
pendingGroupPullUpper = null;
|
|
101
|
+
realtimeAcking = null;
|
|
102
|
+
pendingP2PInlineAcks = null;
|
|
103
|
+
pendingGroupInlineAcks = null;
|
|
104
|
+
pendingGroupInlineAckNamespaces = null;
|
|
105
|
+
// reset 后旧 inline Promise 只能自行结束,不能触碰新身份上下文的状态。
|
|
106
|
+
inlineGeneration = 0;
|
|
107
|
+
// 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
|
|
108
|
+
seqTrackerWriteChain = Promise.resolve();
|
|
68
109
|
constructor(runtime) {
|
|
69
110
|
this.runtime = runtime;
|
|
70
111
|
}
|
|
112
|
+
resetInlineAckState() {
|
|
113
|
+
this.inlineGeneration += 1;
|
|
114
|
+
this.realtimeSyncing = null;
|
|
115
|
+
this.pendingP2pPullUpper = null;
|
|
116
|
+
this.pendingGroupPullUpper = null;
|
|
117
|
+
this.realtimeTailResults = null;
|
|
118
|
+
this.realtimeAcking = null;
|
|
119
|
+
this.pendingP2PInlineAcks = null;
|
|
120
|
+
this.pendingGroupInlineAcks = null;
|
|
121
|
+
this.pendingGroupInlineAckNamespaces = null;
|
|
122
|
+
}
|
|
123
|
+
isInlineGenerationCurrent(generation) {
|
|
124
|
+
return generation === this.inlineGeneration;
|
|
125
|
+
}
|
|
126
|
+
captureInlineGeneration() {
|
|
127
|
+
return this.inlineGeneration;
|
|
128
|
+
}
|
|
129
|
+
beginRealtimeSync(ns) {
|
|
130
|
+
if (!ns || this.realtimeSyncing?.has(ns))
|
|
131
|
+
return false;
|
|
132
|
+
const syncing = this.realtimeSyncing ?? new Set();
|
|
133
|
+
this.realtimeSyncing = syncing;
|
|
134
|
+
syncing.add(ns);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
endRealtimeSync(ns) {
|
|
138
|
+
this.realtimeSyncing?.delete(ns);
|
|
139
|
+
}
|
|
140
|
+
hasPendingPull(ns) {
|
|
141
|
+
const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
|
|
142
|
+
return (pending?.get(ns) ?? 0) > 0;
|
|
143
|
+
}
|
|
144
|
+
onPullGateIdle(ns) {
|
|
145
|
+
if (!ns)
|
|
146
|
+
return;
|
|
147
|
+
this.schedulePendingPullIfNeeded(ns, 'pull-gate-idle');
|
|
148
|
+
}
|
|
149
|
+
recordPendingPull(ns, seq) {
|
|
150
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
151
|
+
return;
|
|
152
|
+
const pending = ns.startsWith('p2p:')
|
|
153
|
+
? (this.pendingP2pPullUpper ?? new Map())
|
|
154
|
+
: (this.pendingGroupPullUpper ?? new Map());
|
|
155
|
+
if (ns.startsWith('p2p:'))
|
|
156
|
+
this.pendingP2pPullUpper = pending;
|
|
157
|
+
else
|
|
158
|
+
this.pendingGroupPullUpper = pending;
|
|
159
|
+
pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
|
|
160
|
+
}
|
|
161
|
+
consumePendingPull(ns, throughSeq) {
|
|
162
|
+
const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
|
|
163
|
+
if ((pending?.get(ns) ?? 0) <= throughSeq)
|
|
164
|
+
pending?.delete(ns);
|
|
165
|
+
}
|
|
166
|
+
schedulePendingPullIfNeeded(ns, reason) {
|
|
167
|
+
const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
|
|
168
|
+
if (!pending)
|
|
169
|
+
return false;
|
|
170
|
+
const upper = pending.get(ns) ?? 0;
|
|
171
|
+
if (upper <= 0) {
|
|
172
|
+
pending.delete(ns);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const client = this.runtime.client;
|
|
176
|
+
const contiguous = client._seqTracker.getContiguousSeq(ns);
|
|
177
|
+
if (upper <= contiguous) {
|
|
178
|
+
pending.delete(ns);
|
|
179
|
+
client._clientLog?.debug(`pending pull upper already covered: ns=${ns}, upper_seq=${upper}, contiguous=${contiguous}, reason=${reason}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (client.state !== ConnectionState.READY || client._closing || this.realtimeSyncing?.has(ns)
|
|
183
|
+
|| client._rpcPipeline?.hasPullActivity?.(ns, false))
|
|
184
|
+
return false;
|
|
185
|
+
pending.delete(ns);
|
|
186
|
+
client._clientLog?.debug(`pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upper}, reason=${reason}`);
|
|
187
|
+
if (ns.startsWith('p2p:')) {
|
|
188
|
+
client._safeAsync(this.onV2PushNotification({ seq: upper }));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
client._safeAsync(this.onRawGroupV2MessageCreated({ group_id: ns.slice('group:'.length), seq: upper }));
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
beginRealtimeAck(ns, seq) {
|
|
196
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
197
|
+
return false;
|
|
198
|
+
const current = this.realtimeAcking?.get(ns);
|
|
199
|
+
if (current !== undefined) {
|
|
200
|
+
if (seq > current)
|
|
201
|
+
this.realtimeAcking?.set(ns, seq);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const acking = this.realtimeAcking ?? new Map();
|
|
205
|
+
this.realtimeAcking = acking;
|
|
206
|
+
acking.set(ns, seq);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
finishRealtimeAck(ns, completedSeq) {
|
|
210
|
+
const latest = this.realtimeAcking?.get(ns) ?? 0;
|
|
211
|
+
this.realtimeAcking?.delete(ns);
|
|
212
|
+
return latest > completedSeq ? latest : 0;
|
|
213
|
+
}
|
|
214
|
+
pendingInlineAckNamespaceCount() {
|
|
215
|
+
return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
|
|
216
|
+
}
|
|
217
|
+
inlinePostAckRow(row) {
|
|
218
|
+
const result = {};
|
|
219
|
+
for (const key of ['envelope_json', 'spk_id', 'group_id', 'message_id']) {
|
|
220
|
+
const value = row[key];
|
|
221
|
+
if (typeof value === 'string' && value)
|
|
222
|
+
result[key] = value;
|
|
223
|
+
}
|
|
224
|
+
if (typeof row.seq === 'number' && Number.isSafeInteger(row.seq) && row.seq > 0)
|
|
225
|
+
result.seq = row.seq;
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
markRealtimeTailCompleted(result) {
|
|
229
|
+
const completed = this.realtimeTailResults ?? new WeakSet();
|
|
230
|
+
this.realtimeTailResults = completed;
|
|
231
|
+
completed.add(result);
|
|
232
|
+
Object.defineProperty(result, '_realtime_tail_completed', { value: true, enumerable: false });
|
|
233
|
+
}
|
|
234
|
+
isRealtimeTailCompleted(result) {
|
|
235
|
+
return this.realtimeTailResults?.has(result) ?? false;
|
|
236
|
+
}
|
|
71
237
|
prunePushedSeqs(ns) {
|
|
72
238
|
const client = this.runtime.client;
|
|
73
239
|
const pushed = client._pushedSeqs.get(ns);
|
|
@@ -106,18 +272,510 @@ export class MessageDeliveryEngine {
|
|
|
106
272
|
client._clientLog.warn(`${label} failed: group=${groupId}`, e);
|
|
107
273
|
});
|
|
108
274
|
}
|
|
109
|
-
fireGroupV2Ack(groupId, ns, seq, label) {
|
|
275
|
+
fireGroupV2Ack(groupId, ns, seq, label, generation = null) {
|
|
110
276
|
const client = this.runtime.client;
|
|
111
277
|
const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
|
|
112
|
-
if (ackSeq <= 0)
|
|
278
|
+
if (ackSeq <= 0 || !this.beginRealtimeAck(ns, ackSeq))
|
|
113
279
|
return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
280
|
+
let task;
|
|
281
|
+
try {
|
|
282
|
+
task = Promise.resolve(client._callRawV2Rpc('group.v2.ack', {
|
|
283
|
+
group_id: groupId,
|
|
284
|
+
up_to_seq: ackSeq,
|
|
285
|
+
_rpc_background: true,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
catch (exc) {
|
|
289
|
+
this.finishRealtimeAck(ns, ackSeq);
|
|
290
|
+
client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${formatDeliveryError(exc)}`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
void task.then((result) => {
|
|
294
|
+
if (generation !== null && this.isInlineGenerationCurrent(generation)) {
|
|
295
|
+
this.completePendingGroupInlineAckUpTo(ns, this.resolveActualAckSeq(result, ackSeq), generation);
|
|
296
|
+
}
|
|
118
297
|
}).catch((e) => {
|
|
119
298
|
client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${String(e)}`);
|
|
299
|
+
}).finally(() => {
|
|
300
|
+
if (generation !== null && !this.isInlineGenerationCurrent(generation))
|
|
301
|
+
return;
|
|
302
|
+
const nextAckSeq = this.finishRealtimeAck(ns, ackSeq);
|
|
303
|
+
if (nextAckSeq > 0)
|
|
304
|
+
this.fireGroupV2Ack(groupId, ns, nextAckSeq, label, generation);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
fireP2PV2Ack(seq, label, generation = null) {
|
|
308
|
+
const client = this.runtime.client;
|
|
309
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
310
|
+
if (!Number.isSafeInteger(seq) || seq <= 0 || typeof client._ackV2 !== 'function'
|
|
311
|
+
|| !this.beginRealtimeAck(ns, seq))
|
|
312
|
+
return;
|
|
313
|
+
try {
|
|
314
|
+
const ackTask = generation === null ? client._ackV2(seq) : client._ackV2(seq, generation);
|
|
315
|
+
void Promise.resolve(ackTask).catch((exc) => {
|
|
316
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
317
|
+
}).finally(() => {
|
|
318
|
+
if (generation !== null && !this.isInlineGenerationCurrent(generation))
|
|
319
|
+
return;
|
|
320
|
+
const nextAckSeq = this.finishRealtimeAck(ns, seq);
|
|
321
|
+
if (nextAckSeq > 0)
|
|
322
|
+
this.fireP2PV2Ack(nextAckSeq, label, generation);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (exc) {
|
|
326
|
+
this.finishRealtimeAck(ns, seq);
|
|
327
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
retryPendingP2PInlineAck(ns, seq, label, ackThrough = seq) {
|
|
331
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
332
|
+
if (!pending || pending.seq !== seq)
|
|
333
|
+
return false;
|
|
334
|
+
if (pending.acking || !pending.committed)
|
|
335
|
+
return true;
|
|
336
|
+
if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
|
|
337
|
+
ackThrough = seq;
|
|
338
|
+
const client = this.runtime.client;
|
|
339
|
+
if (typeof client._ackV2 !== 'function')
|
|
340
|
+
return true;
|
|
341
|
+
pending.acking = true;
|
|
342
|
+
const generation = pending.generation;
|
|
343
|
+
let task;
|
|
344
|
+
try {
|
|
345
|
+
task = Promise.resolve(client._ackV2(ackThrough, generation));
|
|
346
|
+
}
|
|
347
|
+
catch (exc) {
|
|
348
|
+
pending.acking = false;
|
|
349
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
void task.catch((exc) => {
|
|
353
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
354
|
+
}).finally(() => {
|
|
355
|
+
const current = this.pendingP2PInlineAcks?.get(ns);
|
|
356
|
+
if (this.isInlineGenerationCurrent(generation) && current === pending)
|
|
357
|
+
current.acking = false;
|
|
358
|
+
});
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
retryPendingGroupInlineAck(ns, seq, label, ackThrough = seq) {
|
|
362
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
363
|
+
if (!pending || pending.seq !== seq)
|
|
364
|
+
return false;
|
|
365
|
+
if (pending.acking || !pending.committed)
|
|
366
|
+
return true;
|
|
367
|
+
if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
|
|
368
|
+
ackThrough = seq;
|
|
369
|
+
const client = this.runtime.client;
|
|
370
|
+
if (typeof client._ackGroupV2 !== 'function')
|
|
371
|
+
return true;
|
|
372
|
+
pending.acking = true;
|
|
373
|
+
const generation = pending.generation;
|
|
374
|
+
let task;
|
|
375
|
+
try {
|
|
376
|
+
task = Promise.resolve(client._ackGroupV2(pending.groupId, ackThrough, pending.groupAid || undefined, generation));
|
|
377
|
+
}
|
|
378
|
+
catch (exc) {
|
|
379
|
+
pending.acking = false;
|
|
380
|
+
client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
void task.catch((exc) => {
|
|
384
|
+
client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
385
|
+
}).finally(() => {
|
|
386
|
+
const current = this.pendingGroupInlineAcks?.get(ns);
|
|
387
|
+
if (this.isInlineGenerationCurrent(generation) && current === pending)
|
|
388
|
+
current.acking = false;
|
|
389
|
+
});
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
reservePendingP2PInlineAck(ns, seq, row, generation = this.inlineGeneration) {
|
|
393
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
394
|
+
|| !ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingP2PInlineAcks?.has(ns)
|
|
395
|
+
|| this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
|
|
396
|
+
return false;
|
|
397
|
+
const pendingMap = this.pendingP2PInlineAcks ?? new Map();
|
|
398
|
+
this.pendingP2PInlineAcks = pendingMap;
|
|
399
|
+
pendingMap.set(ns, {
|
|
400
|
+
seq,
|
|
401
|
+
row: this.inlinePostAckRow(row),
|
|
402
|
+
generation,
|
|
403
|
+
committed: false,
|
|
404
|
+
observedAckSeq: 0,
|
|
405
|
+
acking: false,
|
|
406
|
+
});
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
dropPendingP2PInlineAck(ns, seq, generation = this.inlineGeneration) {
|
|
410
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
411
|
+
if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
|
|
412
|
+
this.pendingP2PInlineAcks?.delete(ns);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
reservePendingGroupInlineAck(ns, seq, row, groupAid, groupId, generation = this.inlineGeneration) {
|
|
416
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
417
|
+
|| !ns || !groupId || !Number.isSafeInteger(seq) || seq <= 0
|
|
418
|
+
|| this.pendingGroupInlineAcks?.has(ns)
|
|
419
|
+
|| this.pendingGroupInlineAckNamespaces?.has(ns)
|
|
420
|
+
|| this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
|
|
421
|
+
return false;
|
|
422
|
+
const normalizedGroupAid = typeof groupAid === 'string' ? groupAid.trim() : '';
|
|
423
|
+
const aliasNs = normalizedGroupAid ? `group:${normalizedGroupAid}` : '';
|
|
424
|
+
if (aliasNs && aliasNs !== ns
|
|
425
|
+
&& (this.pendingGroupInlineAcks?.has(aliasNs) || this.pendingGroupInlineAckNamespaces?.has(aliasNs)))
|
|
426
|
+
return false;
|
|
427
|
+
const pendingMap = this.pendingGroupInlineAcks ?? new Map();
|
|
428
|
+
this.pendingGroupInlineAcks = pendingMap;
|
|
429
|
+
pendingMap.set(ns, {
|
|
430
|
+
seq,
|
|
431
|
+
row: this.inlinePostAckRow(row),
|
|
432
|
+
generation,
|
|
433
|
+
groupId,
|
|
434
|
+
groupAid: normalizedGroupAid,
|
|
435
|
+
aliasNs: aliasNs !== ns ? aliasNs : '',
|
|
436
|
+
committed: false,
|
|
437
|
+
observedAckSeq: 0,
|
|
438
|
+
acking: false,
|
|
120
439
|
});
|
|
440
|
+
if (aliasNs && aliasNs !== ns) {
|
|
441
|
+
const aliasMap = this.pendingGroupInlineAckNamespaces ?? new Map();
|
|
442
|
+
this.pendingGroupInlineAckNamespaces = aliasMap;
|
|
443
|
+
aliasMap.set(aliasNs, ns);
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
removePendingGroupInlineAck(ns, pending) {
|
|
448
|
+
if (this.pendingGroupInlineAcks?.get(ns) !== pending)
|
|
449
|
+
return false;
|
|
450
|
+
this.pendingGroupInlineAcks.delete(ns);
|
|
451
|
+
if (pending.aliasNs && this.pendingGroupInlineAckNamespaces?.get(pending.aliasNs) === ns) {
|
|
452
|
+
this.pendingGroupInlineAckNamespaces.delete(pending.aliasNs);
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
dropPendingGroupInlineAck(ns, seq, generation = this.inlineGeneration) {
|
|
457
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
458
|
+
if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
|
|
459
|
+
this.removePendingGroupInlineAck(ns, pending);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
resolvePendingGroupInlineAckNamespace(ns) {
|
|
463
|
+
const pendingMap = this.pendingGroupInlineAcks;
|
|
464
|
+
if (!ns || !pendingMap || pendingMap.size === 0 || pendingMap.has(ns))
|
|
465
|
+
return ns;
|
|
466
|
+
const canonicalNs = this.pendingGroupInlineAckNamespaces?.get(ns);
|
|
467
|
+
return canonicalNs && pendingMap.has(canonicalNs) ? canonicalNs : ns;
|
|
468
|
+
}
|
|
469
|
+
pendingP2PInlineAckGeneration(ns) {
|
|
470
|
+
const pendingMap = this.pendingP2PInlineAcks;
|
|
471
|
+
if (!ns || !pendingMap || pendingMap.size === 0)
|
|
472
|
+
return null;
|
|
473
|
+
const pending = pendingMap.get(ns);
|
|
474
|
+
return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
|
|
475
|
+
}
|
|
476
|
+
pendingGroupInlineAckGeneration(ns) {
|
|
477
|
+
const pendingMap = this.pendingGroupInlineAcks;
|
|
478
|
+
if (!ns || !pendingMap || pendingMap.size === 0)
|
|
479
|
+
return null;
|
|
480
|
+
const canonicalNs = pendingMap.has(ns) ? ns : this.pendingGroupInlineAckNamespaces?.get(ns);
|
|
481
|
+
if (!canonicalNs)
|
|
482
|
+
return null;
|
|
483
|
+
const pending = pendingMap.get(canonicalNs);
|
|
484
|
+
return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
|
|
485
|
+
}
|
|
486
|
+
completePendingP2PInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
|
|
487
|
+
const pendingMap = this.pendingP2PInlineAcks;
|
|
488
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
489
|
+
|| !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
|
|
490
|
+
return;
|
|
491
|
+
const pending = pendingMap.get(ns);
|
|
492
|
+
if (!pending || pending.generation !== generation || ackSeq < pending.seq)
|
|
493
|
+
return;
|
|
494
|
+
if (!pending.committed) {
|
|
495
|
+
pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
pendingMap.delete(ns);
|
|
499
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
|
|
500
|
+
}
|
|
501
|
+
completePendingGroupInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
|
|
502
|
+
const pendingMap = this.pendingGroupInlineAcks;
|
|
503
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
504
|
+
|| !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
|
|
505
|
+
return;
|
|
506
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
507
|
+
const pending = pendingMap.get(canonicalNs);
|
|
508
|
+
if (!pending || pending.generation !== generation || ackSeq < pending.seq)
|
|
509
|
+
return;
|
|
510
|
+
if (!pending.committed) {
|
|
511
|
+
pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.removePendingGroupInlineAck(canonicalNs, pending);
|
|
515
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
|
|
516
|
+
}
|
|
517
|
+
runPostInlineCommit(row, seq, scope, generation) {
|
|
518
|
+
const client = this.runtime.client;
|
|
519
|
+
if (!this.isInlineGenerationCurrent(generation) || typeof client._postInlineV2CommitAndAck !== 'function')
|
|
520
|
+
return;
|
|
521
|
+
try {
|
|
522
|
+
void Promise.resolve(client._postInlineV2CommitAndAck(row)).catch((exc) => {
|
|
523
|
+
client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
catch (exc) {
|
|
527
|
+
client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
markPendingP2PInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
|
|
531
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
532
|
+
if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
|
|
533
|
+
return false;
|
|
534
|
+
pending.committed = true;
|
|
535
|
+
if (pending.observedAckSeq < seq)
|
|
536
|
+
return true;
|
|
537
|
+
this.pendingP2PInlineAcks?.delete(ns);
|
|
538
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
markPendingGroupInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
|
|
542
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
543
|
+
if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
|
|
544
|
+
return false;
|
|
545
|
+
pending.committed = true;
|
|
546
|
+
if (pending.observedAckSeq < seq)
|
|
547
|
+
return true;
|
|
548
|
+
this.removePendingGroupInlineAck(ns, pending);
|
|
549
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
resolveActualAckSeq(result, requestedSeq) {
|
|
553
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
554
|
+
return 0;
|
|
555
|
+
if (!isJsonObject(result))
|
|
556
|
+
return requestedSeq;
|
|
557
|
+
const row = result;
|
|
558
|
+
let value;
|
|
559
|
+
if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
|
|
560
|
+
value = row.effective_ack_seq;
|
|
561
|
+
}
|
|
562
|
+
else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
|
|
563
|
+
value = row.ack_seq;
|
|
564
|
+
}
|
|
565
|
+
else if (Object.prototype.hasOwnProperty.call(row, 'cursor')) {
|
|
566
|
+
const cursor = row.cursor;
|
|
567
|
+
if (isJsonObject(cursor)) {
|
|
568
|
+
const cursorRow = cursor;
|
|
569
|
+
if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
|
|
570
|
+
return 0;
|
|
571
|
+
value = cursorRow.current_seq;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
value = cursor;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
return requestedSeq;
|
|
579
|
+
}
|
|
580
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
581
|
+
}
|
|
582
|
+
resolveP2PPullAckSeq(result, requestedSeq) {
|
|
583
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
584
|
+
return 0;
|
|
585
|
+
if (isJsonObject(result)
|
|
586
|
+
&& Object.prototype.hasOwnProperty.call(result, 'server_ack_seq')) {
|
|
587
|
+
const value = result.server_ack_seq;
|
|
588
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
589
|
+
}
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
resolveGroupPullAckSeq(result, requestedSeq) {
|
|
593
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
594
|
+
return 0;
|
|
595
|
+
if (!isJsonObject(result))
|
|
596
|
+
return 0;
|
|
597
|
+
const row = result;
|
|
598
|
+
if (!Object.prototype.hasOwnProperty.call(row, 'cursor'))
|
|
599
|
+
return 0;
|
|
600
|
+
const cursor = row.cursor;
|
|
601
|
+
if (!isJsonObject(cursor))
|
|
602
|
+
return 0;
|
|
603
|
+
const cursorRow = cursor;
|
|
604
|
+
if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
|
|
605
|
+
return 0;
|
|
606
|
+
const value = cursorRow.current_seq;
|
|
607
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
608
|
+
}
|
|
609
|
+
resolveForwardAckSeq(result, requestedSeq) {
|
|
610
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
611
|
+
return 0;
|
|
612
|
+
// 旧服务端成功响应可能不带累计水位字段;RPC 已成功,兼容按请求水位处理。
|
|
613
|
+
if (!isJsonObject(result))
|
|
614
|
+
return requestedSeq;
|
|
615
|
+
const row = result;
|
|
616
|
+
let value;
|
|
617
|
+
if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
|
|
618
|
+
value = row.effective_ack_seq;
|
|
619
|
+
}
|
|
620
|
+
else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
|
|
621
|
+
value = row.ack_seq;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
return requestedSeq;
|
|
625
|
+
}
|
|
626
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
627
|
+
}
|
|
628
|
+
forwardCoordinator() {
|
|
629
|
+
const coordinator = this.runtime.client._v2E2EE;
|
|
630
|
+
if (!coordinator
|
|
631
|
+
|| typeof coordinator.pendingForwardCursor !== 'function'
|
|
632
|
+
|| typeof coordinator.recordForwardCursor !== 'function'
|
|
633
|
+
|| typeof coordinator.clearForwardCursor !== 'function'
|
|
634
|
+
|| typeof coordinator.pendingForwardAck !== 'function'
|
|
635
|
+
|| typeof coordinator.recordForwardAck !== 'function'
|
|
636
|
+
|| typeof coordinator.clearForwardAck !== 'function') {
|
|
637
|
+
throw new Error('Forward cursor coordinator is unavailable');
|
|
638
|
+
}
|
|
639
|
+
return coordinator;
|
|
640
|
+
}
|
|
641
|
+
async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
|
|
642
|
+
const client = this.runtime.client;
|
|
643
|
+
const coordinator = this.forwardCoordinator();
|
|
644
|
+
const generation = this.captureInlineGeneration();
|
|
645
|
+
coordinator.recordForwardAck(ns, ackSeq);
|
|
646
|
+
const params = method === 'message.ack'
|
|
647
|
+
? {
|
|
648
|
+
seq: ackSeq,
|
|
649
|
+
device_id: client._deviceId,
|
|
650
|
+
slot_id: client._slotId,
|
|
651
|
+
_rpc_background: true,
|
|
652
|
+
}
|
|
653
|
+
: {
|
|
654
|
+
group_id: groupId,
|
|
655
|
+
msg_seq: ackSeq,
|
|
656
|
+
device_id: client._deviceId,
|
|
657
|
+
slot_id: client._slotId,
|
|
658
|
+
_rpc_background: true,
|
|
659
|
+
};
|
|
660
|
+
const result = await client._rpcPipeline.rawCall(method, params, { background: true });
|
|
661
|
+
const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
|
|
662
|
+
if (actualAckSeq < ackSeq) {
|
|
663
|
+
throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
|
|
664
|
+
}
|
|
665
|
+
coordinator.clearForwardAck(ns, actualAckSeq);
|
|
666
|
+
if (method === 'message.ack') {
|
|
667
|
+
this.completePendingP2PInlineAckUpTo(ns, actualAckSeq, generation);
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
this.completePendingGroupInlineAckUpTo(ns, actualAckSeq, generation);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
|
|
674
|
+
if (!isJsonObject(result)) {
|
|
675
|
+
throw new Error(`${method} response must be an object`);
|
|
676
|
+
}
|
|
677
|
+
const client = this.runtime.client;
|
|
678
|
+
const response = result;
|
|
679
|
+
const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
|
|
680
|
+
const coordinator = this.forwardCoordinator();
|
|
681
|
+
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
682
|
+
const pageInlineGeneration = this.captureInlineGeneration();
|
|
683
|
+
const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
|
|
684
|
+
const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
|
|
685
|
+
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
686
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
687
|
+
: null;
|
|
688
|
+
response._contig_before = pageContigBefore;
|
|
689
|
+
const cursor = isJsonObject(response.cursor)
|
|
690
|
+
? response.cursor
|
|
691
|
+
: null;
|
|
692
|
+
const serverAckRaw = method === 'message.pull' ? response.server_ack_seq : cursor?.current_seq;
|
|
693
|
+
const hasServerAck = method === 'message.pull'
|
|
694
|
+
? Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')
|
|
695
|
+
: Boolean(cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq'));
|
|
696
|
+
const parsedServerAck = hasServerAck ? nonNegativeSafeSequenceHint(serverAckRaw) : null;
|
|
697
|
+
const serverAckSeq = parsedServerAck ?? 0;
|
|
698
|
+
const retentionFloor = method === 'message.pull'
|
|
699
|
+
? positiveSafeSequenceHint(response.retention_floor_seq)
|
|
700
|
+
: Math.max(positiveSafeSequenceHint(response.retention_floor_message_seq), positiveSafeSequenceHint(response.retention_floor_seq), positiveSafeSequenceHint(cursor?.retention_floor_seq));
|
|
701
|
+
const visibilityFloor = method === 'group.pull'
|
|
702
|
+
? Math.max(positiveSafeSequenceHint(cursor?.join_seq), Math.max(0, positiveSafeSequenceHint(response.earliest_available_message_seq) - 1))
|
|
703
|
+
: 0;
|
|
704
|
+
let publishedCount = 0;
|
|
705
|
+
let committed = false;
|
|
706
|
+
try {
|
|
707
|
+
for (const rawMessage of messages) {
|
|
708
|
+
const seq = positiveSafeSequenceHint(rawMessage.seq);
|
|
709
|
+
if (method === 'message.pull') {
|
|
710
|
+
const appEvent = p2pAppEventFromPlainPullMessage(rawMessage);
|
|
711
|
+
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
712
|
+
publishedCount += 1;
|
|
713
|
+
}
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const message = normalizeGroupMentionMode(rawMessage);
|
|
717
|
+
if (this.recallEventFromGroupMessage(message)) {
|
|
718
|
+
if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
|
|
719
|
+
this.markPublishedSeq(ns, seq);
|
|
720
|
+
publishedCount += 1;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
|
|
724
|
+
publishedCount += 1;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (messages.length > 0)
|
|
728
|
+
client._seqTracker.onPullResult(ns, messages, afterSeq);
|
|
729
|
+
const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
|
|
730
|
+
if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
|
|
731
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
732
|
+
}
|
|
733
|
+
if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
|
|
734
|
+
await this.drainOrderedMessages(ns, undefined, false, false);
|
|
735
|
+
await client._commitSeqTrackerState(ns);
|
|
736
|
+
}
|
|
737
|
+
committed = true;
|
|
738
|
+
}
|
|
739
|
+
catch (exc) {
|
|
740
|
+
if (!committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
741
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
742
|
+
this.dropSeqTrackerPending(ns);
|
|
743
|
+
}
|
|
744
|
+
throw exc;
|
|
745
|
+
}
|
|
746
|
+
const committedAck = client._seqTracker.getContiguousSeq(ns);
|
|
747
|
+
if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
|
|
748
|
+
coordinator.clearForwardCursor(ns, committedAck);
|
|
749
|
+
}
|
|
750
|
+
if (hasServerAck && parsedServerAck !== null && serverAckSeq > committedAck) {
|
|
751
|
+
coordinator.recordForwardCursor(ns, serverAckSeq, committedAck);
|
|
752
|
+
}
|
|
753
|
+
if (hasServerAck && parsedServerAck !== null && serverAckSeq >= deferredForwardAck) {
|
|
754
|
+
coordinator.clearForwardAck(ns, serverAckSeq);
|
|
755
|
+
if (method === 'message.pull') {
|
|
756
|
+
this.completePendingP2PInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
this.completePendingGroupInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const contigAdvanced = committedAck > pageContigBefore;
|
|
763
|
+
const knownServerAck = hasServerAck ? serverAckSeq : deferredServerCursor;
|
|
764
|
+
if (committedAck > 0
|
|
765
|
+
&& ((hasServerAck && committedAck > serverAckSeq)
|
|
766
|
+
|| (contigAdvanced && committedAck > knownServerAck))) {
|
|
767
|
+
coordinator.recordForwardAck(ns, committedAck);
|
|
768
|
+
}
|
|
769
|
+
const pendingAckSeq = Number(coordinator.pendingForwardAck(ns, committedAck) ?? 0);
|
|
770
|
+
if (pendingAckSeq > 0) {
|
|
771
|
+
const ackMethod = method === 'message.pull' ? 'message.ack' : 'group.ack_messages';
|
|
772
|
+
const clampedAckSeq = this.clampAckSeq(ackMethod, ackMethod === 'message.ack' ? 'seq' : 'msg_seq', ns, pendingAckSeq);
|
|
773
|
+
if (clampedAckSeq < pendingAckSeq) {
|
|
774
|
+
throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
|
|
775
|
+
}
|
|
776
|
+
await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
|
|
777
|
+
}
|
|
778
|
+
return { rawCount: messages.length, publishedCount };
|
|
121
779
|
}
|
|
122
780
|
enqueueOrderedMessage(ns, event, seq, payload) {
|
|
123
781
|
const client = this.runtime.client;
|
|
@@ -136,7 +794,7 @@ export class MessageDeliveryEngine {
|
|
|
136
794
|
isGroupEventNamespace(ns) {
|
|
137
795
|
return ns.startsWith('group_event:');
|
|
138
796
|
}
|
|
139
|
-
async publishOrderedQueueItem(ns, event, seq, payload) {
|
|
797
|
+
async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false) {
|
|
140
798
|
const client = this.runtime.client;
|
|
141
799
|
if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
|
|
142
800
|
await this.publishOrderedGroupChanged(payload);
|
|
@@ -146,6 +804,10 @@ export class MessageDeliveryEngine {
|
|
|
146
804
|
await this.publishMessageRecallTombstone(seq, payload);
|
|
147
805
|
return;
|
|
148
806
|
}
|
|
807
|
+
if (pullResponse) {
|
|
808
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
149
811
|
await client._publishAppEvent(event, payload);
|
|
150
812
|
}
|
|
151
813
|
async publishOrderedGroupChanged(payload) {
|
|
@@ -154,8 +816,8 @@ export class MessageDeliveryEngine {
|
|
|
154
816
|
const eventPayload = payload;
|
|
155
817
|
client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
|
|
156
818
|
if (eventPayload.action === 'dissolved') {
|
|
157
|
-
const rawGroupId = eventPayload.
|
|
158
|
-
const groupId =
|
|
819
|
+
const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
|
|
820
|
+
const groupId = String(rawGroupId).trim();
|
|
159
821
|
if (groupId)
|
|
160
822
|
client._cleanupDissolvedGroup?.(groupId);
|
|
161
823
|
}
|
|
@@ -185,13 +847,28 @@ export class MessageDeliveryEngine {
|
|
|
185
847
|
}
|
|
186
848
|
normalizePublishedMessagePayload(event, payload) {
|
|
187
849
|
if (this.isInstanceScopedMessageEvent(event)) {
|
|
850
|
+
payload = this.stripInlineInternalFields(payload);
|
|
851
|
+
if (event === 'group.message_created')
|
|
852
|
+
payload = normalizeGroupMentionMode(payload);
|
|
188
853
|
return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
|
|
189
854
|
}
|
|
190
855
|
if (this.isGroupScopedEvent(event)) {
|
|
856
|
+
payload = this.stripInlineInternalFields(payload);
|
|
191
857
|
return this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
|
|
192
858
|
}
|
|
193
859
|
return payload;
|
|
194
860
|
}
|
|
861
|
+
stripInlineInternalFields(payload) {
|
|
862
|
+
if (!isJsonObject(payload))
|
|
863
|
+
return payload;
|
|
864
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'inline_message')
|
|
865
|
+
&& !Object.prototype.hasOwnProperty.call(payload, '_inline_message_candidates_v1'))
|
|
866
|
+
return payload;
|
|
867
|
+
const result = { ...payload };
|
|
868
|
+
delete result.inline_message;
|
|
869
|
+
delete result._inline_message_candidates_v1;
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
195
872
|
envelopeMetadata(value) {
|
|
196
873
|
let source = value;
|
|
197
874
|
if (source && typeof source === 'object') {
|
|
@@ -248,6 +925,7 @@ export class MessageDeliveryEngine {
|
|
|
248
925
|
if (protectedHeaders)
|
|
249
926
|
envelope.protected_headers = protectedHeaders;
|
|
250
927
|
setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
|
|
928
|
+
setIfPresent('mention_mode', message.mention_mode);
|
|
251
929
|
return envelope;
|
|
252
930
|
}
|
|
253
931
|
isGroupScopedEvent(event) {
|
|
@@ -314,19 +992,30 @@ export class MessageDeliveryEngine {
|
|
|
314
992
|
if (protectedHeaders)
|
|
315
993
|
envelope.protected_headers = protectedHeaders;
|
|
316
994
|
setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
|
|
995
|
+
if (method === 'group.send')
|
|
996
|
+
setIfPresent('mention_mode', resultObj.mention_mode);
|
|
317
997
|
return envelope;
|
|
318
998
|
}
|
|
319
|
-
attachSendResultEnvelope(method, params, result, encrypted) {
|
|
999
|
+
attachSendResultEnvelope(method, params, result, encrypted, modeSource) {
|
|
320
1000
|
if (!APP_SEND_ENVELOPE_METHODS.has(method) || !isJsonObject(result))
|
|
321
1001
|
return result;
|
|
322
|
-
|
|
323
|
-
|
|
1002
|
+
let out = { ...result };
|
|
1003
|
+
const serverEnvelope = method === 'group.send' && isJsonObject(out.envelope)
|
|
1004
|
+
? { ...out.envelope }
|
|
1005
|
+
: undefined;
|
|
324
1006
|
if ('payload' in params) {
|
|
325
1007
|
out.payload = params.payload;
|
|
326
1008
|
}
|
|
327
1009
|
else if ('content' in params) {
|
|
328
1010
|
out.payload = params.content;
|
|
329
1011
|
}
|
|
1012
|
+
const generatedEnvelope = this.sendResultEnvelope(method, params, out, encrypted);
|
|
1013
|
+
out.envelope = serverEnvelope ? { ...generatedEnvelope, ...serverEnvelope } : generatedEnvelope;
|
|
1014
|
+
if (method === 'group.send') {
|
|
1015
|
+
const serverSource = modeSource ?? result;
|
|
1016
|
+
const authoritativeSource = hasGroupMentionModeField(serverSource) ? serverSource : out;
|
|
1017
|
+
out = normalizeGroupMentionMode(out, authoritativeSource);
|
|
1018
|
+
}
|
|
330
1019
|
return out;
|
|
331
1020
|
}
|
|
332
1021
|
attachAppGroupEventEnvelope(payload) {
|
|
@@ -516,7 +1205,7 @@ export class MessageDeliveryEngine {
|
|
|
516
1205
|
// (group_id, sorted message_ids) 已能唯一标识一次撤回;缺 message_ids 时按原消息 id/seq 兜底。
|
|
517
1206
|
// 去重键不含 recalled_at:占位/通知 tombstone(pull)与在线 push 三条通道对同一次撤回
|
|
518
1207
|
// 可能携带不同来源的时间戳(push 在事务后重取),纳入 recalled_at 会使去重失效、回调多次。
|
|
519
|
-
const normalizedGroupId =
|
|
1208
|
+
const normalizedGroupId = String(groupId ?? '').trim();
|
|
520
1209
|
const ids = payload.message_ids;
|
|
521
1210
|
const idPart = Array.isArray(ids)
|
|
522
1211
|
? ids.map((i) => String(i ?? '').trim()).filter(Boolean).sort().join(',')
|
|
@@ -545,9 +1234,7 @@ export class MessageDeliveryEngine {
|
|
|
545
1234
|
if (!eventPayload)
|
|
546
1235
|
return false;
|
|
547
1236
|
const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
|
|
548
|
-
const dedupGroupId =
|
|
549
|
-
if (dedupGroupId)
|
|
550
|
-
eventPayload.group_id = dedupGroupId;
|
|
1237
|
+
const dedupGroupId = rawGroupId;
|
|
551
1238
|
const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
|
|
552
1239
|
let seen = client._groupRecallSeen;
|
|
553
1240
|
if (!seen) {
|
|
@@ -645,7 +1332,7 @@ export class MessageDeliveryEngine {
|
|
|
645
1332
|
}
|
|
646
1333
|
if (isJsonObject(payload)) {
|
|
647
1334
|
try {
|
|
648
|
-
const snapshot = client._agentMdManager.eventSnapshot();
|
|
1335
|
+
const snapshot = await client._agentMdManager.eventSnapshot();
|
|
649
1336
|
const localEtag = snapshot?.local_etag || '';
|
|
650
1337
|
const remoteEtag = snapshot?.remote_etag || '';
|
|
651
1338
|
if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
|
|
@@ -665,24 +1352,76 @@ export class MessageDeliveryEngine {
|
|
|
665
1352
|
if (!isJsonObject(message))
|
|
666
1353
|
return true;
|
|
667
1354
|
const client = this.runtime.client;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (
|
|
1355
|
+
const explicitDevice = this.strictTargetString(message, 'target_device_id');
|
|
1356
|
+
if (explicitDevice.present) {
|
|
1357
|
+
if (!explicitDevice.valid || explicitDevice.value !== client._deviceId)
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
const targetDeviceId = this.boundString(message, ['device_id']);
|
|
1362
|
+
if (targetDeviceId.present && targetDeviceId.value !== client._deviceId) {
|
|
671
1363
|
return false;
|
|
672
1364
|
}
|
|
673
1365
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (
|
|
1366
|
+
const explicitSlot = this.strictTargetString(message, 'target_slot_id');
|
|
1367
|
+
if (explicitSlot.present) {
|
|
1368
|
+
if (!explicitSlot.valid
|
|
1369
|
+
|| slotIsolationKey(explicitSlot.value) !== slotIsolationKey(client._slotId))
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
const targetSlotId = this.boundString(message, ['slot_id']);
|
|
1374
|
+
if (targetSlotId.present
|
|
1375
|
+
&& slotIsolationKey(targetSlotId.value) !== slotIsolationKey(client._slotId)) {
|
|
677
1376
|
return false;
|
|
678
1377
|
}
|
|
679
1378
|
}
|
|
680
1379
|
return true;
|
|
681
1380
|
}
|
|
682
|
-
|
|
1381
|
+
strictTargetString(source, key) {
|
|
1382
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1383
|
+
return { present: false, valid: false, value: '' };
|
|
1384
|
+
}
|
|
1385
|
+
const raw = source[key];
|
|
1386
|
+
if (typeof raw !== 'string')
|
|
1387
|
+
return { present: true, valid: false, value: '' };
|
|
1388
|
+
const value = raw.trim();
|
|
1389
|
+
return { present: true, valid: Boolean(value), value };
|
|
1390
|
+
}
|
|
1391
|
+
async runPushProcessSerialized(ns, operation, inlineGeneration) {
|
|
683
1392
|
const client = this.runtime.client;
|
|
684
|
-
if (
|
|
1393
|
+
if (inlineGeneration === undefined) {
|
|
1394
|
+
if (!ns) {
|
|
1395
|
+
await operation();
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
let queues = client._pushProcessQueues;
|
|
1399
|
+
if (!queues) {
|
|
1400
|
+
queues = new Map();
|
|
1401
|
+
client._pushProcessQueues = queues;
|
|
1402
|
+
}
|
|
1403
|
+
const previous = queues.get(ns) ?? Promise.resolve();
|
|
1404
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
1405
|
+
const stored = current.then(() => undefined, () => undefined);
|
|
1406
|
+
queues.set(ns, stored);
|
|
1407
|
+
try {
|
|
1408
|
+
await current;
|
|
1409
|
+
}
|
|
1410
|
+
finally {
|
|
1411
|
+
if (queues.get(ns) === stored)
|
|
1412
|
+
queues.delete(ns);
|
|
1413
|
+
}
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const run = async () => {
|
|
1417
|
+
if (!this.isInlineGenerationCurrent(inlineGeneration))
|
|
1418
|
+
return;
|
|
685
1419
|
await operation();
|
|
1420
|
+
if (!this.isInlineGenerationCurrent(inlineGeneration))
|
|
1421
|
+
return;
|
|
1422
|
+
};
|
|
1423
|
+
if (!ns) {
|
|
1424
|
+
await run();
|
|
686
1425
|
return;
|
|
687
1426
|
}
|
|
688
1427
|
let queues = client._pushProcessQueues;
|
|
@@ -691,7 +1430,7 @@ export class MessageDeliveryEngine {
|
|
|
691
1430
|
client._pushProcessQueues = queues;
|
|
692
1431
|
}
|
|
693
1432
|
const previous = queues.get(ns) ?? Promise.resolve();
|
|
694
|
-
const current = previous.catch(() => undefined).then(
|
|
1433
|
+
const current = previous.catch(() => undefined).then(run);
|
|
695
1434
|
const stored = current.then(() => undefined, () => undefined);
|
|
696
1435
|
queues.set(ns, stored);
|
|
697
1436
|
try {
|
|
@@ -708,7 +1447,18 @@ export class MessageDeliveryEngine {
|
|
|
708
1447
|
const ns = isJsonObject(data) && client._aid && data.seq !== undefined
|
|
709
1448
|
? `p2p:${client._aid}`
|
|
710
1449
|
: '';
|
|
711
|
-
|
|
1450
|
+
const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
|
|
1451
|
+
if (client._v2Session && ns && Number.isSafeInteger(seq) && seq > 0
|
|
1452
|
+
&& this.messageTargetsCurrentInstance(data)) {
|
|
1453
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
1454
|
+
}
|
|
1455
|
+
const hasInlineObject = isJsonObject(data)
|
|
1456
|
+
&& isJsonObject(data.inline_message);
|
|
1457
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
1458
|
+
const operation = () => this.processAndPublishMessage(data, inlineGeneration);
|
|
1459
|
+
client._safeAsync(client._v2Session && ns && !hasInlineObject
|
|
1460
|
+
? operation()
|
|
1461
|
+
: this.runPushProcessSerialized(ns, operation, inlineGeneration));
|
|
712
1462
|
client._clientLog.debug('_onRawMessageReceived exit: elapsed=0ms (dispatched async)');
|
|
713
1463
|
}
|
|
714
1464
|
async onRawMessageRecalled(data) {
|
|
@@ -729,7 +1479,7 @@ export class MessageDeliveryEngine {
|
|
|
729
1479
|
const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
|
|
730
1480
|
const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
|
|
731
1481
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
732
|
-
if (seqNeedsPull && !published) {
|
|
1482
|
+
if (seqNeedsPull && !published && client._sessionOptions?.background_sync !== false) {
|
|
733
1483
|
client._safeAsync(this.fillP2pGap());
|
|
734
1484
|
}
|
|
735
1485
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -748,7 +1498,7 @@ export class MessageDeliveryEngine {
|
|
|
748
1498
|
}
|
|
749
1499
|
await this.publishMessageRecallTombstone(seq, msg);
|
|
750
1500
|
}
|
|
751
|
-
async processAndPublishMessage(data) {
|
|
1501
|
+
async processAndPublishMessage(data, inlineGeneration) {
|
|
752
1502
|
const client = this.runtime.client;
|
|
753
1503
|
try {
|
|
754
1504
|
if (!isJsonObject(data)) {
|
|
@@ -759,6 +1509,15 @@ export class MessageDeliveryEngine {
|
|
|
759
1509
|
if (!this.messageTargetsCurrentInstance(msg)) {
|
|
760
1510
|
return;
|
|
761
1511
|
}
|
|
1512
|
+
// 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
|
|
1513
|
+
// 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
|
|
1514
|
+
// 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
|
|
1515
|
+
const legacyPushSeq = positiveSafeSequenceHint(msg.seq);
|
|
1516
|
+
const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
1517
|
+
if (client._v2Session && client._aid && (legacyPushSeq > 0 || hasInlineField)) {
|
|
1518
|
+
await this.processV2P2PNotification(msg, 'v1', inlineGeneration);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
762
1521
|
const seq = msg.seq;
|
|
763
1522
|
const encryptedPush = client._isEncryptedPushMessage(msg);
|
|
764
1523
|
if (seq !== undefined && seq !== null && client._aid) {
|
|
@@ -772,7 +1531,7 @@ export class MessageDeliveryEngine {
|
|
|
772
1531
|
: await this.publishOrderedMessage('message.received', ns, seq, msg);
|
|
773
1532
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
774
1533
|
const needPull = seqNeedsPull && !published;
|
|
775
|
-
if (needPull) {
|
|
1534
|
+
if (needPull && client._sessionOptions?.background_sync !== false) {
|
|
776
1535
|
client._safeAsync(this.fillP2pGap());
|
|
777
1536
|
}
|
|
778
1537
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -818,11 +1577,24 @@ export class MessageDeliveryEngine {
|
|
|
818
1577
|
onRawGroupMessageCreated(data) {
|
|
819
1578
|
const client = this.runtime.client;
|
|
820
1579
|
client._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
|
|
821
|
-
const groupId = isJsonObject(data)
|
|
822
|
-
|
|
1580
|
+
const groupId = isJsonObject(data) && typeof data.group_id === 'string' ? data.group_id.trim() : '';
|
|
1581
|
+
const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
|
|
1582
|
+
const kind = isJsonObject(data) ? String(data.kind ?? '').trim() : '';
|
|
1583
|
+
if (client._v2Session && groupId && kind !== 'group.online_unread_hint'
|
|
1584
|
+
&& Number.isSafeInteger(seq) && seq > 0) {
|
|
1585
|
+
client._seqTracker.updateMaxSeen(`group:${groupId}`, seq);
|
|
1586
|
+
}
|
|
1587
|
+
const ns = groupId ? `group:${groupId}` : '';
|
|
1588
|
+
const hasInlineObject = isJsonObject(data)
|
|
1589
|
+
&& isJsonObject(data.inline_message);
|
|
1590
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
1591
|
+
const operation = () => this.processAndPublishGroupMessage(data, inlineGeneration);
|
|
1592
|
+
client._safeAsync(client._v2Session && ns && !hasInlineObject
|
|
1593
|
+
? operation()
|
|
1594
|
+
: this.runPushProcessSerialized(ns, operation, inlineGeneration));
|
|
823
1595
|
client._clientLog.debug('_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)');
|
|
824
1596
|
}
|
|
825
|
-
async processAndPublishGroupMessage(data) {
|
|
1597
|
+
async processAndPublishGroupMessage(data, inlineGeneration) {
|
|
826
1598
|
const client = this.runtime.client;
|
|
827
1599
|
try {
|
|
828
1600
|
if (!isJsonObject(data)) {
|
|
@@ -830,12 +1602,20 @@ export class MessageDeliveryEngine {
|
|
|
830
1602
|
return;
|
|
831
1603
|
}
|
|
832
1604
|
const msg = { ...data };
|
|
833
|
-
const groupId =
|
|
1605
|
+
const groupId = typeof msg.group_id === 'string' ? msg.group_id.trim() : '';
|
|
834
1606
|
const seq = msg.seq;
|
|
835
1607
|
const payload = msg.payload;
|
|
836
1608
|
if (groupId) {
|
|
837
1609
|
client._groupSynced.add(groupId);
|
|
838
1610
|
}
|
|
1611
|
+
// V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
|
|
1612
|
+
// 不能让 payload 路径直接推进旧 ordered publish 状态。
|
|
1613
|
+
const legacyPushSeq = positiveSafeSequenceHint(seq);
|
|
1614
|
+
const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
1615
|
+
if (client._v2Session && groupId && (legacyPushSeq > 0 || hasInlineField)) {
|
|
1616
|
+
await this.processGroupV2MessageCreated(msg, 'v1', inlineGeneration);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
839
1619
|
if (payload === undefined || payload === null
|
|
840
1620
|
|| (typeof payload === 'object' && Object.keys(payload).length === 0)) {
|
|
841
1621
|
await this.autoPullGroupMessages(msg);
|
|
@@ -873,7 +1653,7 @@ export class MessageDeliveryEngine {
|
|
|
873
1653
|
: await this.publishOrderedMessage('group.message_created', ns, seq, msg);
|
|
874
1654
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
875
1655
|
const needPull = seqNeedsPull && !published;
|
|
876
|
-
if (needPull) {
|
|
1656
|
+
if (needPull && client._sessionOptions?.background_sync !== false) {
|
|
877
1657
|
client._safeAsync(this.fillGroupGap(groupId));
|
|
878
1658
|
}
|
|
879
1659
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -924,56 +1704,53 @@ export class MessageDeliveryEngine {
|
|
|
924
1704
|
await client._publishAppEvent('group.message_created', notification);
|
|
925
1705
|
return;
|
|
926
1706
|
}
|
|
927
|
-
|
|
1707
|
+
if (client._sessionOptions?.background_sync === false) {
|
|
1708
|
+
client._clientLog.debug(`auto pull group messages skipped: group=${groupId} background_sync=false`);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
928
1712
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1713
|
+
if (client._v2Session) {
|
|
1714
|
+
try {
|
|
1715
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1716
|
+
await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
catch (exc) {
|
|
1720
|
+
client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
|
|
1721
|
+
await client._publishAppEvent('group.message_created', notification);
|
|
932
1722
|
return;
|
|
933
1723
|
}
|
|
934
|
-
|
|
1724
|
+
}
|
|
1725
|
+
let rawCompleted = false;
|
|
1726
|
+
try {
|
|
1727
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', {
|
|
935
1728
|
group_id: groupId,
|
|
936
1729
|
after_message_seq: afterSeq,
|
|
937
|
-
device_id: client._deviceId,
|
|
938
1730
|
limit: 50,
|
|
939
|
-
_rpc_background: true,
|
|
940
1731
|
});
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
else {
|
|
964
|
-
await client._publishAppEvent('group.message_created', msg);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
this.prunePushedSeqs(ns);
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
catch (exc) {
|
|
974
|
-
client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
|
|
1732
|
+
await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
1733
|
+
const result = await client._rpcPipeline.rawCall('group.pull', {
|
|
1734
|
+
group_id: groupId,
|
|
1735
|
+
after_message_seq: afterSeq,
|
|
1736
|
+
device_id: client._deviceId,
|
|
1737
|
+
slot_id: client._slotId,
|
|
1738
|
+
limit: 50,
|
|
1739
|
+
_rpc_background: true,
|
|
1740
|
+
}, { background: true });
|
|
1741
|
+
rawCompleted = true;
|
|
1742
|
+
await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
|
|
1743
|
+
this.prunePushedSeqs(ns);
|
|
1744
|
+
}, true);
|
|
1745
|
+
}
|
|
1746
|
+
catch (exc) {
|
|
1747
|
+
// 页发布或本地提交失败不能 ACK;提交后 ACK 失败则保留 pending。
|
|
1748
|
+
// 两种情况都不能再把原始通知当作第二条业务消息发布。
|
|
1749
|
+
client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
|
|
1750
|
+
if (!rawCompleted) {
|
|
1751
|
+
await client._publishAppEvent('group.message_created', notification);
|
|
1752
|
+
}
|
|
975
1753
|
}
|
|
976
|
-
await client._publishAppEvent('group.message_created', notification);
|
|
977
1754
|
}
|
|
978
1755
|
scheduleP2pGapRetry(ns, retryAttempt) {
|
|
979
1756
|
const client = this.runtime.client;
|
|
@@ -994,6 +1771,8 @@ export class MessageDeliveryEngine {
|
|
|
994
1771
|
}
|
|
995
1772
|
async fillP2pGap(retryAttempt = 0) {
|
|
996
1773
|
const client = this.runtime.client;
|
|
1774
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1775
|
+
return;
|
|
997
1776
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
998
1777
|
return;
|
|
999
1778
|
if (!client._aid)
|
|
@@ -1007,7 +1786,8 @@ export class MessageDeliveryEngine {
|
|
|
1007
1786
|
this.runtime.delivery.setGapFillActive(true);
|
|
1008
1787
|
let filled = 0;
|
|
1009
1788
|
try {
|
|
1010
|
-
const
|
|
1789
|
+
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 });
|
|
1790
|
+
const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullV2(afterSeq, 50, { gateLocked: true }), true);
|
|
1011
1791
|
filled = messages.length;
|
|
1012
1792
|
this.prunePushedSeqs(ns);
|
|
1013
1793
|
}
|
|
@@ -1031,6 +1811,7 @@ export class MessageDeliveryEngine {
|
|
|
1031
1811
|
finally {
|
|
1032
1812
|
client._gapFillDone.delete(dedupKey);
|
|
1033
1813
|
this.runtime.delivery.setGapFillActive(false);
|
|
1814
|
+
this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
|
|
1034
1815
|
}
|
|
1035
1816
|
}
|
|
1036
1817
|
async fillP2pGapWithPlainPull(ns, afterSeq) {
|
|
@@ -1042,31 +1823,17 @@ export class MessageDeliveryEngine {
|
|
|
1042
1823
|
slot_id: client._slotId,
|
|
1043
1824
|
_rpc_background: true,
|
|
1044
1825
|
};
|
|
1045
|
-
const raw = await client.
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
: raw;
|
|
1049
|
-
const messages = (isJsonObject(result) && Array.isArray(result.messages))
|
|
1050
|
-
? result.messages
|
|
1051
|
-
: [];
|
|
1052
|
-
let published = 0;
|
|
1053
|
-
for (const item of messages) {
|
|
1054
|
-
if (!isJsonObject(item))
|
|
1055
|
-
continue;
|
|
1056
|
-
const msg = item;
|
|
1057
|
-
const seq = Number(msg.seq ?? 0);
|
|
1058
|
-
const appEvent = p2pAppEventFromPlainPullMessage(msg);
|
|
1059
|
-
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload)) {
|
|
1060
|
-
published += 1;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
return published;
|
|
1826
|
+
const raw = await client._rpcPipeline.rawCall('message.pull', params, { background: true });
|
|
1827
|
+
const page = await this.commitPlainForwardPage(ns, afterSeq, 'message.pull', raw);
|
|
1828
|
+
return page.publishedCount;
|
|
1064
1829
|
}
|
|
1065
1830
|
async fillGroupGap(groupId) {
|
|
1066
1831
|
const client = this.runtime.client;
|
|
1832
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1833
|
+
return;
|
|
1067
1834
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
1068
1835
|
return;
|
|
1069
|
-
groupId =
|
|
1836
|
+
groupId = String(groupId ?? '').trim();
|
|
1070
1837
|
if (!groupId)
|
|
1071
1838
|
return;
|
|
1072
1839
|
const ns = `group:${groupId}`;
|
|
@@ -1078,7 +1845,8 @@ export class MessageDeliveryEngine {
|
|
|
1078
1845
|
this.runtime.delivery.setGapFillActive(true);
|
|
1079
1846
|
let filled = 0;
|
|
1080
1847
|
try {
|
|
1081
|
-
const
|
|
1848
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1849
|
+
const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
|
|
1082
1850
|
filled = messages.length;
|
|
1083
1851
|
this.prunePushedSeqs(ns);
|
|
1084
1852
|
}
|
|
@@ -1091,35 +1859,44 @@ export class MessageDeliveryEngine {
|
|
|
1091
1859
|
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
1092
1860
|
client._safeAsync(this.fillGroupGap(groupId));
|
|
1093
1861
|
}
|
|
1862
|
+
this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
|
|
1094
1863
|
}
|
|
1095
1864
|
}
|
|
1096
|
-
async fillGroupEventGap(groupId) {
|
|
1865
|
+
async fillGroupEventGap(groupId, options) {
|
|
1097
1866
|
const client = this.runtime.client;
|
|
1867
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1868
|
+
return;
|
|
1098
1869
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
1099
1870
|
return;
|
|
1100
|
-
groupId =
|
|
1871
|
+
groupId = String(groupId ?? '').trim();
|
|
1101
1872
|
if (!groupId)
|
|
1102
1873
|
return;
|
|
1103
1874
|
const ns = `group_event:${groupId}`;
|
|
1104
|
-
const
|
|
1875
|
+
const requestedAfterSeq = Number(options?.afterSeq ?? 0);
|
|
1876
|
+
const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
|
|
1877
|
+
? requestedAfterSeq
|
|
1878
|
+
: client._seqTracker.getContiguousSeq(ns);
|
|
1879
|
+
const singlePage = options?.singlePage === true;
|
|
1105
1880
|
const dedupKey = `group_event_pull:${ns}`;
|
|
1106
1881
|
if (client._gapFillDone.has(dedupKey))
|
|
1107
1882
|
return;
|
|
1108
1883
|
client._gapFillDone.add(dedupKey);
|
|
1109
1884
|
this.runtime.delivery.setGapFillActive(true);
|
|
1885
|
+
let continuationAfterSeq = 0;
|
|
1110
1886
|
try {
|
|
1111
1887
|
let nextAfterSeq = afterSeq;
|
|
1112
|
-
const maxPages = 100;
|
|
1888
|
+
const maxPages = singlePage ? 1 : 100;
|
|
1113
1889
|
let pageCount = 0;
|
|
1114
1890
|
while (pageCount < maxPages) {
|
|
1115
1891
|
pageCount += 1;
|
|
1116
|
-
const
|
|
1892
|
+
const request = {
|
|
1117
1893
|
group_id: groupId,
|
|
1118
1894
|
after_event_seq: nextAfterSeq,
|
|
1119
1895
|
device_id: client._deviceId,
|
|
1120
1896
|
limit: 50,
|
|
1121
1897
|
_rpc_background: true,
|
|
1122
|
-
}
|
|
1898
|
+
};
|
|
1899
|
+
const result = await client.call('group.pull_events', request);
|
|
1123
1900
|
if (!isJsonObject(result))
|
|
1124
1901
|
return;
|
|
1125
1902
|
const events = result.events;
|
|
@@ -1131,12 +1908,16 @@ export class MessageDeliveryEngine {
|
|
|
1131
1908
|
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
|
|
1132
1909
|
}
|
|
1133
1910
|
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1911
|
+
const retentionFloor = Math.max(0, Number(result.retention_floor_event_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
|
|
1912
|
+
const serverAckFloor = Math.max(0, Number(cursor?.current_seq ?? 0));
|
|
1913
|
+
const earliestAvailable = Number(result.earliest_available_event_seq ?? 0);
|
|
1914
|
+
const visibilityFloor = Math.max(0, Number(cursor?.join_seq ?? 0), Number.isFinite(earliestAvailable) && earliestAvailable > 0 ? earliestAvailable - 1 : 0);
|
|
1915
|
+
const effectiveFloor = Math.max(...[retentionFloor, serverAckFloor, visibilityFloor].filter((value) => Number.isFinite(value)));
|
|
1916
|
+
if (effectiveFloor > 0) {
|
|
1136
1917
|
const contigBeforeFloor = client._seqTracker.getContiguousSeq(ns);
|
|
1137
|
-
if (contigBeforeFloor <
|
|
1138
|
-
client._clientLog.info('group.pull_events
|
|
1139
|
-
client._seqTracker.forceContiguousSeq(ns,
|
|
1918
|
+
if (contigBeforeFloor < effectiveFloor) {
|
|
1919
|
+
client._clientLog.info('group.pull_events floor advance: ns=' + ns + ' contiguous=' + contigBeforeFloor + ' -> effective_floor=' + effectiveFloor + ' retention_floor=' + retentionFloor + ' server_ack=' + serverAckFloor + ' visibility_floor=' + visibilityFloor);
|
|
1920
|
+
client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
|
|
1140
1921
|
}
|
|
1141
1922
|
}
|
|
1142
1923
|
const eventSeqs = [];
|
|
@@ -1171,17 +1952,28 @@ export class MessageDeliveryEngine {
|
|
|
1171
1952
|
this.persistSeq(ns);
|
|
1172
1953
|
}
|
|
1173
1954
|
if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1955
|
+
try {
|
|
1956
|
+
const ackPromise = client._transport.call('group.ack_events', {
|
|
1957
|
+
group_id: groupId,
|
|
1958
|
+
event_seq: ackContig,
|
|
1959
|
+
device_id: client._deviceId,
|
|
1960
|
+
slot_id: client._slotId,
|
|
1961
|
+
_rpc_background: true,
|
|
1962
|
+
});
|
|
1963
|
+
if (singlePage)
|
|
1964
|
+
await ackPromise;
|
|
1965
|
+
else
|
|
1966
|
+
ackPromise.catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
|
|
1967
|
+
}
|
|
1968
|
+
catch (e) {
|
|
1969
|
+
client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
|
|
1970
|
+
}
|
|
1182
1971
|
}
|
|
1183
1972
|
const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
|
|
1184
|
-
if (
|
|
1973
|
+
if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
|
|
1974
|
+
continuationAfterSeq = nextAfter;
|
|
1975
|
+
}
|
|
1976
|
+
if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more !== true)
|
|
1185
1977
|
break;
|
|
1186
1978
|
nextAfterSeq = nextAfter;
|
|
1187
1979
|
}
|
|
@@ -1195,11 +1987,17 @@ export class MessageDeliveryEngine {
|
|
|
1195
1987
|
finally {
|
|
1196
1988
|
client._gapFillDone.delete(dedupKey);
|
|
1197
1989
|
this.runtime.delivery.setGapFillActive(false);
|
|
1990
|
+
if (singlePage && continuationAfterSeq > afterSeq) {
|
|
1991
|
+
this.enqueueOnlineUnreadEventHint({
|
|
1992
|
+
group_id: groupId,
|
|
1993
|
+
_online_hint_after_event_seq: continuationAfterSeq,
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1198
1996
|
}
|
|
1199
1997
|
}
|
|
1200
1998
|
async handleGroupChangedEventSeq(data, groupId) {
|
|
1201
1999
|
const client = this.runtime.client;
|
|
1202
|
-
groupId =
|
|
2000
|
+
groupId = String(groupId ?? '').trim();
|
|
1203
2001
|
let needPull = false;
|
|
1204
2002
|
const rawEventSeq = data.event_seq;
|
|
1205
2003
|
const eventSeq = Number(rawEventSeq);
|
|
@@ -1208,6 +2006,15 @@ export class MessageDeliveryEngine {
|
|
|
1208
2006
|
return;
|
|
1209
2007
|
}
|
|
1210
2008
|
const ns = `group_event:${groupId}`;
|
|
2009
|
+
if (String(data.kind ?? '').trim() === 'group.online_unread_hint') {
|
|
2010
|
+
if (client._sessionOptions?.background_sync === false) {
|
|
2011
|
+
client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
client._clientLog.debug(`group.changed online unread hint triggering event Forward: group=${groupId}, event_seq=${eventSeq}`);
|
|
2015
|
+
this.enqueueOnlineUnreadEventHint(data);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
1211
2018
|
if (this.isSelfJoinGroupChanged(data)) {
|
|
1212
2019
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
1213
2020
|
const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
|
|
@@ -1235,10 +2042,9 @@ export class MessageDeliveryEngine {
|
|
|
1235
2042
|
if (ackContig > 0 && ackContig !== contigBefore) {
|
|
1236
2043
|
if (data.action !== 'dissolved')
|
|
1237
2044
|
this.persistSeq(ns);
|
|
1238
|
-
const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
|
|
1239
2045
|
client._transport.call('group.ack_events', {
|
|
1240
2046
|
group_id: groupId,
|
|
1241
|
-
event_seq:
|
|
2047
|
+
event_seq: ackContig,
|
|
1242
2048
|
device_id: client._deviceId,
|
|
1243
2049
|
slot_id: client._slotId,
|
|
1244
2050
|
_rpc_background: true,
|
|
@@ -1246,7 +2052,7 @@ export class MessageDeliveryEngine {
|
|
|
1246
2052
|
client._clientLog.warn('group event push auto-ack failed: group=' + groupId, e);
|
|
1247
2053
|
});
|
|
1248
2054
|
}
|
|
1249
|
-
if (needPull && groupId && !data._from_gap_fill) {
|
|
2055
|
+
if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
|
|
1250
2056
|
client._safeAsync(this.fillGroupEventGap(groupId));
|
|
1251
2057
|
}
|
|
1252
2058
|
}
|
|
@@ -1268,185 +2074,882 @@ export class MessageDeliveryEngine {
|
|
|
1268
2074
|
const groupId = String(data.group_id ?? '').trim();
|
|
1269
2075
|
if (!groupId)
|
|
1270
2076
|
return;
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
this.runtime.delivery.setOnlineUnreadHintTimer(setTimeout(() => {
|
|
1276
|
-
this.runtime.delivery.setOnlineUnreadHintTimer(null);
|
|
1277
|
-
client._safeAsync(this.drainOnlineUnreadHints());
|
|
1278
|
-
}, delayMs));
|
|
2077
|
+
const ns = `group:${groupId}`;
|
|
2078
|
+
client._safeAsync(this.runGroupForwardRecovery(groupId, ns, 50, true).catch((exc) => {
|
|
2079
|
+
client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2080
|
+
}));
|
|
1279
2081
|
}
|
|
1280
|
-
|
|
2082
|
+
enqueueOnlineUnreadEventHint(data) {
|
|
1281
2083
|
const client = this.runtime.client;
|
|
1282
|
-
|
|
2084
|
+
const groupId = String(data.group_id ?? '').trim();
|
|
2085
|
+
if (!groupId || client._sessionOptions?.background_sync === false)
|
|
1283
2086
|
return;
|
|
1284
|
-
|
|
2087
|
+
const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
|
|
2088
|
+
const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
|
|
2089
|
+
client._safeAsync(this.fillGroupEventGap(groupId, {
|
|
2090
|
+
afterSeq,
|
|
2091
|
+
singlePage: true,
|
|
2092
|
+
}));
|
|
2093
|
+
}
|
|
2094
|
+
async runP2PForwardRecovery(ns, pageLimit, background = false) {
|
|
2095
|
+
const client = this.runtime.client;
|
|
2096
|
+
const request = {
|
|
2097
|
+
after_seq: this.syncState(ns).ack,
|
|
2098
|
+
limit: pageLimit,
|
|
2099
|
+
max_pages: 1,
|
|
2100
|
+
};
|
|
2101
|
+
const invoke = async () => {
|
|
2102
|
+
const after = this.syncState(ns).ack;
|
|
2103
|
+
const messages = await client._pullV2(after, pageLimit, { gateLocked: true, maxPages: 1 });
|
|
2104
|
+
return { messages, raw_count: messages.length };
|
|
2105
|
+
};
|
|
2106
|
+
const pipeline = client._rpcPipeline;
|
|
2107
|
+
if (!pipeline)
|
|
2108
|
+
return await invoke();
|
|
2109
|
+
const key = pipeline.pullGateKeyForCall('message.pull', request);
|
|
2110
|
+
return await pipeline.runPullSerialized(key, invoke, background);
|
|
2111
|
+
}
|
|
2112
|
+
async runGroupForwardRecovery(groupId, ns, pageLimit, background = false) {
|
|
2113
|
+
const client = this.runtime.client;
|
|
2114
|
+
const request = {
|
|
2115
|
+
group_id: groupId,
|
|
2116
|
+
after_seq: this.syncState(ns).ack,
|
|
2117
|
+
limit: pageLimit,
|
|
2118
|
+
max_pages: 1,
|
|
2119
|
+
};
|
|
2120
|
+
const invoke = async () => {
|
|
2121
|
+
const after = this.syncState(ns).ack;
|
|
2122
|
+
const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages: 1 });
|
|
2123
|
+
return { messages, raw_count: messages.length };
|
|
2124
|
+
};
|
|
2125
|
+
const pipeline = client._rpcPipeline;
|
|
2126
|
+
if (!pipeline)
|
|
2127
|
+
return await invoke();
|
|
2128
|
+
const key = pipeline.pullGateKeyForCall('group.pull', request);
|
|
2129
|
+
return await pipeline.runPullSerialized(key, invoke, background);
|
|
2130
|
+
}
|
|
2131
|
+
syncState(ns) {
|
|
2132
|
+
const tracker = this.runtime.client._seqTracker;
|
|
2133
|
+
if (typeof tracker.getSyncState === 'function')
|
|
2134
|
+
return tracker.getSyncState(ns);
|
|
2135
|
+
const ack = Number(tracker.getContiguousSeq(ns) || 0);
|
|
2136
|
+
return { ack, tail: ack, head: Math.max(ack, Number(tracker.getMaxSeenSeq?.(ns) || 0)) };
|
|
2137
|
+
}
|
|
2138
|
+
inlineMessage(data) {
|
|
2139
|
+
if (!isJsonObject(data))
|
|
2140
|
+
return null;
|
|
2141
|
+
const inline = data.inline_message;
|
|
2142
|
+
return isJsonObject(inline)
|
|
2143
|
+
? inline
|
|
2144
|
+
: null;
|
|
2145
|
+
}
|
|
2146
|
+
boundString(source, keys) {
|
|
2147
|
+
for (const key of keys) {
|
|
2148
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
2149
|
+
return { present: true, value: String(source[key] ?? '').trim() };
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return { present: false, value: '' };
|
|
2153
|
+
}
|
|
2154
|
+
sameRequiredBinding(left, leftKeys, right, rightKeys) {
|
|
2155
|
+
const lhs = this.consistentBindingValue(left, leftKeys);
|
|
2156
|
+
const rhs = this.consistentBindingValue(right, rightKeys);
|
|
2157
|
+
return lhs.present && rhs.present && Boolean(lhs.value) && lhs.value === rhs.value;
|
|
2158
|
+
}
|
|
2159
|
+
consistentBindingValue(source, keys) {
|
|
2160
|
+
const binding = this.strictBoundString(source, keys);
|
|
2161
|
+
return binding.present && binding.valid
|
|
2162
|
+
? { present: true, value: binding.value }
|
|
2163
|
+
: { present: false, value: '' };
|
|
2164
|
+
}
|
|
2165
|
+
strictBoundString(source, keys) {
|
|
2166
|
+
let value = null;
|
|
2167
|
+
for (const key of keys) {
|
|
2168
|
+
if (!Object.prototype.hasOwnProperty.call(source, key))
|
|
2169
|
+
continue;
|
|
2170
|
+
const raw = source[key];
|
|
2171
|
+
if (typeof raw !== 'string')
|
|
2172
|
+
return { present: true, valid: false, value: '' };
|
|
2173
|
+
const candidate = raw.trim();
|
|
2174
|
+
if (!candidate || (value !== null && candidate !== value)) {
|
|
2175
|
+
return { present: true, valid: false, value: '' };
|
|
2176
|
+
}
|
|
2177
|
+
value = candidate;
|
|
2178
|
+
}
|
|
2179
|
+
return { present: value !== null, valid: value !== null, value: value ?? '' };
|
|
2180
|
+
}
|
|
2181
|
+
onlyEmptyOptionalStrings(source, keys) {
|
|
2182
|
+
for (const key of keys) {
|
|
2183
|
+
if (!Object.prototype.hasOwnProperty.call(source, key))
|
|
2184
|
+
continue;
|
|
2185
|
+
const raw = source[key];
|
|
2186
|
+
if (typeof raw !== 'string' || raw.trim())
|
|
2187
|
+
return false;
|
|
2188
|
+
}
|
|
2189
|
+
return true;
|
|
2190
|
+
}
|
|
2191
|
+
inlineTargetMatchesCurrentInstance(head, inline) {
|
|
2192
|
+
const client = this.runtime.client;
|
|
2193
|
+
const explicitHeadDevice = this.strictTargetString(head, 'target_device_id');
|
|
2194
|
+
if (explicitHeadDevice.present && !explicitHeadDevice.valid)
|
|
2195
|
+
return false;
|
|
2196
|
+
const headDevice = explicitHeadDevice.present
|
|
2197
|
+
? explicitHeadDevice
|
|
2198
|
+
: this.strictBoundString(head, ['device_id']);
|
|
2199
|
+
if (!headDevice.present || !headDevice.valid
|
|
2200
|
+
|| headDevice.value !== String(client._deviceId ?? '').trim())
|
|
2201
|
+
return false;
|
|
2202
|
+
const explicitRowDevice = this.strictTargetString(inline, 'target_device_id');
|
|
2203
|
+
if (explicitRowDevice.present
|
|
2204
|
+
&& (!explicitRowDevice.valid || explicitRowDevice.value !== headDevice.value))
|
|
2205
|
+
return false;
|
|
2206
|
+
if (!explicitRowDevice.present) {
|
|
2207
|
+
const rowDevice = this.strictBoundString(inline, ['device_id']);
|
|
2208
|
+
if (rowDevice.present && (!rowDevice.valid || rowDevice.value !== headDevice.value))
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
const currentSlot = slotIsolationKey(String(client._slotId ?? ''));
|
|
2212
|
+
const explicitHeadSlot = this.strictTargetString(head, 'target_slot_id');
|
|
2213
|
+
if (explicitHeadSlot.present && !explicitHeadSlot.valid)
|
|
2214
|
+
return false;
|
|
2215
|
+
const headSlot = explicitHeadSlot.present
|
|
2216
|
+
? explicitHeadSlot
|
|
2217
|
+
: this.strictBoundString(head, ['slot_id']);
|
|
2218
|
+
if (headSlot.present && (!headSlot.valid || slotIsolationKey(headSlot.value) !== currentSlot))
|
|
2219
|
+
return false;
|
|
2220
|
+
const explicitRowSlot = this.strictTargetString(inline, 'target_slot_id');
|
|
2221
|
+
if (explicitRowSlot.present && (!explicitRowSlot.valid
|
|
2222
|
+
|| slotIsolationKey(explicitRowSlot.value) !== currentSlot
|
|
2223
|
+
|| (headSlot.present
|
|
2224
|
+
&& slotIsolationKey(explicitRowSlot.value) !== slotIsolationKey(headSlot.value))))
|
|
2225
|
+
return false;
|
|
2226
|
+
if (!explicitRowSlot.present) {
|
|
2227
|
+
const rowSlot = this.strictBoundString(inline, ['slot_id']);
|
|
2228
|
+
if (rowSlot.present && (!rowSlot.valid
|
|
2229
|
+
|| slotIsolationKey(rowSlot.value) !== currentSlot
|
|
2230
|
+
|| (headSlot.present
|
|
2231
|
+
&& slotIsolationKey(rowSlot.value) !== slotIsolationKey(headSlot.value))))
|
|
2232
|
+
return false;
|
|
2233
|
+
}
|
|
2234
|
+
return true;
|
|
2235
|
+
}
|
|
2236
|
+
inlineV2Envelope(inline) {
|
|
2237
|
+
const raw = inline.envelope_json;
|
|
2238
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
2239
|
+
return null;
|
|
1285
2240
|
try {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
2241
|
+
const envelope = JSON.parse(raw);
|
|
2242
|
+
return isJsonObject(envelope)
|
|
2243
|
+
? envelope
|
|
2244
|
+
: null;
|
|
2245
|
+
}
|
|
2246
|
+
catch {
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
isInlineV2Row(inline) {
|
|
2251
|
+
return inline.version === 'v2'
|
|
2252
|
+
&& typeof inline.envelope_json === 'string'
|
|
2253
|
+
&& Boolean(inline.envelope_json.trim());
|
|
2254
|
+
}
|
|
2255
|
+
inlineRecipientMatchesCurrentInstance(envelope) {
|
|
2256
|
+
if (Object.prototype.hasOwnProperty.call(envelope, 'recipients'))
|
|
2257
|
+
return false;
|
|
2258
|
+
if (!isJsonObject(envelope.recipient))
|
|
2259
|
+
return false;
|
|
2260
|
+
const recipient = envelope.recipient;
|
|
2261
|
+
const client = this.runtime.client;
|
|
2262
|
+
if (typeof recipient.aid !== 'string' || typeof recipient.device_id !== 'string'
|
|
2263
|
+
|| typeof client._aid !== 'string' || typeof client._deviceId !== 'string'
|
|
2264
|
+
|| !recipient.aid || !client._aid || !client._deviceId
|
|
2265
|
+
|| recipient.aid !== client._aid)
|
|
2266
|
+
return false;
|
|
2267
|
+
const hasDeliveryDevice = Object.prototype.hasOwnProperty.call(envelope, 'delivery_device_id');
|
|
2268
|
+
const deliveryDevice = envelope.delivery_device_id;
|
|
2269
|
+
if (hasDeliveryDevice
|
|
2270
|
+
&& (typeof deliveryDevice !== 'string' || !deliveryDevice || deliveryDevice !== client._deviceId))
|
|
2271
|
+
return false;
|
|
2272
|
+
if (recipient.device_id)
|
|
2273
|
+
return recipient.device_id === client._deviceId;
|
|
2274
|
+
return hasDeliveryDevice;
|
|
2275
|
+
}
|
|
2276
|
+
inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) {
|
|
2277
|
+
if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
|
|
2278
|
+
|| typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
|
|
2279
|
+
|| inline.version !== expectedInlineVersion
|
|
2280
|
+
|| !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
|
|
2281
|
+
|| !this.sameRequiredBinding(head, ['from_aid', 'sender_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
|
|
2282
|
+
|| !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
|
|
2283
|
+
|| !this.inlineTargetMatchesCurrentInstance(head, inline))
|
|
2284
|
+
return false;
|
|
2285
|
+
const logicalTo = this.consistentBindingValue(head, ['to_aid', 'to']);
|
|
2286
|
+
if (!logicalTo.present || !logicalTo.value)
|
|
2287
|
+
return false;
|
|
2288
|
+
if (expectedInlineVersion === 'v1') {
|
|
2289
|
+
if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
|
|
2290
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'payload')
|
|
2291
|
+
|| !isJsonObject(inline.legacy_v1))
|
|
2292
|
+
return false;
|
|
2293
|
+
const legacy = inline.legacy_v1;
|
|
2294
|
+
const payload = legacy.payload;
|
|
2295
|
+
if (payload === undefined || payload === null)
|
|
2296
|
+
return false;
|
|
2297
|
+
if (isJsonObject(payload)) {
|
|
2298
|
+
const payloadType = String(payload.type ?? '').trim();
|
|
2299
|
+
if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
2300
|
+
return false;
|
|
2301
|
+
}
|
|
2302
|
+
const legacyTo = this.strictBoundString(legacy, ['to']);
|
|
2303
|
+
return legacyTo.present && legacyTo.valid && legacyTo.value === logicalTo.value;
|
|
2304
|
+
}
|
|
2305
|
+
if (!this.isInlineV2Row(inline)
|
|
2306
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
|
|
2307
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'payload')
|
|
2308
|
+
|| !this.onlyEmptyOptionalStrings(head, ['group_id', 'group_aid'])
|
|
2309
|
+
|| !this.onlyEmptyOptionalStrings(inline, ['group_id', 'group_aid']))
|
|
2310
|
+
return false;
|
|
2311
|
+
const envelope = this.inlineV2Envelope(inline);
|
|
2312
|
+
if (!envelope
|
|
2313
|
+
|| envelope.type !== 'e2ee.p2p_encrypted'
|
|
2314
|
+
|| envelope.version !== 'v2'
|
|
2315
|
+
|| !this.onlyEmptyOptionalStrings(envelope, ['group_id'])
|
|
2316
|
+
|| !this.inlineRecipientMatchesCurrentInstance(envelope))
|
|
2317
|
+
return false;
|
|
2318
|
+
if (!isJsonObject(envelope.aad))
|
|
2319
|
+
return false;
|
|
2320
|
+
const aad = envelope.aad;
|
|
2321
|
+
if (!this.onlyEmptyOptionalStrings(aad, ['group_id']))
|
|
2322
|
+
return false;
|
|
2323
|
+
const aadMessageId = this.consistentBindingValue(aad, ['message_id']);
|
|
2324
|
+
const aadFrom = this.consistentBindingValue(aad, ['from']);
|
|
2325
|
+
const aadFromDevice = this.consistentBindingValue(aad, ['from_device']);
|
|
2326
|
+
const aadTo = this.consistentBindingValue(aad, ['to']);
|
|
2327
|
+
if (!aadMessageId.present || aadMessageId.value !== this.consistentBindingValue(inline, ['message_id']).value
|
|
2328
|
+
|| !aadFrom.present || aadFrom.value !== this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
|
|
2329
|
+
|| !aadFromDevice.present || aadFromDevice.value !== this.consistentBindingValue(inline, ['sender_device_id']).value
|
|
2330
|
+
|| !aadTo.present)
|
|
2331
|
+
return false;
|
|
2332
|
+
if (aadTo.value === logicalTo.value)
|
|
2333
|
+
return true;
|
|
2334
|
+
const recipient = envelope.recipient;
|
|
2335
|
+
return typeof recipient.role === 'string'
|
|
2336
|
+
&& recipient.role.trim() === 'self_sync'
|
|
2337
|
+
&& aadTo.value.length > 0
|
|
2338
|
+
&& aadFrom.value === String(this.runtime.client._aid ?? '').trim()
|
|
2339
|
+
&& logicalTo.value === String(this.runtime.client._aid ?? '').trim();
|
|
2340
|
+
}
|
|
2341
|
+
inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) {
|
|
2342
|
+
const headGroupId = this.strictBoundString(head, ['group_id']);
|
|
2343
|
+
const rowGroupId = this.strictBoundString(inline, ['group_id']);
|
|
2344
|
+
const headGroupAid = this.strictBoundString(head, ['group_aid']);
|
|
2345
|
+
const rowGroupAid = this.strictBoundString(inline, ['group_aid']);
|
|
2346
|
+
if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
|
|
2347
|
+
|| typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
|
|
2348
|
+
|| inline.version !== expectedInlineVersion
|
|
2349
|
+
|| !headGroupId.present || !headGroupId.valid || headGroupId.value !== groupId
|
|
2350
|
+
|| !rowGroupId.present || !rowGroupId.valid || rowGroupId.value !== groupId
|
|
2351
|
+
|| (headGroupAid.present && !headGroupAid.valid)
|
|
2352
|
+
|| (rowGroupAid.present && !rowGroupAid.valid)
|
|
2353
|
+
|| !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
|
|
2354
|
+
|| !this.sameRequiredBinding(head, ['sender_aid', 'from_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
|
|
2355
|
+
|| !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
|
|
2356
|
+
|| !this.inlineTargetMatchesCurrentInstance(head, inline))
|
|
2357
|
+
return false;
|
|
2358
|
+
if (expectedInlineVersion === 'v1') {
|
|
2359
|
+
if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
|
|
2360
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
|
|
2361
|
+
|| !Object.prototype.hasOwnProperty.call(inline, 'payload')
|
|
2362
|
+
|| inline.payload === null
|
|
2363
|
+
|| inline.payload === undefined)
|
|
2364
|
+
return false;
|
|
2365
|
+
if (isJsonObject(inline.payload)) {
|
|
2366
|
+
const payloadType = String(inline.payload.type ?? '').trim();
|
|
2367
|
+
if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
2368
|
+
return false;
|
|
2369
|
+
}
|
|
2370
|
+
return true;
|
|
2371
|
+
}
|
|
2372
|
+
if (!this.isInlineV2Row(inline)
|
|
2373
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
|
|
2374
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'payload'))
|
|
2375
|
+
return false;
|
|
2376
|
+
const envelope = this.inlineV2Envelope(inline);
|
|
2377
|
+
const envelopeGroupId = envelope ? this.strictBoundString(envelope, ['group_id']) : null;
|
|
2378
|
+
if (!envelope
|
|
2379
|
+
|| envelope.type !== 'e2ee.group_encrypted'
|
|
2380
|
+
|| envelope.version !== 'v2'
|
|
2381
|
+
|| (envelopeGroupId?.present && (!envelopeGroupId.valid || envelopeGroupId.value !== groupId))
|
|
2382
|
+
|| !this.inlineRecipientMatchesCurrentInstance(envelope))
|
|
2383
|
+
return false;
|
|
2384
|
+
if (!isJsonObject(envelope.aad))
|
|
2385
|
+
return false;
|
|
2386
|
+
const aad = envelope.aad;
|
|
2387
|
+
return this.consistentBindingValue(aad, ['message_id']).value === this.consistentBindingValue(inline, ['message_id']).value
|
|
2388
|
+
&& this.consistentBindingValue(aad, ['from']).value === this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
|
|
2389
|
+
&& this.consistentBindingValue(aad, ['from_device']).value === this.consistentBindingValue(inline, ['sender_device_id']).value
|
|
2390
|
+
&& this.consistentBindingValue(aad, ['group_id']).value === groupId;
|
|
2391
|
+
}
|
|
2392
|
+
inlineWindowReady(ns, seq) {
|
|
2393
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
2394
|
+
return false;
|
|
2395
|
+
const expected = seq - 1;
|
|
2396
|
+
const state = this.syncState(ns);
|
|
2397
|
+
if (state.ack !== expected || state.tail !== expected || state.head !== expected)
|
|
2398
|
+
return false;
|
|
2399
|
+
const client = this.runtime.client;
|
|
2400
|
+
// 入口会先登记当前 Push 的 Head,因此 maxSeen==seq 仍是合法的
|
|
2401
|
+
// 单条 inline 快路径;只有已经观察到更高序号时才退回同步路径。
|
|
2402
|
+
if (Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? expected) > seq)
|
|
2403
|
+
return false;
|
|
2404
|
+
const pending = client._pendingOrderedMsgs.get(ns);
|
|
2405
|
+
return !pending || pending.size === 0;
|
|
2406
|
+
}
|
|
2407
|
+
decodedInlineP2PMatches(head, decoded, seq) {
|
|
2408
|
+
const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
|
|
2409
|
+
const headMessageId = this.consistentBindingValue(head, ['message_id']);
|
|
2410
|
+
const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
|
|
2411
|
+
const headFrom = this.consistentBindingValue(head, ['from_aid', 'sender_aid', 'from']);
|
|
2412
|
+
const decodedTo = this.consistentBindingValue(decoded, ['to', 'to_aid']);
|
|
2413
|
+
const headTo = this.consistentBindingValue(head, ['to_aid', 'to']);
|
|
2414
|
+
return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
|
|
2415
|
+
&& decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
|
|
2416
|
+
&& decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
|
|
2417
|
+
&& decodedTo.present && headTo.present && decodedTo.value === headTo.value;
|
|
2418
|
+
}
|
|
2419
|
+
decodedInlineGroupMatches(head, decoded, groupId, seq) {
|
|
2420
|
+
const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
|
|
2421
|
+
const headMessageId = this.consistentBindingValue(head, ['message_id']);
|
|
2422
|
+
const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
|
|
2423
|
+
const headFrom = this.consistentBindingValue(head, ['sender_aid', 'from_aid', 'from']);
|
|
2424
|
+
const decodedGroupId = this.consistentBindingValue(decoded, ['group_id']);
|
|
2425
|
+
return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
|
|
2426
|
+
&& decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
|
|
2427
|
+
&& decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
|
|
2428
|
+
&& decodedGroupId.present && decodedGroupId.value === groupId;
|
|
2429
|
+
}
|
|
2430
|
+
async commitInlineMessage(ns, seq, event, payload, generation) {
|
|
2431
|
+
const client = this.runtime.client;
|
|
2432
|
+
const tracker = client._seqTracker;
|
|
2433
|
+
const snapshot = typeof tracker.snapshotNamespace === 'function'
|
|
2434
|
+
? tracker.snapshotNamespace(ns)
|
|
2435
|
+
: null;
|
|
2436
|
+
try {
|
|
2437
|
+
const published = await this.publishPulledMessage(event, ns, seq, payload);
|
|
2438
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2439
|
+
return false;
|
|
2440
|
+
if (!published)
|
|
2441
|
+
return false;
|
|
2442
|
+
const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
|
|
2443
|
+
const committed = this.syncState(ns);
|
|
2444
|
+
if (needsPull || committed.ack !== seq || committed.tail !== seq || committed.head !== seq) {
|
|
2445
|
+
throw new Error('inline realtime cursor did not collapse after consume');
|
|
2446
|
+
}
|
|
2447
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2448
|
+
return false;
|
|
2449
|
+
await client._commitSeqTrackerState(ns);
|
|
2450
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2451
|
+
return false;
|
|
2452
|
+
return true;
|
|
2453
|
+
}
|
|
2454
|
+
catch (exc) {
|
|
2455
|
+
if (this.isInlineGenerationCurrent(generation) && typeof tracker.restoreNamespaceSnapshot === 'function') {
|
|
2456
|
+
tracker.restoreNamespaceSnapshot(ns, snapshot);
|
|
2457
|
+
}
|
|
2458
|
+
client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2459
|
+
return false;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
async tryConsumeInlineP2P(head, inline, ns, seq, expectedInlineVersion, generation) {
|
|
2463
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2464
|
+
|| !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
|
|
2465
|
+
return false;
|
|
2466
|
+
const client = this.runtime.client;
|
|
2467
|
+
try {
|
|
2468
|
+
const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
|
|
2469
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2470
|
+
|| !this.inlineWindowReady(ns, seq)
|
|
2471
|
+
|| !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion)
|
|
2472
|
+
|| !this.reservePendingP2PInlineAck(ns, seq, inline, generation))
|
|
2473
|
+
return false;
|
|
2474
|
+
let committed = false;
|
|
2475
|
+
try {
|
|
2476
|
+
const decoded = await client._v2E2EE.decodeInlineP2PMessage(inline);
|
|
2477
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2478
|
+
|| !decoded || !this.decodedInlineP2PMatches(head, decoded, seq)
|
|
2479
|
+
|| !this.inlineWindowReady(ns, seq))
|
|
2480
|
+
return false;
|
|
2481
|
+
const appEvent = this.p2pAppEventForMessage(decoded);
|
|
2482
|
+
if (!await this.commitInlineMessage(ns, seq, appEvent.event, appEvent.payload, generation))
|
|
2483
|
+
return false;
|
|
2484
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2485
|
+
return false;
|
|
2486
|
+
committed = this.markPendingP2PInlineAckCommitted(ns, seq, generation);
|
|
2487
|
+
return committed;
|
|
2488
|
+
}
|
|
2489
|
+
finally {
|
|
2490
|
+
if (!committed && this.isInlineGenerationCurrent(generation))
|
|
2491
|
+
this.dropPendingP2PInlineAck(ns, seq, generation);
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
|
|
2495
|
+
return consumed;
|
|
2496
|
+
}
|
|
2497
|
+
catch (exc) {
|
|
2498
|
+
client._clientLog.debug(`inline P2P consume failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
async tryConsumeInlineGroup(head, inline, groupId, ns, seq, expectedInlineVersion, generation) {
|
|
2503
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2504
|
+
|| !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
|
|
2505
|
+
return false;
|
|
2506
|
+
const client = this.runtime.client;
|
|
2507
|
+
const groupAid = typeof head.group_aid === 'string' && head.group_aid.trim()
|
|
2508
|
+
? head.group_aid.trim()
|
|
2509
|
+
: groupId;
|
|
2510
|
+
try {
|
|
2511
|
+
const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
|
|
2512
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2513
|
+
|| !this.inlineWindowReady(ns, seq)
|
|
2514
|
+
|| !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion)
|
|
2515
|
+
|| !this.reservePendingGroupInlineAck(ns, seq, inline, groupAid, groupId, generation))
|
|
2516
|
+
return false;
|
|
2517
|
+
let committed = false;
|
|
2518
|
+
try {
|
|
2519
|
+
const decoded = await client._v2E2EE.decodeInlineGroupMessage(inline, groupId, groupAid);
|
|
2520
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2521
|
+
|| !decoded || !this.decodedInlineGroupMatches(head, decoded, groupId, seq)
|
|
2522
|
+
|| !this.inlineWindowReady(ns, seq))
|
|
2523
|
+
return false;
|
|
2524
|
+
if (!await this.commitInlineMessage(ns, seq, 'group.message_created', decoded, generation))
|
|
2525
|
+
return false;
|
|
2526
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2527
|
+
return false;
|
|
2528
|
+
committed = this.markPendingGroupInlineAckCommitted(ns, seq, generation);
|
|
2529
|
+
return committed;
|
|
2530
|
+
}
|
|
2531
|
+
finally {
|
|
2532
|
+
if (!committed && this.isInlineGenerationCurrent(generation))
|
|
2533
|
+
this.dropPendingGroupInlineAck(ns, seq, generation);
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
|
|
2537
|
+
return consumed;
|
|
2538
|
+
}
|
|
2539
|
+
catch (exc) {
|
|
2540
|
+
client._clientLog.debug(`inline Group consume failed: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
hasOutstandingRealtimeGap(ns) {
|
|
2545
|
+
const state = this.syncState(ns);
|
|
2546
|
+
// Tail 已建立可靠后缀窗口;后台续拉只针对尚未接上窗口的真实缺口。
|
|
2547
|
+
// maxSeen 可能只是 Tail 期间到达的通知,不能单独据此启动全量补洞。
|
|
2548
|
+
return state.ack < state.tail - 1;
|
|
2549
|
+
}
|
|
2550
|
+
scheduleP2PRealtimeFullGapFill(ns) {
|
|
2551
|
+
const client = this.runtime.client;
|
|
2552
|
+
const currentNs = client._aid ? `p2p:${client._aid}` : '';
|
|
2553
|
+
if (client._sessionOptions?.background_sync === false
|
|
2554
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2555
|
+
|| currentNs !== ns || !this.hasOutstandingRealtimeGap(ns))
|
|
2556
|
+
return;
|
|
2557
|
+
const scheduleKey = `p2p_realtime_full_gap:${ns}`;
|
|
2558
|
+
if (client._gapFillDone.has(scheduleKey))
|
|
2559
|
+
return;
|
|
2560
|
+
client._gapFillDone.add(scheduleKey);
|
|
2561
|
+
client._safeAsync(Promise.resolve().then(async () => {
|
|
2562
|
+
try {
|
|
2563
|
+
const liveNs = client._aid ? `p2p:${client._aid}` : '';
|
|
2564
|
+
if (client._sessionOptions?.background_sync === false
|
|
2565
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2566
|
+
|| liveNs !== ns || !this.hasOutstandingRealtimeGap(ns))
|
|
1290
2567
|
return;
|
|
1291
|
-
|
|
1292
|
-
|
|
2568
|
+
await this.fillP2pGap();
|
|
2569
|
+
}
|
|
2570
|
+
finally {
|
|
2571
|
+
client._gapFillDone.delete(scheduleKey);
|
|
2572
|
+
}
|
|
2573
|
+
}));
|
|
2574
|
+
}
|
|
2575
|
+
scheduleGroupRealtimeFullGapFill(groupId, ns) {
|
|
2576
|
+
const client = this.runtime.client;
|
|
2577
|
+
groupId = String(groupId ?? '').trim();
|
|
2578
|
+
if (!groupId || client._sessionOptions?.background_sync === false
|
|
2579
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2580
|
+
|| !this.hasOutstandingRealtimeGap(ns))
|
|
2581
|
+
return;
|
|
2582
|
+
const scheduleKey = `group_realtime_full_gap:${ns}`;
|
|
2583
|
+
if (client._gapFillDone.has(scheduleKey))
|
|
2584
|
+
return;
|
|
2585
|
+
client._gapFillDone.add(scheduleKey);
|
|
2586
|
+
client._safeAsync(Promise.resolve().then(async () => {
|
|
2587
|
+
try {
|
|
2588
|
+
if (client._sessionOptions?.background_sync === false
|
|
2589
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2590
|
+
|| !this.hasOutstandingRealtimeGap(ns))
|
|
1293
2591
|
return;
|
|
1294
|
-
|
|
1295
|
-
client._onlineUnreadHintQueue.delete(groupId);
|
|
1296
|
-
if (!payload)
|
|
1297
|
-
continue;
|
|
1298
|
-
await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
|
|
1299
|
-
const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
|
|
1300
|
-
if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
|
|
1301
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1302
|
-
}
|
|
2592
|
+
await this.fillGroupGap(groupId);
|
|
1303
2593
|
}
|
|
2594
|
+
finally {
|
|
2595
|
+
client._gapFillDone.delete(scheduleKey);
|
|
2596
|
+
}
|
|
2597
|
+
}));
|
|
2598
|
+
}
|
|
2599
|
+
async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
2600
|
+
const client = this.runtime.client;
|
|
2601
|
+
const before = this.syncState(ns);
|
|
2602
|
+
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
2603
|
+
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
2604
|
+
: (() => {
|
|
2605
|
+
const gap = pushSeq - before.ack;
|
|
2606
|
+
if (gap <= 0)
|
|
2607
|
+
return before.ack < before.tail - 1 ? ['forward'] : [];
|
|
2608
|
+
return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
|
|
2609
|
+
})();
|
|
2610
|
+
// Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
|
|
2611
|
+
const headForward = order[0] === 'forward' && pushSeq > before.ack;
|
|
2612
|
+
let result = { messages: [], raw_count: 0 };
|
|
2613
|
+
let tailCompleted = false;
|
|
2614
|
+
const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
|
|
2615
|
+
const live = this.syncState(ns);
|
|
2616
|
+
if (tail && !bypassCoveredHead && pushSeq <= live.head)
|
|
2617
|
+
return { messages: [], raw_count: 0 };
|
|
2618
|
+
if (!tail && !forceForward && live.ack >= live.tail - 1)
|
|
2619
|
+
return { messages: [], raw_count: 0 };
|
|
2620
|
+
const request = { ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
|
|
2621
|
+
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
|
|
2622
|
+
return await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
2623
|
+
if (tail) {
|
|
2624
|
+
const tailResult = await client._v2E2EE.pullV2TailInternal({
|
|
2625
|
+
window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
|
|
2626
|
+
}, true);
|
|
2627
|
+
tailCompleted = true;
|
|
2628
|
+
return tailResult;
|
|
2629
|
+
}
|
|
2630
|
+
const messages = await client._pullV2(live.ack, pageLimit, {
|
|
2631
|
+
gateLocked: true,
|
|
2632
|
+
...(maxPages ? { maxPages } : {}),
|
|
2633
|
+
});
|
|
2634
|
+
return { messages, raw_count: messages.length };
|
|
2635
|
+
}, background);
|
|
2636
|
+
};
|
|
2637
|
+
if (forceTail || order[0] === 'tail')
|
|
2638
|
+
result = await run(true, false, undefined, false, forceTail);
|
|
2639
|
+
const state = this.syncState(ns);
|
|
2640
|
+
// Tail 的连续性证明只覆盖其返回的 head。Tail 进行期间到达的新
|
|
2641
|
+
// Push 不能再次进入 realtime planner(那会再做一次 Tail);以已经
|
|
2642
|
+
// 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
|
|
2643
|
+
const tailMissedDuringPull = tailCompleted
|
|
2644
|
+
&& Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
|
|
2645
|
+
const pendingThroughSeq = tailMissedDuringPull
|
|
2646
|
+
? Number(client._seqTracker.getMaxSeenSeq(ns) || 0)
|
|
2647
|
+
: 0;
|
|
2648
|
+
const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
|
|
2649
|
+
const backgroundWindowForward = order[0] === 'forward' && !headForward;
|
|
2650
|
+
if (headForward || tailForward
|
|
2651
|
+
|| (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
|
|
2652
|
+
result = await run(false, !(headForward || tailForward), headForward ? 2 : order[0] === 'tail' ? 1 : undefined, headForward || tailMissedDuringPull);
|
|
2653
|
+
if (pendingThroughSeq > 0)
|
|
2654
|
+
this.consumePendingPull(ns, pendingThroughSeq);
|
|
2655
|
+
}
|
|
2656
|
+
const finalState = this.syncState(ns);
|
|
2657
|
+
if (tailCompleted && finalState.ack < finalState.tail - 1) {
|
|
2658
|
+
this.scheduleP2PRealtimeFullGapFill(ns);
|
|
2659
|
+
}
|
|
2660
|
+
if (tailCompleted)
|
|
2661
|
+
this.markRealtimeTailCompleted(result);
|
|
2662
|
+
return result;
|
|
2663
|
+
}
|
|
2664
|
+
async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
2665
|
+
const client = this.runtime.client;
|
|
2666
|
+
const before = this.syncState(ns);
|
|
2667
|
+
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
2668
|
+
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
2669
|
+
: (() => {
|
|
2670
|
+
const gap = pushSeq - before.ack;
|
|
2671
|
+
if (gap <= 0)
|
|
2672
|
+
return before.ack < before.tail - 1 ? ['forward'] : [];
|
|
2673
|
+
return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
|
|
2674
|
+
})();
|
|
2675
|
+
// Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
|
|
2676
|
+
const headForward = order[0] === 'forward' && pushSeq > before.ack;
|
|
2677
|
+
let result = { messages: [], raw_count: 0 };
|
|
2678
|
+
let tailCompleted = false;
|
|
2679
|
+
const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
|
|
2680
|
+
const live = this.syncState(ns);
|
|
2681
|
+
if (tail && !bypassCoveredHead && pushSeq <= live.head)
|
|
2682
|
+
return { messages: [], raw_count: 0 };
|
|
2683
|
+
if (!tail && !forceForward && live.ack >= live.tail - 1)
|
|
2684
|
+
return { messages: [], raw_count: 0 };
|
|
2685
|
+
const request = { group_id: groupId, ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
|
|
2686
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
2687
|
+
return await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
2688
|
+
if (tail) {
|
|
2689
|
+
const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
|
|
2690
|
+
group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
|
|
2691
|
+
}, true);
|
|
2692
|
+
tailCompleted = true;
|
|
2693
|
+
return tailResult;
|
|
2694
|
+
}
|
|
2695
|
+
const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
|
|
2696
|
+
gateLocked: true,
|
|
2697
|
+
...(maxPages ? { maxPages } : {}),
|
|
2698
|
+
});
|
|
2699
|
+
return { messages, raw_count: messages.length };
|
|
2700
|
+
}, background);
|
|
2701
|
+
};
|
|
2702
|
+
if (forceTail || order[0] === 'tail')
|
|
2703
|
+
result = await run(true, false, undefined, false, forceTail);
|
|
2704
|
+
const state = this.syncState(ns);
|
|
2705
|
+
// 与 P2P 相同:真实完成 Tail 后,至多用一页 Forward 收敛 Tail
|
|
2706
|
+
// 期间新到达的序号;绝不重新规划或再执行 Tail。
|
|
2707
|
+
const tailMissedDuringPull = tailCompleted
|
|
2708
|
+
&& Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
|
|
2709
|
+
const pendingThroughSeq = tailMissedDuringPull
|
|
2710
|
+
? Number(client._seqTracker.getMaxSeenSeq(ns) || 0)
|
|
2711
|
+
: 0;
|
|
2712
|
+
const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
|
|
2713
|
+
const backgroundWindowForward = order[0] === 'forward' && !headForward;
|
|
2714
|
+
if (headForward || tailForward
|
|
2715
|
+
|| (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
|
|
2716
|
+
result = await run(false, !(headForward || tailForward), headForward ? 2 : order[0] === 'tail' ? 1 : undefined, headForward || tailMissedDuringPull);
|
|
2717
|
+
if (pendingThroughSeq > 0)
|
|
2718
|
+
this.consumePendingPull(ns, pendingThroughSeq);
|
|
2719
|
+
}
|
|
2720
|
+
const finalState = this.syncState(ns);
|
|
2721
|
+
if (tailCompleted && finalState.ack < finalState.tail - 1) {
|
|
2722
|
+
this.scheduleGroupRealtimeFullGapFill(groupId, ns);
|
|
2723
|
+
}
|
|
2724
|
+
if (tailCompleted)
|
|
2725
|
+
this.markRealtimeTailCompleted(result);
|
|
2726
|
+
return result;
|
|
2727
|
+
}
|
|
2728
|
+
async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
|
|
2729
|
+
const client = this.runtime.client;
|
|
2730
|
+
client._clientLog.warn(`V2 P2P inline Head has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
|
|
2731
|
+
if (!this.beginRealtimeSync(ns))
|
|
2732
|
+
return;
|
|
2733
|
+
try {
|
|
2734
|
+
await this.runP2PRealtimeSync(ns, this.syncState(ns).head, 50, true);
|
|
1304
2735
|
}
|
|
1305
2736
|
catch (exc) {
|
|
1306
|
-
client._clientLog.
|
|
2737
|
+
client._clientLog.warn(`V2 P2P invalid-seq Tail failed: ${formatDeliveryError(exc)}`);
|
|
1307
2738
|
}
|
|
1308
2739
|
finally {
|
|
1309
|
-
this.
|
|
2740
|
+
this.endRealtimeSync(ns);
|
|
1310
2741
|
}
|
|
1311
2742
|
}
|
|
1312
|
-
async
|
|
2743
|
+
async recoverInvalidGroupRealtimeHead(groupId, ns, rawSeq) {
|
|
1313
2744
|
const client = this.runtime.client;
|
|
1314
|
-
|
|
1315
|
-
if (!
|
|
1316
|
-
client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: non-object type=${typeof data}`);
|
|
2745
|
+
client._clientLog.warn(`V2 Group inline Head has invalid seq; forcing authoritative Tail: group=${groupId} seq=${String(rawSeq)}`);
|
|
2746
|
+
if (!this.beginRealtimeSync(ns))
|
|
1317
2747
|
return;
|
|
2748
|
+
try {
|
|
2749
|
+
await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, 50, true);
|
|
1318
2750
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const messageId = String(data.message_id ?? '').trim();
|
|
1322
|
-
const senderAid = String(data.sender_aid ?? '').trim();
|
|
1323
|
-
client._clientLog.debug(`_onRawGroupV2MessageCreated group=${groupId} seq=${seq} message_id=${messageId} sender=${senderAid}`);
|
|
1324
|
-
if (!groupId || seq <= 0) {
|
|
1325
|
-
client._clientLog.debug('_onRawGroupV2MessageCreated skipped: missing group_id or seq');
|
|
1326
|
-
return;
|
|
2751
|
+
catch (exc) {
|
|
2752
|
+
client._clientLog.warn(`V2 Group invalid-seq Tail failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
1327
2753
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
2754
|
+
finally {
|
|
2755
|
+
this.endRealtimeSync(ns);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
async onRawGroupV2MessageCreated(data) {
|
|
2759
|
+
const client = this.runtime.client;
|
|
2760
|
+
const row = isJsonObject(data) ? data : null;
|
|
2761
|
+
if (!row)
|
|
2762
|
+
return;
|
|
2763
|
+
const groupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
|
|
2764
|
+
if (!groupId)
|
|
1330
2765
|
return;
|
|
2766
|
+
const seq = typeof row.seq === 'number' ? row.seq : 0;
|
|
2767
|
+
const eventKind = String(row.kind ?? '').trim();
|
|
2768
|
+
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
2769
|
+
if (client._v2Session && eventKind !== 'group.online_unread_hint'
|
|
2770
|
+
&& Number.isSafeInteger(seq) && seq > 0) {
|
|
2771
|
+
// 必须在进入 namespace 队列前登记,才能让在途 Tail 看见后来 Push。
|
|
2772
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
1331
2773
|
}
|
|
2774
|
+
const hasInlineObject = isJsonObject(row.inline_message)
|
|
2775
|
+
&& eventKind !== 'group.online_unread_hint';
|
|
2776
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
2777
|
+
const operation = () => this.processGroupV2MessageCreated(data, 'v2', inlineGeneration);
|
|
2778
|
+
if (hasInlineObject)
|
|
2779
|
+
await this.runPushProcessSerialized(ns, operation, inlineGeneration);
|
|
2780
|
+
else
|
|
2781
|
+
await operation();
|
|
2782
|
+
}
|
|
2783
|
+
async processGroupV2MessageCreated(data, expectedInlineVersion, queuedInlineGeneration) {
|
|
2784
|
+
if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
|
|
2785
|
+
return;
|
|
2786
|
+
const client = this.runtime.client;
|
|
2787
|
+
if (!isJsonObject(data) || !client._v2Session)
|
|
2788
|
+
return;
|
|
2789
|
+
const groupId = typeof data.group_id === 'string' ? data.group_id.trim() : '';
|
|
2790
|
+
const seq = positiveSafeSequenceHint(data.seq);
|
|
2791
|
+
const strictSeq = typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
|
|
2792
|
+
if (!groupId)
|
|
2793
|
+
return;
|
|
1332
2794
|
const eventKind = String(data.kind ?? '').trim();
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
2795
|
+
// Hint 是恢复信号而不是消息 Head:不得消费 inline、推进水位或直接 ACK。
|
|
2796
|
+
if (eventKind === 'group.online_unread_hint') {
|
|
2797
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1336
2798
|
return;
|
|
1337
|
-
}
|
|
1338
2799
|
this.enqueueOnlineUnreadHint(data);
|
|
1339
2800
|
return;
|
|
1340
2801
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
2802
|
+
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
2803
|
+
const inlinePresent = Object.prototype.hasOwnProperty.call(data, 'inline_message');
|
|
2804
|
+
if (seq <= 0) {
|
|
2805
|
+
if (inlinePresent)
|
|
2806
|
+
await this.recoverInvalidGroupRealtimeHead(groupId, ns, data.seq);
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
const inline = this.inlineMessage(data);
|
|
2810
|
+
const inlineMatches = inline !== null
|
|
2811
|
+
&& this.inlineGroupBindingMatches(data, inline, groupId, seq, expectedInlineVersion);
|
|
2812
|
+
const pendingInline = this.pendingGroupInlineAcks?.get(ns);
|
|
2813
|
+
const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== seq;
|
|
2814
|
+
const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
|
|
2815
|
+
if (inlineMatches && !pendingConflict
|
|
2816
|
+
&& await this.tryConsumeInlineGroup(data, inline, groupId, ns, seq, expectedInlineVersion, inlineGeneration)) {
|
|
2817
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
2818
|
+
if (pending)
|
|
2819
|
+
this.retryPendingGroupInlineAck(ns, pending.seq, 'inline Group ack');
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
|
|
2823
|
+
return;
|
|
2824
|
+
const before = this.syncState(ns);
|
|
2825
|
+
// 无正文的旧服务端 Head 可把 JSON-safe 数字字符串作为恢复 hint;
|
|
2826
|
+
// inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
|
|
2827
|
+
if (strictSeq || !inlinePresent)
|
|
1343
2828
|
client._seqTracker.updateMaxSeen(ns, seq);
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
2829
|
+
if (seq <= before.ack && before.ack >= before.tail - 1) {
|
|
2830
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
2831
|
+
const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
|
|
2832
|
+
if (pending && seq >= pending.seq) {
|
|
2833
|
+
this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
|
|
1349
2834
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if (client._gapFillDone.has(dedupKey)) {
|
|
1353
|
-
client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: dedupKey=${dedupKey} in flight`);
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
client._gapFillDone.add(dedupKey);
|
|
1357
|
-
try {
|
|
1358
|
-
client._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
|
|
1359
|
-
const messages = await client._withBackgroundRpc(() => client._pullGroupV2(groupId, afterSeq, 50));
|
|
1360
|
-
client._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
|
|
1361
|
-
}
|
|
1362
|
-
finally {
|
|
1363
|
-
client._gapFillDone.delete(dedupKey);
|
|
2835
|
+
else if (!inlinePresent || inlineMatches) {
|
|
2836
|
+
this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
|
|
1364
2837
|
}
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
2841
|
+
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
2842
|
+
this.recordPendingPull(ns, seq);
|
|
2843
|
+
client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
try {
|
|
2847
|
+
// namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
|
|
2848
|
+
await this.runGroupRealtimeSync(groupId, ns, seq, 50, false);
|
|
1365
2849
|
}
|
|
1366
2850
|
catch (exc) {
|
|
1367
|
-
client._clientLog.warn(`
|
|
2851
|
+
client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
1368
2852
|
}
|
|
1369
2853
|
finally {
|
|
1370
|
-
|
|
2854
|
+
this.endRealtimeSync(ns);
|
|
2855
|
+
this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
1371
2856
|
}
|
|
1372
2857
|
}
|
|
1373
2858
|
async onV2PushNotification(data) {
|
|
1374
2859
|
const client = this.runtime.client;
|
|
2860
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
2861
|
+
if (!ns)
|
|
2862
|
+
return;
|
|
2863
|
+
if (isJsonObject(data) && String(data.kind ?? '').trim() === 'message.online_unread_hint') {
|
|
2864
|
+
if (client._sessionOptions?.background_sync === false)
|
|
2865
|
+
return;
|
|
2866
|
+
this.enqueueOnlineUnreadP2pHint(data);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
|
|
2870
|
+
if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
|
|
2871
|
+
// 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
|
|
2872
|
+
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
2873
|
+
}
|
|
2874
|
+
const hasInlineObject = isJsonObject(data)
|
|
2875
|
+
&& isJsonObject(data.inline_message);
|
|
2876
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
2877
|
+
const operation = () => this.processV2P2PNotification(data, 'v2', inlineGeneration);
|
|
2878
|
+
if (hasInlineObject)
|
|
2879
|
+
await this.runPushProcessSerialized(ns, operation, inlineGeneration);
|
|
2880
|
+
else
|
|
2881
|
+
await operation();
|
|
2882
|
+
}
|
|
2883
|
+
async processV2P2PNotification(data, expectedInlineVersion, queuedInlineGeneration) {
|
|
2884
|
+
if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
|
|
2885
|
+
return;
|
|
2886
|
+
const client = this.runtime.client;
|
|
1375
2887
|
if (!client._v2Session)
|
|
1376
2888
|
return;
|
|
1377
|
-
const pushSeq = isJsonObject(data) ?
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
const envelopeJson = isJsonObject(data) ? data.envelope_json : undefined;
|
|
1381
|
-
const hasPayload = !!envelopeJson;
|
|
2889
|
+
const pushSeq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
|
|
2890
|
+
const strictSeq = isJsonObject(data)
|
|
2891
|
+
&& typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
|
|
1382
2892
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
if (
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
}
|
|
1391
|
-
contigBefore = client._repairPushContiguousBound(ns, pushSeq, hasPayload, '_raw.peer.v2.message_received');
|
|
2893
|
+
if (!ns)
|
|
2894
|
+
return;
|
|
2895
|
+
const inlinePresent = isJsonObject(data) && Object.prototype.hasOwnProperty.call(data, 'inline_message');
|
|
2896
|
+
if (pushSeq <= 0) {
|
|
2897
|
+
if (inlinePresent)
|
|
2898
|
+
await this.recoverInvalidP2PRealtimeHead(ns, isJsonObject(data) ? data.seq : undefined);
|
|
2899
|
+
return;
|
|
1392
2900
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
if (!needPull && (published || newContig >= pushSeq || pushSeq <= contigBefore)) {
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
client._clientLog.debug(`_onV2PushNotification: payload push seq=${pushSeq} 因空洞挂起,继续 pull 补齐 after_seq=${newContig}`);
|
|
1417
|
-
}
|
|
2901
|
+
const inline = isJsonObject(data) ? this.inlineMessage(data) : null;
|
|
2902
|
+
const inlineMatches = inline !== null
|
|
2903
|
+
&& this.inlineP2PBindingMatches(data, inline, pushSeq, expectedInlineVersion);
|
|
2904
|
+
const pendingInline = this.pendingP2PInlineAcks?.get(ns);
|
|
2905
|
+
const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== pushSeq;
|
|
2906
|
+
const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
|
|
2907
|
+
if (inlineMatches && !pendingConflict
|
|
2908
|
+
&& await this.tryConsumeInlineP2P(data, inline, ns, pushSeq, expectedInlineVersion, inlineGeneration)) {
|
|
2909
|
+
this.retryPendingP2PInlineAck(ns, pushSeq, 'inline P2P ack');
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
|
|
2913
|
+
return;
|
|
2914
|
+
const before = this.syncState(ns);
|
|
2915
|
+
if (strictSeq || !inlinePresent)
|
|
2916
|
+
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
2917
|
+
if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
|
|
2918
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2919
|
+
if (pending && pushSeq >= pending.seq) {
|
|
2920
|
+
this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
|
|
1418
2921
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
2922
|
+
else if (inlineMatches) {
|
|
2923
|
+
this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
|
|
1421
2924
|
}
|
|
2925
|
+
return;
|
|
1422
2926
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
this.runtime.delivery.setV2PullPending(true);
|
|
2927
|
+
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
2928
|
+
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
2929
|
+
this.recordPendingPull(ns, pushSeq);
|
|
2930
|
+
client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
|
|
1428
2931
|
return;
|
|
1429
2932
|
}
|
|
1430
|
-
this.runtime.delivery.setV2PullInflight(true);
|
|
1431
|
-
const dedupKey = `p2p_pull:${ns}`;
|
|
1432
|
-
client._gapFillDone.add(dedupKey);
|
|
1433
2933
|
try {
|
|
1434
|
-
|
|
1435
|
-
this.runtime.delivery.setV2PullPending(false);
|
|
1436
|
-
await client._withBackgroundRpc(() => client._pullV2());
|
|
1437
|
-
const newContig = ns ? client._seqTracker.getContiguousSeq(ns) : -1;
|
|
1438
|
-
client._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
|
|
1439
|
-
} while (client._v2PullPending);
|
|
2934
|
+
await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
|
|
1440
2935
|
}
|
|
1441
2936
|
catch (exc) {
|
|
1442
|
-
|
|
1443
|
-
client._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${exc}`);
|
|
2937
|
+
client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
|
|
1444
2938
|
}
|
|
1445
2939
|
finally {
|
|
1446
|
-
this.
|
|
1447
|
-
|
|
2940
|
+
this.endRealtimeSync(ns);
|
|
2941
|
+
this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
1448
2942
|
}
|
|
1449
2943
|
}
|
|
2944
|
+
enqueueOnlineUnreadP2pHint(data) {
|
|
2945
|
+
const client = this.runtime.client;
|
|
2946
|
+
if (!client._aid || !client._v2Session)
|
|
2947
|
+
return;
|
|
2948
|
+
const ns = `p2p:${client._aid}`;
|
|
2949
|
+
client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
|
|
2950
|
+
client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2951
|
+
}));
|
|
2952
|
+
}
|
|
1450
2953
|
async restoreSeqTrackerState() {
|
|
1451
2954
|
const client = this.runtime.client;
|
|
1452
2955
|
if (!client._aid)
|
|
@@ -1458,6 +2961,17 @@ export class MessageDeliveryEngine {
|
|
|
1458
2961
|
const deviceId = client._deviceId;
|
|
1459
2962
|
const slotId = client._slotId;
|
|
1460
2963
|
try {
|
|
2964
|
+
const loadSync = client._tokenStore.loadAllSyncStates?.bind(client._tokenStore);
|
|
2965
|
+
if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
|
|
2966
|
+
let state = await loadSync(aid, deviceId, slotId);
|
|
2967
|
+
if (client._seqTrackerContext !== context)
|
|
2968
|
+
return;
|
|
2969
|
+
if (state && typeof state === 'object' && Object.keys(state).length > 0) {
|
|
2970
|
+
state = await this.migrateSeqStateGroupIds(state);
|
|
2971
|
+
client._seqTracker.restoreSyncState(state);
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
1461
2975
|
const loadAll = client._tokenStore.loadAllSeqs?.bind(client._tokenStore);
|
|
1462
2976
|
if (typeof loadAll === 'function') {
|
|
1463
2977
|
let state = await loadAll(aid, deviceId, slotId);
|
|
@@ -1494,71 +3008,7 @@ export class MessageDeliveryEngine {
|
|
|
1494
3008
|
}
|
|
1495
3009
|
}
|
|
1496
3010
|
async migrateSeqStateGroupIds(state) {
|
|
1497
|
-
|
|
1498
|
-
if (!state || Object.keys(state).length === 0)
|
|
1499
|
-
return state;
|
|
1500
|
-
const renameMap = {};
|
|
1501
|
-
for (const ns of Object.keys(state)) {
|
|
1502
|
-
for (const prefix of ['group_event:', 'group_msg:']) {
|
|
1503
|
-
if (ns.startsWith(prefix)) {
|
|
1504
|
-
const oldGid = ns.slice(prefix.length);
|
|
1505
|
-
const newGid = normalizeGroupId(oldGid);
|
|
1506
|
-
if (newGid && newGid !== oldGid) {
|
|
1507
|
-
renameMap[ns] = `${prefix}${newGid}`;
|
|
1508
|
-
}
|
|
1509
|
-
break;
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
if (Object.keys(renameMap).length === 0)
|
|
1514
|
-
return state;
|
|
1515
|
-
const newState = { ...state };
|
|
1516
|
-
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
1517
|
-
const oldVal = Number(newState[oldNs] ?? 0);
|
|
1518
|
-
const curVal = Number(newState[newNs] ?? 0);
|
|
1519
|
-
delete newState[oldNs];
|
|
1520
|
-
newState[newNs] = Math.max(oldVal, curVal);
|
|
1521
|
-
}
|
|
1522
|
-
if (!client._aid)
|
|
1523
|
-
return newState;
|
|
1524
|
-
const aid = client._aid;
|
|
1525
|
-
const deviceId = client._deviceId;
|
|
1526
|
-
const slotId = client._slotId;
|
|
1527
|
-
const saver = client._tokenStore.saveSeq?.bind(client._tokenStore);
|
|
1528
|
-
const deleter = client._tokenStore.deleteSeq?.bind(client._tokenStore);
|
|
1529
|
-
if (typeof saver === 'function') {
|
|
1530
|
-
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
1531
|
-
if (typeof deleter === 'function') {
|
|
1532
|
-
try {
|
|
1533
|
-
await deleter(aid, deviceId, slotId, oldNs);
|
|
1534
|
-
}
|
|
1535
|
-
catch (e) {
|
|
1536
|
-
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
1537
|
-
phase: 'migrate_delete',
|
|
1538
|
-
aid,
|
|
1539
|
-
device_id: deviceId,
|
|
1540
|
-
slot_id: slotId,
|
|
1541
|
-
ns: oldNs,
|
|
1542
|
-
error: String(e),
|
|
1543
|
-
}).catch(() => { });
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
try {
|
|
1547
|
-
await saver(aid, deviceId, slotId, newNs, newState[newNs]);
|
|
1548
|
-
}
|
|
1549
|
-
catch (e) {
|
|
1550
|
-
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
1551
|
-
phase: 'migrate_save',
|
|
1552
|
-
aid,
|
|
1553
|
-
device_id: deviceId,
|
|
1554
|
-
slot_id: slotId,
|
|
1555
|
-
ns: newNs,
|
|
1556
|
-
error: String(e),
|
|
1557
|
-
}).catch(() => { });
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
return newState;
|
|
3011
|
+
return state;
|
|
1562
3012
|
}
|
|
1563
3013
|
async saveSeqTrackerState() {
|
|
1564
3014
|
const client = this.runtime.client;
|
|
@@ -1567,7 +3017,9 @@ export class MessageDeliveryEngine {
|
|
|
1567
3017
|
await this.flushSeqTrackerPending();
|
|
1568
3018
|
return;
|
|
1569
3019
|
}
|
|
1570
|
-
const state = client._seqTracker.
|
|
3020
|
+
const state = typeof client._seqTracker.exportSyncState === 'function'
|
|
3021
|
+
? client._seqTracker.exportSyncState()
|
|
3022
|
+
: Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
|
|
1571
3023
|
const pending = this.seqTrackerPending();
|
|
1572
3024
|
if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
|
|
1573
3025
|
const liveNamespaces = new Set(Object.keys(state));
|
|
@@ -1595,9 +3047,34 @@ export class MessageDeliveryEngine {
|
|
|
1595
3047
|
if (forceSeq !== undefined) {
|
|
1596
3048
|
client._seqTracker.forceContiguousSeq(ns, seq);
|
|
1597
3049
|
}
|
|
1598
|
-
|
|
3050
|
+
const state = typeof client._seqTracker.getSyncState === 'function'
|
|
3051
|
+
? client._seqTracker.getSyncState(ns)
|
|
3052
|
+
: { ack: seq, tail: seq, head: seq };
|
|
3053
|
+
this.mergeSeqTrackerPending(context, { [ns]: state });
|
|
1599
3054
|
this.scheduleSeqTrackerFlush();
|
|
1600
3055
|
}
|
|
3056
|
+
async commitSeqTrackerState(ns) {
|
|
3057
|
+
const client = this.runtime.client;
|
|
3058
|
+
const context = this.currentSeqTrackerContext();
|
|
3059
|
+
if (!context || !ns)
|
|
3060
|
+
return;
|
|
3061
|
+
const current = client._seqTracker.getContiguousSeq(ns);
|
|
3062
|
+
const state = typeof client._seqTracker.getSyncState === 'function'
|
|
3063
|
+
? client._seqTracker.getSyncState(ns)
|
|
3064
|
+
: { ack: current, tail: current, head: current };
|
|
3065
|
+
if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
|
|
3066
|
+
return;
|
|
3067
|
+
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
3068
|
+
const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
|
|
3069
|
+
try {
|
|
3070
|
+
await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
|
|
3071
|
+
}
|
|
3072
|
+
catch (exc) {
|
|
3073
|
+
this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
|
|
3074
|
+
throw exc;
|
|
3075
|
+
}
|
|
3076
|
+
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
3077
|
+
}
|
|
1601
3078
|
async flushSeqTrackerPending() {
|
|
1602
3079
|
const client = this.runtime.client;
|
|
1603
3080
|
this.cancelSeqTrackerFlushTimer();
|
|
@@ -1622,6 +3099,34 @@ export class MessageDeliveryEngine {
|
|
|
1622
3099
|
this.cancelSeqTrackerFlushTimer();
|
|
1623
3100
|
}
|
|
1624
3101
|
}
|
|
3102
|
+
takeSeqTrackerPending(context, ns) {
|
|
3103
|
+
const client = this.runtime.client;
|
|
3104
|
+
if (client._seqTrackerPendingPersistContext !== context)
|
|
3105
|
+
return null;
|
|
3106
|
+
const pending = this.seqTrackerPending();
|
|
3107
|
+
const state = pending.get(ns);
|
|
3108
|
+
if (!state)
|
|
3109
|
+
return null;
|
|
3110
|
+
pending.delete(ns);
|
|
3111
|
+
if (pending.size === 0) {
|
|
3112
|
+
client._seqTrackerPendingPersistContext = null;
|
|
3113
|
+
this.cancelSeqTrackerFlushTimer();
|
|
3114
|
+
}
|
|
3115
|
+
return { ...state };
|
|
3116
|
+
}
|
|
3117
|
+
restoreSeqTrackerPending(context, ns, state) {
|
|
3118
|
+
if (!state || this.currentSeqTrackerContext() !== context)
|
|
3119
|
+
return;
|
|
3120
|
+
const client = this.runtime.client;
|
|
3121
|
+
const pendingContext = client._seqTrackerPendingPersistContext;
|
|
3122
|
+
if (pendingContext && pendingContext !== context)
|
|
3123
|
+
return;
|
|
3124
|
+
const pending = this.seqTrackerPending();
|
|
3125
|
+
if (!pending.has(ns))
|
|
3126
|
+
pending.set(ns, state);
|
|
3127
|
+
client._seqTrackerPendingPersistContext = context;
|
|
3128
|
+
this.scheduleSeqTrackerFlush();
|
|
3129
|
+
}
|
|
1625
3130
|
currentSeqTrackerContext() {
|
|
1626
3131
|
const client = this.runtime.client;
|
|
1627
3132
|
if (!client._aid)
|
|
@@ -1643,10 +3148,12 @@ export class MessageDeliveryEngine {
|
|
|
1643
3148
|
void this.flushSeqTrackerPending();
|
|
1644
3149
|
}
|
|
1645
3150
|
client._seqTrackerPendingPersistContext = context;
|
|
1646
|
-
for (const [ns,
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
|
|
3151
|
+
for (const [ns, raw] of Object.entries(state)) {
|
|
3152
|
+
const ack = Number(raw.ack);
|
|
3153
|
+
const tail = Number(raw.tail);
|
|
3154
|
+
const head = Number(raw.head);
|
|
3155
|
+
if (ns && [ack, tail, head].every(Number.isFinite) && Math.max(ack, tail, head) > 0) {
|
|
3156
|
+
pending.set(ns, { ack, tail, head });
|
|
1650
3157
|
}
|
|
1651
3158
|
}
|
|
1652
3159
|
}
|
|
@@ -1669,14 +3176,27 @@ export class MessageDeliveryEngine {
|
|
|
1669
3176
|
client._seqTrackerFlushTimer = null;
|
|
1670
3177
|
}
|
|
1671
3178
|
}
|
|
1672
|
-
async writeSeqTrackerState(aid, deviceId, slotId, state) {
|
|
3179
|
+
async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
3180
|
+
await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
|
|
3181
|
+
}
|
|
3182
|
+
enqueueSeqTrackerWrite(operation) {
|
|
3183
|
+
const run = this.seqTrackerWriteChain.then(operation, operation);
|
|
3184
|
+
this.seqTrackerWriteChain = run.catch(() => { });
|
|
3185
|
+
return run;
|
|
3186
|
+
}
|
|
3187
|
+
async writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError = false) {
|
|
1673
3188
|
const client = this.runtime.client;
|
|
1674
3189
|
if (!aid || Object.keys(state).length === 0)
|
|
1675
3190
|
return;
|
|
1676
3191
|
try {
|
|
3192
|
+
const saveSync = client._tokenStore.saveSyncStates?.bind(client._tokenStore);
|
|
3193
|
+
if (typeof saveSync === 'function') {
|
|
3194
|
+
await saveSync(aid, deviceId, slotId, state);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
1677
3197
|
const saveFn = client._tokenStore.saveSeq?.bind(client._tokenStore);
|
|
1678
3198
|
if (typeof saveFn === 'function') {
|
|
1679
|
-
await Promise.all(Object.entries(state).map(([ns,
|
|
3199
|
+
await Promise.all(Object.entries(state).map(([ns, triplet]) => saveFn(aid, deviceId, slotId, ns, triplet.ack)));
|
|
1680
3200
|
return;
|
|
1681
3201
|
}
|
|
1682
3202
|
if (typeof client._tokenStore.updateInstanceState === 'function') {
|
|
@@ -1690,69 +3210,99 @@ export class MessageDeliveryEngine {
|
|
|
1690
3210
|
}
|
|
1691
3211
|
}
|
|
1692
3212
|
catch (exc) {
|
|
3213
|
+
const error = formatDeliveryError(exc);
|
|
3214
|
+
client._clientLog.warn(`save SeqTracker state failed: ${error}`);
|
|
1693
3215
|
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
1694
3216
|
phase: 'save',
|
|
1695
3217
|
aid,
|
|
1696
3218
|
device_id: deviceId,
|
|
1697
3219
|
slot_id: slotId,
|
|
1698
|
-
error: String(
|
|
3220
|
+
error: String(error),
|
|
1699
3221
|
}).catch(() => { });
|
|
3222
|
+
if (throwOnError)
|
|
3223
|
+
throw exc;
|
|
1700
3224
|
}
|
|
1701
3225
|
}
|
|
1702
3226
|
persistRepairedSeq(ns) {
|
|
1703
3227
|
const client = this.runtime.client;
|
|
1704
3228
|
if (!client._aid || !ns)
|
|
1705
3229
|
return;
|
|
3230
|
+
const aid = client._aid;
|
|
3231
|
+
const deviceId = client._deviceId;
|
|
3232
|
+
const slotId = client._slotId;
|
|
1706
3233
|
const seq = client._seqTracker.getContiguousSeq(ns);
|
|
1707
3234
|
this.dropSeqTrackerPending(ns);
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
3235
|
+
const saveSeq = client._tokenStore.saveSeq;
|
|
3236
|
+
if (seq > 0 && typeof saveSeq === 'function') {
|
|
3237
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3238
|
+
try {
|
|
3239
|
+
await saveSeq.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
|
|
3240
|
+
}
|
|
3241
|
+
catch (exc) {
|
|
1711
3242
|
client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
const deleteSeq = client._tokenStore.deleteSeq;
|
|
3248
|
+
if (seq <= 0 && typeof deleteSeq === 'function') {
|
|
3249
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3250
|
+
try {
|
|
3251
|
+
await deleteSeq.call(client._tokenStore, aid, deviceId, slotId, ns);
|
|
3252
|
+
}
|
|
3253
|
+
catch (exc) {
|
|
1718
3254
|
client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
3255
|
+
}
|
|
3256
|
+
});
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
const updater = client._tokenStore.updateInstanceState;
|
|
3260
|
+
if (seq <= 0 && typeof updater === 'function') {
|
|
3261
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3262
|
+
try {
|
|
3263
|
+
await updater.call(client._tokenStore, aid, deviceId, slotId, (current) => {
|
|
3264
|
+
if (isJsonObject(current.seq_tracker_state)) {
|
|
3265
|
+
const next = { ...current.seq_tracker_state };
|
|
3266
|
+
delete next[ns];
|
|
3267
|
+
current.seq_tracker_state = next;
|
|
3268
|
+
}
|
|
3269
|
+
return current;
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
catch (exc) {
|
|
1731
3273
|
client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
if (seq > 0) {
|
|
1736
|
-
void this.saveSeqTrackerState();
|
|
1737
|
-
}
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
return;
|
|
1738
3277
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
3278
|
+
if (seq > 0) {
|
|
3279
|
+
void this.saveSeqTrackerState();
|
|
1741
3280
|
}
|
|
1742
3281
|
}
|
|
1743
3282
|
clampAckSeq(method, field, ns, seq) {
|
|
1744
3283
|
const client = this.runtime.client;
|
|
1745
3284
|
const original = seq;
|
|
1746
|
-
let next =
|
|
1747
|
-
if (!Number.
|
|
3285
|
+
let next = seq;
|
|
3286
|
+
if (typeof next !== 'number' || !Number.isSafeInteger(next))
|
|
1748
3287
|
return 0;
|
|
1749
3288
|
if (next < 0)
|
|
1750
3289
|
next = 0;
|
|
1751
3290
|
if (ns) {
|
|
1752
|
-
const
|
|
1753
|
-
if (
|
|
1754
|
-
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} >
|
|
1755
|
-
next =
|
|
3291
|
+
const committedAck = client._seqTracker.getContiguousSeq(ns);
|
|
3292
|
+
if (next > committedAck) {
|
|
3293
|
+
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
|
|
3294
|
+
next = committedAck;
|
|
3295
|
+
}
|
|
3296
|
+
if ((method === 'message.v2.ack' || method === 'message.ack')) {
|
|
3297
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
3298
|
+
if (pending && !pending.committed && next >= pending.seq)
|
|
3299
|
+
next = pending.seq - 1;
|
|
3300
|
+
}
|
|
3301
|
+
else if (method === 'group.v2.ack' || method === 'group.ack_messages') {
|
|
3302
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
3303
|
+
const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
|
|
3304
|
+
if (pending && !pending.committed && next >= pending.seq)
|
|
3305
|
+
next = pending.seq - 1;
|
|
1756
3306
|
}
|
|
1757
3307
|
}
|
|
1758
3308
|
return next;
|
|
@@ -1761,7 +3311,8 @@ export class MessageDeliveryEngine {
|
|
|
1761
3311
|
const client = this.runtime.client;
|
|
1762
3312
|
const clampField = (field, ns) => {
|
|
1763
3313
|
const raw = params[field];
|
|
1764
|
-
|
|
3314
|
+
// 保留非法类型/数值,让 V2 ACK 严格入口失败关闭,不能先 clamp 成合法游标。
|
|
3315
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
|
|
1765
3316
|
return params;
|
|
1766
3317
|
const next = this.clampAckSeq(method, field, ns, raw);
|
|
1767
3318
|
return next === raw ? params : { ...params, [field]: next };
|
|
@@ -1775,7 +3326,7 @@ export class MessageDeliveryEngine {
|
|
|
1775
3326
|
return clampField('seq', ns);
|
|
1776
3327
|
}
|
|
1777
3328
|
if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
|
|
1778
|
-
const groupId =
|
|
3329
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1779
3330
|
const ns = groupId ? `group:${groupId}` : '';
|
|
1780
3331
|
if (method === 'group.v2.ack')
|
|
1781
3332
|
return clampField('up_to_seq', ns);
|
|
@@ -1786,7 +3337,7 @@ export class MessageDeliveryEngine {
|
|
|
1786
3337
|
}
|
|
1787
3338
|
return params;
|
|
1788
3339
|
}
|
|
1789
|
-
async drainOrderedMessages(ns, beforeSeq) {
|
|
3340
|
+
async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
|
|
1790
3341
|
const client = this.runtime.client;
|
|
1791
3342
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
1792
3343
|
if (!queue || queue.size === 0)
|
|
@@ -1801,13 +3352,13 @@ export class MessageDeliveryEngine {
|
|
|
1801
3352
|
queue.delete(seq);
|
|
1802
3353
|
if (!item || client._pushedSeqs.get(ns)?.has(seq))
|
|
1803
3354
|
continue;
|
|
1804
|
-
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload);
|
|
3355
|
+
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse);
|
|
1805
3356
|
this.markPublishedSeq(ns, seq);
|
|
1806
3357
|
delivered = true;
|
|
1807
3358
|
}
|
|
1808
3359
|
if (queue.size === 0) {
|
|
1809
3360
|
client._pendingOrderedMsgs.delete(ns);
|
|
1810
|
-
if (delivered)
|
|
3361
|
+
if (delivered && persist)
|
|
1811
3362
|
await this.saveSeqTrackerState();
|
|
1812
3363
|
}
|
|
1813
3364
|
}
|
|
@@ -1830,7 +3381,7 @@ export class MessageDeliveryEngine {
|
|
|
1830
3381
|
this.enqueueOrderedMessage(ns, event, seqNum, payload);
|
|
1831
3382
|
return false;
|
|
1832
3383
|
}
|
|
1833
|
-
await this.drainOrderedMessages(ns, seqNum);
|
|
3384
|
+
await this.drainOrderedMessages(ns, seqNum, true);
|
|
1834
3385
|
if (client._pushedSeqs.get(ns)?.has(seqNum))
|
|
1835
3386
|
return false;
|
|
1836
3387
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -1844,14 +3395,14 @@ export class MessageDeliveryEngine {
|
|
|
1844
3395
|
await this.saveSeqTrackerState();
|
|
1845
3396
|
return true;
|
|
1846
3397
|
}
|
|
1847
|
-
async publishPulledMessage(event, ns, seq, payload) {
|
|
3398
|
+
async publishPulledMessage(event, ns, seq, payload, persist = true) {
|
|
1848
3399
|
const client = this.runtime.client;
|
|
1849
3400
|
const seqNum = Number(seq);
|
|
1850
3401
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
|
|
1851
3402
|
if (event === 'message.recalled') {
|
|
1852
|
-
return this.publishMessageRecallTombstone(seq, payload);
|
|
3403
|
+
return await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
|
|
1853
3404
|
}
|
|
1854
|
-
await client._publishAppEvent(event, payload);
|
|
3405
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
1855
3406
|
return true;
|
|
1856
3407
|
}
|
|
1857
3408
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -1861,16 +3412,16 @@ export class MessageDeliveryEngine {
|
|
|
1861
3412
|
client._pendingOrderedMsgs.delete(ns);
|
|
1862
3413
|
return false;
|
|
1863
3414
|
}
|
|
1864
|
-
await this.drainOrderedMessages(ns, seqNum);
|
|
3415
|
+
await this.drainOrderedMessages(ns, seqNum, false, persist);
|
|
1865
3416
|
queue?.delete(seqNum);
|
|
1866
3417
|
if (queue && queue.size === 0)
|
|
1867
3418
|
client._pendingOrderedMsgs.delete(ns);
|
|
1868
3419
|
if (event === 'message.recalled') {
|
|
1869
|
-
const published = await this.publishMessageRecallTombstone(seqNum, payload);
|
|
3420
|
+
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload));
|
|
1870
3421
|
this.markPublishedSeq(ns, seqNum);
|
|
1871
3422
|
return published;
|
|
1872
3423
|
}
|
|
1873
|
-
await client._publishAppEvent(event, payload);
|
|
3424
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
1874
3425
|
this.markPublishedSeq(ns, seqNum);
|
|
1875
3426
|
return true;
|
|
1876
3427
|
}
|