@agentunion/fastaun-browser 0.5.4 → 0.5.6
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 +130 -94
- 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 +130 -94
- package/_packed_docs/INDEX.md +127 -69
- package/_packed_docs/KITE_DOCS_GUIDE.md +63 -29
- 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 +227 -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 +1004 -0
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +999 -0
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
- 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 +62 -17
- 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 +67 -78
- 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 +50 -14
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +18 -5
- package/_packed_docs/sdk/09-group-rpc-manual.md +312 -282
- package/_packed_docs/sdk/09-message-rpc-manual.md +142 -103
- package/_packed_docs/sdk/09-payload-reference.md +1 -1
- package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +26 -17
- package/_packed_docs/sdk/INDEX.md +42 -34
- package/_packed_docs/sdk/README.md +7 -6
- 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.d.ts +4 -5
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +66 -43
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +2 -6
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +20 -14
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +30 -11
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +3297 -1023
- package/dist/client/delivery.d.ts +75 -8
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1419 -245
- 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.map +1 -1
- package/dist/client/lifecycle.js +48 -4
- 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 +22 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +309 -74
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +25 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +606 -97
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +24 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +404 -215
- 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 +3 -0
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +91 -53
- package/dist/facades.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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 +218 -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 +376 -10
- 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 +218 -101
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
- 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,15 @@ 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
|
+
}
|
|
50
60
|
function p2pAppEventFromPlainPullMessage(message) {
|
|
51
61
|
const payload = message.payload;
|
|
52
62
|
const payloadType = isJsonObject(payload)
|
|
@@ -65,9 +75,85 @@ function p2pAppEventFromPlainPullMessage(message) {
|
|
|
65
75
|
}
|
|
66
76
|
export class MessageDeliveryEngine {
|
|
67
77
|
runtime;
|
|
78
|
+
realtimeTailResults = null;
|
|
79
|
+
realtimeSyncing = null;
|
|
80
|
+
realtimeAcking = null;
|
|
81
|
+
pendingP2PInlineAcks = null;
|
|
82
|
+
pendingGroupInlineAcks = null;
|
|
83
|
+
pendingGroupInlineAckNamespaces = null;
|
|
84
|
+
// reset 后旧 inline Promise 只能自行结束,不能触碰新身份上下文的状态。
|
|
85
|
+
inlineGeneration = 0;
|
|
68
86
|
constructor(runtime) {
|
|
69
87
|
this.runtime = runtime;
|
|
70
88
|
}
|
|
89
|
+
resetInlineAckState() {
|
|
90
|
+
this.inlineGeneration += 1;
|
|
91
|
+
this.realtimeSyncing = null;
|
|
92
|
+
this.realtimeTailResults = null;
|
|
93
|
+
this.realtimeAcking = null;
|
|
94
|
+
this.pendingP2PInlineAcks = null;
|
|
95
|
+
this.pendingGroupInlineAcks = null;
|
|
96
|
+
this.pendingGroupInlineAckNamespaces = null;
|
|
97
|
+
}
|
|
98
|
+
isInlineGenerationCurrent(generation) {
|
|
99
|
+
return generation === this.inlineGeneration;
|
|
100
|
+
}
|
|
101
|
+
captureInlineGeneration() {
|
|
102
|
+
return this.inlineGeneration;
|
|
103
|
+
}
|
|
104
|
+
beginRealtimeSync(ns) {
|
|
105
|
+
if (!ns || this.realtimeSyncing?.has(ns))
|
|
106
|
+
return false;
|
|
107
|
+
const syncing = this.realtimeSyncing ?? new Set();
|
|
108
|
+
this.realtimeSyncing = syncing;
|
|
109
|
+
syncing.add(ns);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
endRealtimeSync(ns) {
|
|
113
|
+
this.realtimeSyncing?.delete(ns);
|
|
114
|
+
}
|
|
115
|
+
beginRealtimeAck(ns, seq) {
|
|
116
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
117
|
+
return false;
|
|
118
|
+
const current = this.realtimeAcking?.get(ns);
|
|
119
|
+
if (current !== undefined) {
|
|
120
|
+
if (seq > current)
|
|
121
|
+
this.realtimeAcking?.set(ns, seq);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const acking = this.realtimeAcking ?? new Map();
|
|
125
|
+
this.realtimeAcking = acking;
|
|
126
|
+
acking.set(ns, seq);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
finishRealtimeAck(ns, completedSeq) {
|
|
130
|
+
const latest = this.realtimeAcking?.get(ns) ?? 0;
|
|
131
|
+
this.realtimeAcking?.delete(ns);
|
|
132
|
+
return latest > completedSeq ? latest : 0;
|
|
133
|
+
}
|
|
134
|
+
pendingInlineAckNamespaceCount() {
|
|
135
|
+
return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
|
|
136
|
+
}
|
|
137
|
+
inlinePostAckRow(row) {
|
|
138
|
+
const result = {};
|
|
139
|
+
for (const key of ['envelope_json', 'spk_id', 'group_id', 'message_id']) {
|
|
140
|
+
const value = row[key];
|
|
141
|
+
if (typeof value === 'string' && value)
|
|
142
|
+
result[key] = value;
|
|
143
|
+
}
|
|
144
|
+
if (typeof row.seq === 'number' && Number.isSafeInteger(row.seq) && row.seq > 0)
|
|
145
|
+
result.seq = row.seq;
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
markRealtimeTailCompleted(result) {
|
|
149
|
+
const completed = this.realtimeTailResults ?? new WeakSet();
|
|
150
|
+
this.realtimeTailResults = completed;
|
|
151
|
+
completed.add(result);
|
|
152
|
+
Object.defineProperty(result, '_realtime_tail_completed', { value: true, enumerable: false });
|
|
153
|
+
}
|
|
154
|
+
isRealtimeTailCompleted(result) {
|
|
155
|
+
return this.realtimeTailResults?.has(result) ?? false;
|
|
156
|
+
}
|
|
71
157
|
prunePushedSeqs(ns) {
|
|
72
158
|
const client = this.runtime.client;
|
|
73
159
|
const pushed = client._pushedSeqs.get(ns);
|
|
@@ -106,18 +192,312 @@ export class MessageDeliveryEngine {
|
|
|
106
192
|
client._clientLog.warn(`${label} failed: group=${groupId}`, e);
|
|
107
193
|
});
|
|
108
194
|
}
|
|
109
|
-
fireGroupV2Ack(groupId, ns, seq, label) {
|
|
195
|
+
fireGroupV2Ack(groupId, ns, seq, label, generation = null) {
|
|
110
196
|
const client = this.runtime.client;
|
|
111
197
|
const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, seq);
|
|
112
|
-
if (ackSeq <= 0)
|
|
198
|
+
if (ackSeq <= 0 || !this.beginRealtimeAck(ns, ackSeq))
|
|
113
199
|
return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
200
|
+
let task;
|
|
201
|
+
try {
|
|
202
|
+
task = Promise.resolve(client._callRawV2Rpc('group.v2.ack', {
|
|
203
|
+
group_id: groupId,
|
|
204
|
+
up_to_seq: ackSeq,
|
|
205
|
+
_rpc_background: true,
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
catch (exc) {
|
|
209
|
+
this.finishRealtimeAck(ns, ackSeq);
|
|
210
|
+
client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${formatDeliveryError(exc)}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
void task.then((result) => {
|
|
214
|
+
if (generation !== null && this.isInlineGenerationCurrent(generation)) {
|
|
215
|
+
this.completePendingGroupInlineAckUpTo(ns, this.resolveActualAckSeq(result, ackSeq), generation);
|
|
216
|
+
}
|
|
118
217
|
}).catch((e) => {
|
|
119
218
|
client._clientLog.debug(`${label} failed: group=${groupId} seq=${ackSeq} err=${String(e)}`);
|
|
219
|
+
}).finally(() => {
|
|
220
|
+
if (generation !== null && !this.isInlineGenerationCurrent(generation))
|
|
221
|
+
return;
|
|
222
|
+
const nextAckSeq = this.finishRealtimeAck(ns, ackSeq);
|
|
223
|
+
if (nextAckSeq > 0)
|
|
224
|
+
this.fireGroupV2Ack(groupId, ns, nextAckSeq, label, generation);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
fireP2PV2Ack(seq, label, generation = null) {
|
|
228
|
+
const client = this.runtime.client;
|
|
229
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
230
|
+
if (!Number.isSafeInteger(seq) || seq <= 0 || typeof client._ackV2 !== 'function'
|
|
231
|
+
|| !this.beginRealtimeAck(ns, seq))
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
const ackTask = generation === null ? client._ackV2(seq) : client._ackV2(seq, generation);
|
|
235
|
+
void Promise.resolve(ackTask).catch((exc) => {
|
|
236
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
237
|
+
}).finally(() => {
|
|
238
|
+
if (generation !== null && !this.isInlineGenerationCurrent(generation))
|
|
239
|
+
return;
|
|
240
|
+
const nextAckSeq = this.finishRealtimeAck(ns, seq);
|
|
241
|
+
if (nextAckSeq > 0)
|
|
242
|
+
this.fireP2PV2Ack(nextAckSeq, label, generation);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (exc) {
|
|
246
|
+
this.finishRealtimeAck(ns, seq);
|
|
247
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
retryPendingP2PInlineAck(ns, seq, label, ackThrough = seq) {
|
|
251
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
252
|
+
if (!pending || pending.seq !== seq)
|
|
253
|
+
return false;
|
|
254
|
+
if (pending.acking || !pending.committed)
|
|
255
|
+
return true;
|
|
256
|
+
if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
|
|
257
|
+
ackThrough = seq;
|
|
258
|
+
const client = this.runtime.client;
|
|
259
|
+
if (typeof client._ackV2 !== 'function')
|
|
260
|
+
return true;
|
|
261
|
+
pending.acking = true;
|
|
262
|
+
const generation = pending.generation;
|
|
263
|
+
let task;
|
|
264
|
+
try {
|
|
265
|
+
task = Promise.resolve(client._ackV2(ackThrough, generation));
|
|
266
|
+
}
|
|
267
|
+
catch (exc) {
|
|
268
|
+
pending.acking = false;
|
|
269
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
void task.catch((exc) => {
|
|
273
|
+
client._clientLog.debug(`${label} failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
274
|
+
}).finally(() => {
|
|
275
|
+
const current = this.pendingP2PInlineAcks?.get(ns);
|
|
276
|
+
if (this.isInlineGenerationCurrent(generation) && current === pending)
|
|
277
|
+
current.acking = false;
|
|
120
278
|
});
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
retryPendingGroupInlineAck(ns, seq, label, ackThrough = seq) {
|
|
282
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
283
|
+
if (!pending || pending.seq !== seq)
|
|
284
|
+
return false;
|
|
285
|
+
if (pending.acking || !pending.committed)
|
|
286
|
+
return true;
|
|
287
|
+
if (!Number.isSafeInteger(ackThrough) || ackThrough < seq)
|
|
288
|
+
ackThrough = seq;
|
|
289
|
+
const client = this.runtime.client;
|
|
290
|
+
if (typeof client._ackGroupV2 !== 'function')
|
|
291
|
+
return true;
|
|
292
|
+
pending.acking = true;
|
|
293
|
+
const generation = pending.generation;
|
|
294
|
+
let task;
|
|
295
|
+
try {
|
|
296
|
+
task = Promise.resolve(client._ackGroupV2(pending.groupId, ackThrough, pending.groupAid || undefined, generation));
|
|
297
|
+
}
|
|
298
|
+
catch (exc) {
|
|
299
|
+
pending.acking = false;
|
|
300
|
+
client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
void task.catch((exc) => {
|
|
304
|
+
client._clientLog.debug(`${label} failed: group=${pending.groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
305
|
+
}).finally(() => {
|
|
306
|
+
const current = this.pendingGroupInlineAcks?.get(ns);
|
|
307
|
+
if (this.isInlineGenerationCurrent(generation) && current === pending)
|
|
308
|
+
current.acking = false;
|
|
309
|
+
});
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
reservePendingP2PInlineAck(ns, seq, row, generation = this.inlineGeneration) {
|
|
313
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
314
|
+
|| !ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingP2PInlineAcks?.has(ns)
|
|
315
|
+
|| this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
|
|
316
|
+
return false;
|
|
317
|
+
const pendingMap = this.pendingP2PInlineAcks ?? new Map();
|
|
318
|
+
this.pendingP2PInlineAcks = pendingMap;
|
|
319
|
+
pendingMap.set(ns, {
|
|
320
|
+
seq,
|
|
321
|
+
row: this.inlinePostAckRow(row),
|
|
322
|
+
generation,
|
|
323
|
+
committed: false,
|
|
324
|
+
observedAckSeq: 0,
|
|
325
|
+
acking: false,
|
|
326
|
+
});
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
dropPendingP2PInlineAck(ns, seq, generation = this.inlineGeneration) {
|
|
330
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
331
|
+
if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
|
|
332
|
+
this.pendingP2PInlineAcks?.delete(ns);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
reservePendingGroupInlineAck(ns, seq, row, groupAid, groupId, generation = this.inlineGeneration) {
|
|
336
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
337
|
+
|| !ns || !groupId || !Number.isSafeInteger(seq) || seq <= 0
|
|
338
|
+
|| this.pendingGroupInlineAcks?.has(ns)
|
|
339
|
+
|| this.pendingGroupInlineAckNamespaces?.has(ns)
|
|
340
|
+
|| this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
|
|
341
|
+
return false;
|
|
342
|
+
const normalizedGroupAid = typeof groupAid === 'string' ? groupAid.trim() : '';
|
|
343
|
+
const aliasNs = normalizedGroupAid ? `group:${normalizedGroupAid}` : '';
|
|
344
|
+
if (aliasNs && aliasNs !== ns
|
|
345
|
+
&& (this.pendingGroupInlineAcks?.has(aliasNs) || this.pendingGroupInlineAckNamespaces?.has(aliasNs)))
|
|
346
|
+
return false;
|
|
347
|
+
const pendingMap = this.pendingGroupInlineAcks ?? new Map();
|
|
348
|
+
this.pendingGroupInlineAcks = pendingMap;
|
|
349
|
+
pendingMap.set(ns, {
|
|
350
|
+
seq,
|
|
351
|
+
row: this.inlinePostAckRow(row),
|
|
352
|
+
generation,
|
|
353
|
+
groupId,
|
|
354
|
+
groupAid: normalizedGroupAid,
|
|
355
|
+
aliasNs: aliasNs !== ns ? aliasNs : '',
|
|
356
|
+
committed: false,
|
|
357
|
+
observedAckSeq: 0,
|
|
358
|
+
acking: false,
|
|
359
|
+
});
|
|
360
|
+
if (aliasNs && aliasNs !== ns) {
|
|
361
|
+
const aliasMap = this.pendingGroupInlineAckNamespaces ?? new Map();
|
|
362
|
+
this.pendingGroupInlineAckNamespaces = aliasMap;
|
|
363
|
+
aliasMap.set(aliasNs, ns);
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
removePendingGroupInlineAck(ns, pending) {
|
|
368
|
+
if (this.pendingGroupInlineAcks?.get(ns) !== pending)
|
|
369
|
+
return false;
|
|
370
|
+
this.pendingGroupInlineAcks.delete(ns);
|
|
371
|
+
if (pending.aliasNs && this.pendingGroupInlineAckNamespaces?.get(pending.aliasNs) === ns) {
|
|
372
|
+
this.pendingGroupInlineAckNamespaces.delete(pending.aliasNs);
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
dropPendingGroupInlineAck(ns, seq, generation = this.inlineGeneration) {
|
|
377
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
378
|
+
if (this.isInlineGenerationCurrent(generation) && pending?.seq === seq && pending.generation === generation) {
|
|
379
|
+
this.removePendingGroupInlineAck(ns, pending);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
resolvePendingGroupInlineAckNamespace(ns) {
|
|
383
|
+
const pendingMap = this.pendingGroupInlineAcks;
|
|
384
|
+
if (!ns || !pendingMap || pendingMap.size === 0 || pendingMap.has(ns))
|
|
385
|
+
return ns;
|
|
386
|
+
const canonicalNs = this.pendingGroupInlineAckNamespaces?.get(ns);
|
|
387
|
+
return canonicalNs && pendingMap.has(canonicalNs) ? canonicalNs : ns;
|
|
388
|
+
}
|
|
389
|
+
pendingP2PInlineAckGeneration(ns) {
|
|
390
|
+
const pendingMap = this.pendingP2PInlineAcks;
|
|
391
|
+
if (!ns || !pendingMap || pendingMap.size === 0)
|
|
392
|
+
return null;
|
|
393
|
+
const pending = pendingMap.get(ns);
|
|
394
|
+
return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
|
|
395
|
+
}
|
|
396
|
+
pendingGroupInlineAckGeneration(ns) {
|
|
397
|
+
const pendingMap = this.pendingGroupInlineAcks;
|
|
398
|
+
if (!ns || !pendingMap || pendingMap.size === 0)
|
|
399
|
+
return null;
|
|
400
|
+
const canonicalNs = pendingMap.has(ns) ? ns : this.pendingGroupInlineAckNamespaces?.get(ns);
|
|
401
|
+
if (!canonicalNs)
|
|
402
|
+
return null;
|
|
403
|
+
const pending = pendingMap.get(canonicalNs);
|
|
404
|
+
return pending && this.isInlineGenerationCurrent(pending.generation) ? pending.generation : null;
|
|
405
|
+
}
|
|
406
|
+
completePendingP2PInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
|
|
407
|
+
const pendingMap = this.pendingP2PInlineAcks;
|
|
408
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
409
|
+
|| !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
|
|
410
|
+
return;
|
|
411
|
+
const pending = pendingMap.get(ns);
|
|
412
|
+
if (!pending || pending.generation !== generation || ackSeq < pending.seq)
|
|
413
|
+
return;
|
|
414
|
+
if (!pending.committed) {
|
|
415
|
+
pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
pendingMap.delete(ns);
|
|
419
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
|
|
420
|
+
}
|
|
421
|
+
completePendingGroupInlineAckUpTo(ns, ackSeq, generation = this.inlineGeneration) {
|
|
422
|
+
const pendingMap = this.pendingGroupInlineAcks;
|
|
423
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
424
|
+
|| !ns || !Number.isSafeInteger(ackSeq) || ackSeq <= 0 || !pendingMap || pendingMap.size === 0)
|
|
425
|
+
return;
|
|
426
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
427
|
+
const pending = pendingMap.get(canonicalNs);
|
|
428
|
+
if (!pending || pending.generation !== generation || ackSeq < pending.seq)
|
|
429
|
+
return;
|
|
430
|
+
if (!pending.committed) {
|
|
431
|
+
pending.observedAckSeq = Math.max(pending.observedAckSeq, ackSeq);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.removePendingGroupInlineAck(canonicalNs, pending);
|
|
435
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
|
|
436
|
+
}
|
|
437
|
+
runPostInlineCommit(row, seq, scope, generation) {
|
|
438
|
+
const client = this.runtime.client;
|
|
439
|
+
if (!this.isInlineGenerationCurrent(generation) || typeof client._postInlineV2CommitAndAck !== 'function')
|
|
440
|
+
return;
|
|
441
|
+
try {
|
|
442
|
+
void Promise.resolve(client._postInlineV2CommitAndAck(row)).catch((exc) => {
|
|
443
|
+
client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
catch (exc) {
|
|
447
|
+
client._clientLog.debug(`inline ${scope} post-commit work failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
markPendingP2PInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
|
|
451
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
452
|
+
if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
|
|
453
|
+
return false;
|
|
454
|
+
pending.committed = true;
|
|
455
|
+
if (pending.observedAckSeq < seq)
|
|
456
|
+
return true;
|
|
457
|
+
this.pendingP2PInlineAcks?.delete(ns);
|
|
458
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'P2P', generation);
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
markPendingGroupInlineAckCommitted(ns, seq, generation = this.inlineGeneration) {
|
|
462
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
463
|
+
if (!this.isInlineGenerationCurrent(generation) || !pending || pending.seq !== seq || pending.generation !== generation)
|
|
464
|
+
return false;
|
|
465
|
+
pending.committed = true;
|
|
466
|
+
if (pending.observedAckSeq < seq)
|
|
467
|
+
return true;
|
|
468
|
+
this.removePendingGroupInlineAck(ns, pending);
|
|
469
|
+
this.runPostInlineCommit(pending.row, pending.seq, 'Group', generation);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
resolveActualAckSeq(result, requestedSeq) {
|
|
473
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
474
|
+
return 0;
|
|
475
|
+
if (!isJsonObject(result))
|
|
476
|
+
return requestedSeq;
|
|
477
|
+
const row = result;
|
|
478
|
+
let value;
|
|
479
|
+
if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
|
|
480
|
+
value = row.effective_ack_seq;
|
|
481
|
+
}
|
|
482
|
+
else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
|
|
483
|
+
value = row.ack_seq;
|
|
484
|
+
}
|
|
485
|
+
else if (Object.prototype.hasOwnProperty.call(row, 'cursor')) {
|
|
486
|
+
const cursor = row.cursor;
|
|
487
|
+
if (isJsonObject(cursor)) {
|
|
488
|
+
const cursorRow = cursor;
|
|
489
|
+
if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
|
|
490
|
+
return 0;
|
|
491
|
+
value = cursorRow.current_seq;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
value = cursor;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
return requestedSeq;
|
|
499
|
+
}
|
|
500
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
121
501
|
}
|
|
122
502
|
enqueueOrderedMessage(ns, event, seq, payload) {
|
|
123
503
|
const client = this.runtime.client;
|
|
@@ -136,7 +516,7 @@ export class MessageDeliveryEngine {
|
|
|
136
516
|
isGroupEventNamespace(ns) {
|
|
137
517
|
return ns.startsWith('group_event:');
|
|
138
518
|
}
|
|
139
|
-
async publishOrderedQueueItem(ns, event, seq, payload) {
|
|
519
|
+
async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false) {
|
|
140
520
|
const client = this.runtime.client;
|
|
141
521
|
if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
|
|
142
522
|
await this.publishOrderedGroupChanged(payload);
|
|
@@ -146,6 +526,10 @@ export class MessageDeliveryEngine {
|
|
|
146
526
|
await this.publishMessageRecallTombstone(seq, payload);
|
|
147
527
|
return;
|
|
148
528
|
}
|
|
529
|
+
if (pullResponse) {
|
|
530
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
149
533
|
await client._publishAppEvent(event, payload);
|
|
150
534
|
}
|
|
151
535
|
async publishOrderedGroupChanged(payload) {
|
|
@@ -154,8 +538,8 @@ export class MessageDeliveryEngine {
|
|
|
154
538
|
const eventPayload = payload;
|
|
155
539
|
client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
|
|
156
540
|
if (eventPayload.action === 'dissolved') {
|
|
157
|
-
const rawGroupId = eventPayload.
|
|
158
|
-
const groupId =
|
|
541
|
+
const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
|
|
542
|
+
const groupId = String(rawGroupId).trim();
|
|
159
543
|
if (groupId)
|
|
160
544
|
client._cleanupDissolvedGroup?.(groupId);
|
|
161
545
|
}
|
|
@@ -185,13 +569,28 @@ export class MessageDeliveryEngine {
|
|
|
185
569
|
}
|
|
186
570
|
normalizePublishedMessagePayload(event, payload) {
|
|
187
571
|
if (this.isInstanceScopedMessageEvent(event)) {
|
|
572
|
+
payload = this.stripInlineInternalFields(payload);
|
|
573
|
+
if (event === 'group.message_created')
|
|
574
|
+
payload = normalizeGroupMentionMode(payload);
|
|
188
575
|
return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
|
|
189
576
|
}
|
|
190
577
|
if (this.isGroupScopedEvent(event)) {
|
|
578
|
+
payload = this.stripInlineInternalFields(payload);
|
|
191
579
|
return this.attachAppGroupEventEnvelope(this.attachCurrentInstanceContext(payload));
|
|
192
580
|
}
|
|
193
581
|
return payload;
|
|
194
582
|
}
|
|
583
|
+
stripInlineInternalFields(payload) {
|
|
584
|
+
if (!isJsonObject(payload))
|
|
585
|
+
return payload;
|
|
586
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'inline_message')
|
|
587
|
+
&& !Object.prototype.hasOwnProperty.call(payload, '_inline_message_candidates_v1'))
|
|
588
|
+
return payload;
|
|
589
|
+
const result = { ...payload };
|
|
590
|
+
delete result.inline_message;
|
|
591
|
+
delete result._inline_message_candidates_v1;
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
195
594
|
envelopeMetadata(value) {
|
|
196
595
|
let source = value;
|
|
197
596
|
if (source && typeof source === 'object') {
|
|
@@ -248,6 +647,7 @@ export class MessageDeliveryEngine {
|
|
|
248
647
|
if (protectedHeaders)
|
|
249
648
|
envelope.protected_headers = protectedHeaders;
|
|
250
649
|
setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
|
|
650
|
+
setIfPresent('mention_mode', message.mention_mode);
|
|
251
651
|
return envelope;
|
|
252
652
|
}
|
|
253
653
|
isGroupScopedEvent(event) {
|
|
@@ -314,19 +714,30 @@ export class MessageDeliveryEngine {
|
|
|
314
714
|
if (protectedHeaders)
|
|
315
715
|
envelope.protected_headers = protectedHeaders;
|
|
316
716
|
setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
|
|
717
|
+
if (method === 'group.send')
|
|
718
|
+
setIfPresent('mention_mode', resultObj.mention_mode);
|
|
317
719
|
return envelope;
|
|
318
720
|
}
|
|
319
|
-
attachSendResultEnvelope(method, params, result, encrypted) {
|
|
721
|
+
attachSendResultEnvelope(method, params, result, encrypted, modeSource) {
|
|
320
722
|
if (!APP_SEND_ENVELOPE_METHODS.has(method) || !isJsonObject(result))
|
|
321
723
|
return result;
|
|
322
|
-
|
|
323
|
-
|
|
724
|
+
let out = { ...result };
|
|
725
|
+
const serverEnvelope = method === 'group.send' && isJsonObject(out.envelope)
|
|
726
|
+
? { ...out.envelope }
|
|
727
|
+
: undefined;
|
|
324
728
|
if ('payload' in params) {
|
|
325
729
|
out.payload = params.payload;
|
|
326
730
|
}
|
|
327
731
|
else if ('content' in params) {
|
|
328
732
|
out.payload = params.content;
|
|
329
733
|
}
|
|
734
|
+
const generatedEnvelope = this.sendResultEnvelope(method, params, out, encrypted);
|
|
735
|
+
out.envelope = serverEnvelope ? { ...generatedEnvelope, ...serverEnvelope } : generatedEnvelope;
|
|
736
|
+
if (method === 'group.send') {
|
|
737
|
+
const serverSource = modeSource ?? result;
|
|
738
|
+
const authoritativeSource = hasGroupMentionModeField(serverSource) ? serverSource : out;
|
|
739
|
+
out = normalizeGroupMentionMode(out, authoritativeSource);
|
|
740
|
+
}
|
|
330
741
|
return out;
|
|
331
742
|
}
|
|
332
743
|
attachAppGroupEventEnvelope(payload) {
|
|
@@ -516,7 +927,7 @@ export class MessageDeliveryEngine {
|
|
|
516
927
|
// (group_id, sorted message_ids) 已能唯一标识一次撤回;缺 message_ids 时按原消息 id/seq 兜底。
|
|
517
928
|
// 去重键不含 recalled_at:占位/通知 tombstone(pull)与在线 push 三条通道对同一次撤回
|
|
518
929
|
// 可能携带不同来源的时间戳(push 在事务后重取),纳入 recalled_at 会使去重失效、回调多次。
|
|
519
|
-
const normalizedGroupId =
|
|
930
|
+
const normalizedGroupId = String(groupId ?? '').trim();
|
|
520
931
|
const ids = payload.message_ids;
|
|
521
932
|
const idPart = Array.isArray(ids)
|
|
522
933
|
? ids.map((i) => String(i ?? '').trim()).filter(Boolean).sort().join(',')
|
|
@@ -545,9 +956,7 @@ export class MessageDeliveryEngine {
|
|
|
545
956
|
if (!eventPayload)
|
|
546
957
|
return false;
|
|
547
958
|
const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
|
|
548
|
-
const dedupGroupId =
|
|
549
|
-
if (dedupGroupId)
|
|
550
|
-
eventPayload.group_id = dedupGroupId;
|
|
959
|
+
const dedupGroupId = rawGroupId;
|
|
551
960
|
const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
|
|
552
961
|
let seen = client._groupRecallSeen;
|
|
553
962
|
if (!seen) {
|
|
@@ -645,7 +1054,7 @@ export class MessageDeliveryEngine {
|
|
|
645
1054
|
}
|
|
646
1055
|
if (isJsonObject(payload)) {
|
|
647
1056
|
try {
|
|
648
|
-
const snapshot = client._agentMdManager.eventSnapshot();
|
|
1057
|
+
const snapshot = await client._agentMdManager.eventSnapshot();
|
|
649
1058
|
const localEtag = snapshot?.local_etag || '';
|
|
650
1059
|
const remoteEtag = snapshot?.remote_etag || '';
|
|
651
1060
|
if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
|
|
@@ -665,24 +1074,76 @@ export class MessageDeliveryEngine {
|
|
|
665
1074
|
if (!isJsonObject(message))
|
|
666
1075
|
return true;
|
|
667
1076
|
const client = this.runtime.client;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (
|
|
1077
|
+
const explicitDevice = this.strictTargetString(message, 'target_device_id');
|
|
1078
|
+
if (explicitDevice.present) {
|
|
1079
|
+
if (!explicitDevice.valid || explicitDevice.value !== client._deviceId)
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
const targetDeviceId = this.boundString(message, ['device_id']);
|
|
1084
|
+
if (targetDeviceId.present && targetDeviceId.value !== client._deviceId) {
|
|
671
1085
|
return false;
|
|
672
1086
|
}
|
|
673
1087
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (
|
|
1088
|
+
const explicitSlot = this.strictTargetString(message, 'target_slot_id');
|
|
1089
|
+
if (explicitSlot.present) {
|
|
1090
|
+
if (!explicitSlot.valid
|
|
1091
|
+
|| slotIsolationKey(explicitSlot.value) !== slotIsolationKey(client._slotId))
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
const targetSlotId = this.boundString(message, ['slot_id']);
|
|
1096
|
+
if (targetSlotId.present
|
|
1097
|
+
&& slotIsolationKey(targetSlotId.value) !== slotIsolationKey(client._slotId)) {
|
|
677
1098
|
return false;
|
|
678
1099
|
}
|
|
679
1100
|
}
|
|
680
1101
|
return true;
|
|
681
1102
|
}
|
|
682
|
-
|
|
1103
|
+
strictTargetString(source, key) {
|
|
1104
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1105
|
+
return { present: false, valid: false, value: '' };
|
|
1106
|
+
}
|
|
1107
|
+
const raw = source[key];
|
|
1108
|
+
if (typeof raw !== 'string')
|
|
1109
|
+
return { present: true, valid: false, value: '' };
|
|
1110
|
+
const value = raw.trim();
|
|
1111
|
+
return { present: true, valid: Boolean(value), value };
|
|
1112
|
+
}
|
|
1113
|
+
async runPushProcessSerialized(ns, operation, inlineGeneration) {
|
|
683
1114
|
const client = this.runtime.client;
|
|
684
|
-
if (
|
|
1115
|
+
if (inlineGeneration === undefined) {
|
|
1116
|
+
if (!ns) {
|
|
1117
|
+
await operation();
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
let queues = client._pushProcessQueues;
|
|
1121
|
+
if (!queues) {
|
|
1122
|
+
queues = new Map();
|
|
1123
|
+
client._pushProcessQueues = queues;
|
|
1124
|
+
}
|
|
1125
|
+
const previous = queues.get(ns) ?? Promise.resolve();
|
|
1126
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
1127
|
+
const stored = current.then(() => undefined, () => undefined);
|
|
1128
|
+
queues.set(ns, stored);
|
|
1129
|
+
try {
|
|
1130
|
+
await current;
|
|
1131
|
+
}
|
|
1132
|
+
finally {
|
|
1133
|
+
if (queues.get(ns) === stored)
|
|
1134
|
+
queues.delete(ns);
|
|
1135
|
+
}
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const run = async () => {
|
|
1139
|
+
if (!this.isInlineGenerationCurrent(inlineGeneration))
|
|
1140
|
+
return;
|
|
685
1141
|
await operation();
|
|
1142
|
+
if (!this.isInlineGenerationCurrent(inlineGeneration))
|
|
1143
|
+
return;
|
|
1144
|
+
};
|
|
1145
|
+
if (!ns) {
|
|
1146
|
+
await run();
|
|
686
1147
|
return;
|
|
687
1148
|
}
|
|
688
1149
|
let queues = client._pushProcessQueues;
|
|
@@ -691,7 +1152,7 @@ export class MessageDeliveryEngine {
|
|
|
691
1152
|
client._pushProcessQueues = queues;
|
|
692
1153
|
}
|
|
693
1154
|
const previous = queues.get(ns) ?? Promise.resolve();
|
|
694
|
-
const current = previous.catch(() => undefined).then(
|
|
1155
|
+
const current = previous.catch(() => undefined).then(run);
|
|
695
1156
|
const stored = current.then(() => undefined, () => undefined);
|
|
696
1157
|
queues.set(ns, stored);
|
|
697
1158
|
try {
|
|
@@ -708,7 +1169,18 @@ export class MessageDeliveryEngine {
|
|
|
708
1169
|
const ns = isJsonObject(data) && client._aid && data.seq !== undefined
|
|
709
1170
|
? `p2p:${client._aid}`
|
|
710
1171
|
: '';
|
|
711
|
-
|
|
1172
|
+
const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
|
|
1173
|
+
if (client._v2Session && ns && Number.isSafeInteger(seq) && seq > 0
|
|
1174
|
+
&& this.messageTargetsCurrentInstance(data)) {
|
|
1175
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
1176
|
+
}
|
|
1177
|
+
const hasInlineObject = isJsonObject(data)
|
|
1178
|
+
&& isJsonObject(data.inline_message);
|
|
1179
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
1180
|
+
const operation = () => this.processAndPublishMessage(data, inlineGeneration);
|
|
1181
|
+
client._safeAsync(client._v2Session && ns && !hasInlineObject
|
|
1182
|
+
? operation()
|
|
1183
|
+
: this.runPushProcessSerialized(ns, operation, inlineGeneration));
|
|
712
1184
|
client._clientLog.debug('_onRawMessageReceived exit: elapsed=0ms (dispatched async)');
|
|
713
1185
|
}
|
|
714
1186
|
async onRawMessageRecalled(data) {
|
|
@@ -729,7 +1201,7 @@ export class MessageDeliveryEngine {
|
|
|
729
1201
|
const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
|
|
730
1202
|
const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
|
|
731
1203
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
732
|
-
if (seqNeedsPull && !published) {
|
|
1204
|
+
if (seqNeedsPull && !published && client._sessionOptions?.background_sync !== false) {
|
|
733
1205
|
client._safeAsync(this.fillP2pGap());
|
|
734
1206
|
}
|
|
735
1207
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -748,7 +1220,7 @@ export class MessageDeliveryEngine {
|
|
|
748
1220
|
}
|
|
749
1221
|
await this.publishMessageRecallTombstone(seq, msg);
|
|
750
1222
|
}
|
|
751
|
-
async processAndPublishMessage(data) {
|
|
1223
|
+
async processAndPublishMessage(data, inlineGeneration) {
|
|
752
1224
|
const client = this.runtime.client;
|
|
753
1225
|
try {
|
|
754
1226
|
if (!isJsonObject(data)) {
|
|
@@ -759,6 +1231,15 @@ export class MessageDeliveryEngine {
|
|
|
759
1231
|
if (!this.messageTargetsCurrentInstance(msg)) {
|
|
760
1232
|
return;
|
|
761
1233
|
}
|
|
1234
|
+
// 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
|
|
1235
|
+
// 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
|
|
1236
|
+
// 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
|
|
1237
|
+
const legacyPushSeq = positiveSafeSequenceHint(msg.seq);
|
|
1238
|
+
const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
1239
|
+
if (client._v2Session && client._aid && (legacyPushSeq > 0 || hasInlineField)) {
|
|
1240
|
+
await this.processV2P2PNotification(msg, 'v1', inlineGeneration);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
762
1243
|
const seq = msg.seq;
|
|
763
1244
|
const encryptedPush = client._isEncryptedPushMessage(msg);
|
|
764
1245
|
if (seq !== undefined && seq !== null && client._aid) {
|
|
@@ -772,7 +1253,7 @@ export class MessageDeliveryEngine {
|
|
|
772
1253
|
: await this.publishOrderedMessage('message.received', ns, seq, msg);
|
|
773
1254
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
774
1255
|
const needPull = seqNeedsPull && !published;
|
|
775
|
-
if (needPull) {
|
|
1256
|
+
if (needPull && client._sessionOptions?.background_sync !== false) {
|
|
776
1257
|
client._safeAsync(this.fillP2pGap());
|
|
777
1258
|
}
|
|
778
1259
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -818,11 +1299,24 @@ export class MessageDeliveryEngine {
|
|
|
818
1299
|
onRawGroupMessageCreated(data) {
|
|
819
1300
|
const client = this.runtime.client;
|
|
820
1301
|
client._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
|
|
821
|
-
const groupId = isJsonObject(data)
|
|
822
|
-
|
|
1302
|
+
const groupId = isJsonObject(data) && typeof data.group_id === 'string' ? data.group_id.trim() : '';
|
|
1303
|
+
const seq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
|
|
1304
|
+
const kind = isJsonObject(data) ? String(data.kind ?? '').trim() : '';
|
|
1305
|
+
if (client._v2Session && groupId && kind !== 'group.online_unread_hint'
|
|
1306
|
+
&& Number.isSafeInteger(seq) && seq > 0) {
|
|
1307
|
+
client._seqTracker.updateMaxSeen(`group:${groupId}`, seq);
|
|
1308
|
+
}
|
|
1309
|
+
const ns = groupId ? `group:${groupId}` : '';
|
|
1310
|
+
const hasInlineObject = isJsonObject(data)
|
|
1311
|
+
&& isJsonObject(data.inline_message);
|
|
1312
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
1313
|
+
const operation = () => this.processAndPublishGroupMessage(data, inlineGeneration);
|
|
1314
|
+
client._safeAsync(client._v2Session && ns && !hasInlineObject
|
|
1315
|
+
? operation()
|
|
1316
|
+
: this.runPushProcessSerialized(ns, operation, inlineGeneration));
|
|
823
1317
|
client._clientLog.debug('_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)');
|
|
824
1318
|
}
|
|
825
|
-
async processAndPublishGroupMessage(data) {
|
|
1319
|
+
async processAndPublishGroupMessage(data, inlineGeneration) {
|
|
826
1320
|
const client = this.runtime.client;
|
|
827
1321
|
try {
|
|
828
1322
|
if (!isJsonObject(data)) {
|
|
@@ -830,12 +1324,20 @@ export class MessageDeliveryEngine {
|
|
|
830
1324
|
return;
|
|
831
1325
|
}
|
|
832
1326
|
const msg = { ...data };
|
|
833
|
-
const groupId =
|
|
1327
|
+
const groupId = typeof msg.group_id === 'string' ? msg.group_id.trim() : '';
|
|
834
1328
|
const seq = msg.seq;
|
|
835
1329
|
const payload = msg.payload;
|
|
836
1330
|
if (groupId) {
|
|
837
1331
|
client._groupSynced.add(groupId);
|
|
838
1332
|
}
|
|
1333
|
+
// V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
|
|
1334
|
+
// 不能让 payload 路径直接推进旧 ordered publish 状态。
|
|
1335
|
+
const legacyPushSeq = positiveSafeSequenceHint(seq);
|
|
1336
|
+
const hasInlineField = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
1337
|
+
if (client._v2Session && groupId && (legacyPushSeq > 0 || hasInlineField)) {
|
|
1338
|
+
await this.processGroupV2MessageCreated(msg, 'v1', inlineGeneration);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
839
1341
|
if (payload === undefined || payload === null
|
|
840
1342
|
|| (typeof payload === 'object' && Object.keys(payload).length === 0)) {
|
|
841
1343
|
await this.autoPullGroupMessages(msg);
|
|
@@ -873,7 +1375,7 @@ export class MessageDeliveryEngine {
|
|
|
873
1375
|
: await this.publishOrderedMessage('group.message_created', ns, seq, msg);
|
|
874
1376
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
875
1377
|
const needPull = seqNeedsPull && !published;
|
|
876
|
-
if (needPull) {
|
|
1378
|
+
if (needPull && client._sessionOptions?.background_sync !== false) {
|
|
877
1379
|
client._safeAsync(this.fillGroupGap(groupId));
|
|
878
1380
|
}
|
|
879
1381
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -924,11 +1426,16 @@ export class MessageDeliveryEngine {
|
|
|
924
1426
|
await client._publishAppEvent('group.message_created', notification);
|
|
925
1427
|
return;
|
|
926
1428
|
}
|
|
927
|
-
|
|
1429
|
+
if (client._sessionOptions?.background_sync === false) {
|
|
1430
|
+
client._clientLog.debug(`auto pull group messages skipped: group=${groupId} background_sync=false`);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
928
1434
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
929
1435
|
try {
|
|
930
1436
|
if (client._v2Session) {
|
|
931
|
-
|
|
1437
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1438
|
+
await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
|
|
932
1439
|
return;
|
|
933
1440
|
}
|
|
934
1441
|
const result = await client.call('group.pull', {
|
|
@@ -994,6 +1501,8 @@ export class MessageDeliveryEngine {
|
|
|
994
1501
|
}
|
|
995
1502
|
async fillP2pGap(retryAttempt = 0) {
|
|
996
1503
|
const client = this.runtime.client;
|
|
1504
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1505
|
+
return;
|
|
997
1506
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
998
1507
|
return;
|
|
999
1508
|
if (!client._aid)
|
|
@@ -1007,7 +1516,8 @@ export class MessageDeliveryEngine {
|
|
|
1007
1516
|
this.runtime.delivery.setGapFillActive(true);
|
|
1008
1517
|
let filled = 0;
|
|
1009
1518
|
try {
|
|
1010
|
-
const
|
|
1519
|
+
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 });
|
|
1520
|
+
const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullV2(afterSeq, 50, { gateLocked: true }), true);
|
|
1011
1521
|
filled = messages.length;
|
|
1012
1522
|
this.prunePushedSeqs(ns);
|
|
1013
1523
|
}
|
|
@@ -1064,9 +1574,11 @@ export class MessageDeliveryEngine {
|
|
|
1064
1574
|
}
|
|
1065
1575
|
async fillGroupGap(groupId) {
|
|
1066
1576
|
const client = this.runtime.client;
|
|
1577
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1578
|
+
return;
|
|
1067
1579
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
1068
1580
|
return;
|
|
1069
|
-
groupId =
|
|
1581
|
+
groupId = String(groupId ?? '').trim();
|
|
1070
1582
|
if (!groupId)
|
|
1071
1583
|
return;
|
|
1072
1584
|
const ns = `group:${groupId}`;
|
|
@@ -1078,7 +1590,8 @@ export class MessageDeliveryEngine {
|
|
|
1078
1590
|
this.runtime.delivery.setGapFillActive(true);
|
|
1079
1591
|
let filled = 0;
|
|
1080
1592
|
try {
|
|
1081
|
-
const
|
|
1593
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1594
|
+
const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
|
|
1082
1595
|
filled = messages.length;
|
|
1083
1596
|
this.prunePushedSeqs(ns);
|
|
1084
1597
|
}
|
|
@@ -1095,9 +1608,11 @@ export class MessageDeliveryEngine {
|
|
|
1095
1608
|
}
|
|
1096
1609
|
async fillGroupEventGap(groupId) {
|
|
1097
1610
|
const client = this.runtime.client;
|
|
1611
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1612
|
+
return;
|
|
1098
1613
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
1099
1614
|
return;
|
|
1100
|
-
groupId =
|
|
1615
|
+
groupId = String(groupId ?? '').trim();
|
|
1101
1616
|
if (!groupId)
|
|
1102
1617
|
return;
|
|
1103
1618
|
const ns = `group_event:${groupId}`;
|
|
@@ -1131,12 +1646,16 @@ export class MessageDeliveryEngine {
|
|
|
1131
1646
|
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
|
|
1132
1647
|
}
|
|
1133
1648
|
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1649
|
+
const retentionFloor = Math.max(0, Number(result.retention_floor_event_seq ?? 0), Number(cursor?.retention_floor_seq ?? 0));
|
|
1650
|
+
const serverAckFloor = Math.max(0, Number(cursor?.current_seq ?? 0));
|
|
1651
|
+
const earliestAvailable = Number(result.earliest_available_event_seq ?? 0);
|
|
1652
|
+
const visibilityFloor = Math.max(0, Number(cursor?.join_seq ?? 0), Number.isFinite(earliestAvailable) && earliestAvailable > 0 ? earliestAvailable - 1 : 0);
|
|
1653
|
+
const effectiveFloor = Math.max(...[retentionFloor, serverAckFloor, visibilityFloor].filter((value) => Number.isFinite(value)));
|
|
1654
|
+
if (effectiveFloor > 0) {
|
|
1136
1655
|
const contigBeforeFloor = client._seqTracker.getContiguousSeq(ns);
|
|
1137
|
-
if (contigBeforeFloor <
|
|
1138
|
-
client._clientLog.info('group.pull_events
|
|
1139
|
-
client._seqTracker.forceContiguousSeq(ns,
|
|
1656
|
+
if (contigBeforeFloor < effectiveFloor) {
|
|
1657
|
+
client._clientLog.info('group.pull_events floor advance: ns=' + ns + ' contiguous=' + contigBeforeFloor + ' -> effective_floor=' + effectiveFloor + ' retention_floor=' + retentionFloor + ' server_ack=' + serverAckFloor + ' visibility_floor=' + visibilityFloor);
|
|
1658
|
+
client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
|
|
1140
1659
|
}
|
|
1141
1660
|
}
|
|
1142
1661
|
const eventSeqs = [];
|
|
@@ -1171,10 +1690,9 @@ export class MessageDeliveryEngine {
|
|
|
1171
1690
|
this.persistSeq(ns);
|
|
1172
1691
|
}
|
|
1173
1692
|
if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
|
|
1174
|
-
const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
|
|
1175
1693
|
client._transport.call('group.ack_events', {
|
|
1176
1694
|
group_id: groupId,
|
|
1177
|
-
event_seq:
|
|
1695
|
+
event_seq: ackContig,
|
|
1178
1696
|
device_id: client._deviceId,
|
|
1179
1697
|
slot_id: client._slotId,
|
|
1180
1698
|
_rpc_background: true,
|
|
@@ -1199,7 +1717,7 @@ export class MessageDeliveryEngine {
|
|
|
1199
1717
|
}
|
|
1200
1718
|
async handleGroupChangedEventSeq(data, groupId) {
|
|
1201
1719
|
const client = this.runtime.client;
|
|
1202
|
-
groupId =
|
|
1720
|
+
groupId = String(groupId ?? '').trim();
|
|
1203
1721
|
let needPull = false;
|
|
1204
1722
|
const rawEventSeq = data.event_seq;
|
|
1205
1723
|
const eventSeq = Number(rawEventSeq);
|
|
@@ -1208,6 +1726,15 @@ export class MessageDeliveryEngine {
|
|
|
1208
1726
|
return;
|
|
1209
1727
|
}
|
|
1210
1728
|
const ns = `group_event:${groupId}`;
|
|
1729
|
+
if (String(data.kind ?? '').trim() === 'group.online_unread_hint') {
|
|
1730
|
+
if (client._sessionOptions?.background_sync === false) {
|
|
1731
|
+
client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
client._clientLog.debug(`group.changed online unread hint triggering gap fill: group=${groupId}, event_seq=${eventSeq}`);
|
|
1735
|
+
client._safeAsync(this.fillGroupEventGap(groupId));
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1211
1738
|
if (this.isSelfJoinGroupChanged(data)) {
|
|
1212
1739
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
1213
1740
|
const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
|
|
@@ -1235,10 +1762,9 @@ export class MessageDeliveryEngine {
|
|
|
1235
1762
|
if (ackContig > 0 && ackContig !== contigBefore) {
|
|
1236
1763
|
if (data.action !== 'dissolved')
|
|
1237
1764
|
this.persistSeq(ns);
|
|
1238
|
-
const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
|
|
1239
1765
|
client._transport.call('group.ack_events', {
|
|
1240
1766
|
group_id: groupId,
|
|
1241
|
-
event_seq:
|
|
1767
|
+
event_seq: ackContig,
|
|
1242
1768
|
device_id: client._deviceId,
|
|
1243
1769
|
slot_id: client._slotId,
|
|
1244
1770
|
_rpc_background: true,
|
|
@@ -1246,7 +1772,7 @@ export class MessageDeliveryEngine {
|
|
|
1246
1772
|
client._clientLog.warn('group event push auto-ack failed: group=' + groupId, e);
|
|
1247
1773
|
});
|
|
1248
1774
|
}
|
|
1249
|
-
if (needPull && groupId && !data._from_gap_fill) {
|
|
1775
|
+
if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
|
|
1250
1776
|
client._safeAsync(this.fillGroupEventGap(groupId));
|
|
1251
1777
|
}
|
|
1252
1778
|
}
|
|
@@ -1282,12 +1808,11 @@ export class MessageDeliveryEngine {
|
|
|
1282
1808
|
if (client._onlineUnreadHintDrainActive)
|
|
1283
1809
|
return;
|
|
1284
1810
|
this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
|
|
1811
|
+
const attemptedAckByGroup = new Map();
|
|
1285
1812
|
try {
|
|
1286
1813
|
while (client._onlineUnreadHintQueue.size > 0) {
|
|
1287
1814
|
if (client.state !== ConnectionState.READY)
|
|
1288
1815
|
return;
|
|
1289
|
-
if (client._sessionOptions?.background_sync === false)
|
|
1290
|
-
return;
|
|
1291
1816
|
const groupId = client._onlineUnreadHintQueue.keys().next().value;
|
|
1292
1817
|
if (!groupId)
|
|
1293
1818
|
return;
|
|
@@ -1295,6 +1820,11 @@ export class MessageDeliveryEngine {
|
|
|
1295
1820
|
client._onlineUnreadHintQueue.delete(groupId);
|
|
1296
1821
|
if (!payload)
|
|
1297
1822
|
continue;
|
|
1823
|
+
const currentAck = this.syncState(`group:${groupId}`).ack;
|
|
1824
|
+
const attemptedAck = attemptedAckByGroup.get(groupId);
|
|
1825
|
+
if (attemptedAck !== undefined && currentAck <= attemptedAck)
|
|
1826
|
+
continue;
|
|
1827
|
+
attemptedAckByGroup.set(groupId, currentAck);
|
|
1298
1828
|
await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
|
|
1299
1829
|
const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
|
|
1300
1830
|
if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
|
|
@@ -1309,142 +1839,795 @@ export class MessageDeliveryEngine {
|
|
|
1309
1839
|
this.runtime.delivery.setOnlineUnreadHintDrainActive(false);
|
|
1310
1840
|
}
|
|
1311
1841
|
}
|
|
1312
|
-
|
|
1842
|
+
syncState(ns) {
|
|
1843
|
+
const tracker = this.runtime.client._seqTracker;
|
|
1844
|
+
if (typeof tracker.getSyncState === 'function')
|
|
1845
|
+
return tracker.getSyncState(ns);
|
|
1846
|
+
const ack = Number(tracker.getContiguousSeq(ns) || 0);
|
|
1847
|
+
return { ack, tail: ack, head: Math.max(ack, Number(tracker.getMaxSeenSeq?.(ns) || 0)) };
|
|
1848
|
+
}
|
|
1849
|
+
inlineMessage(data) {
|
|
1850
|
+
if (!isJsonObject(data))
|
|
1851
|
+
return null;
|
|
1852
|
+
const inline = data.inline_message;
|
|
1853
|
+
return isJsonObject(inline)
|
|
1854
|
+
? inline
|
|
1855
|
+
: null;
|
|
1856
|
+
}
|
|
1857
|
+
boundString(source, keys) {
|
|
1858
|
+
for (const key of keys) {
|
|
1859
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1860
|
+
return { present: true, value: String(source[key] ?? '').trim() };
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return { present: false, value: '' };
|
|
1864
|
+
}
|
|
1865
|
+
sameRequiredBinding(left, leftKeys, right, rightKeys) {
|
|
1866
|
+
const lhs = this.consistentBindingValue(left, leftKeys);
|
|
1867
|
+
const rhs = this.consistentBindingValue(right, rightKeys);
|
|
1868
|
+
return lhs.present && rhs.present && Boolean(lhs.value) && lhs.value === rhs.value;
|
|
1869
|
+
}
|
|
1870
|
+
consistentBindingValue(source, keys) {
|
|
1871
|
+
const binding = this.strictBoundString(source, keys);
|
|
1872
|
+
return binding.present && binding.valid
|
|
1873
|
+
? { present: true, value: binding.value }
|
|
1874
|
+
: { present: false, value: '' };
|
|
1875
|
+
}
|
|
1876
|
+
strictBoundString(source, keys) {
|
|
1877
|
+
let value = null;
|
|
1878
|
+
for (const key of keys) {
|
|
1879
|
+
if (!Object.prototype.hasOwnProperty.call(source, key))
|
|
1880
|
+
continue;
|
|
1881
|
+
const raw = source[key];
|
|
1882
|
+
if (typeof raw !== 'string')
|
|
1883
|
+
return { present: true, valid: false, value: '' };
|
|
1884
|
+
const candidate = raw.trim();
|
|
1885
|
+
if (!candidate || (value !== null && candidate !== value)) {
|
|
1886
|
+
return { present: true, valid: false, value: '' };
|
|
1887
|
+
}
|
|
1888
|
+
value = candidate;
|
|
1889
|
+
}
|
|
1890
|
+
return { present: value !== null, valid: value !== null, value: value ?? '' };
|
|
1891
|
+
}
|
|
1892
|
+
onlyEmptyOptionalStrings(source, keys) {
|
|
1893
|
+
for (const key of keys) {
|
|
1894
|
+
if (!Object.prototype.hasOwnProperty.call(source, key))
|
|
1895
|
+
continue;
|
|
1896
|
+
const raw = source[key];
|
|
1897
|
+
if (typeof raw !== 'string' || raw.trim())
|
|
1898
|
+
return false;
|
|
1899
|
+
}
|
|
1900
|
+
return true;
|
|
1901
|
+
}
|
|
1902
|
+
inlineTargetMatchesCurrentInstance(head, inline) {
|
|
1313
1903
|
const client = this.runtime.client;
|
|
1314
|
-
const
|
|
1315
|
-
if (!
|
|
1316
|
-
|
|
1317
|
-
|
|
1904
|
+
const explicitHeadDevice = this.strictTargetString(head, 'target_device_id');
|
|
1905
|
+
if (explicitHeadDevice.present && !explicitHeadDevice.valid)
|
|
1906
|
+
return false;
|
|
1907
|
+
const headDevice = explicitHeadDevice.present
|
|
1908
|
+
? explicitHeadDevice
|
|
1909
|
+
: this.strictBoundString(head, ['device_id']);
|
|
1910
|
+
if (!headDevice.present || !headDevice.valid
|
|
1911
|
+
|| headDevice.value !== String(client._deviceId ?? '').trim())
|
|
1912
|
+
return false;
|
|
1913
|
+
const explicitRowDevice = this.strictTargetString(inline, 'target_device_id');
|
|
1914
|
+
if (explicitRowDevice.present
|
|
1915
|
+
&& (!explicitRowDevice.valid || explicitRowDevice.value !== headDevice.value))
|
|
1916
|
+
return false;
|
|
1917
|
+
if (!explicitRowDevice.present) {
|
|
1918
|
+
const rowDevice = this.strictBoundString(inline, ['device_id']);
|
|
1919
|
+
if (rowDevice.present && (!rowDevice.valid || rowDevice.value !== headDevice.value))
|
|
1920
|
+
return false;
|
|
1318
1921
|
}
|
|
1319
|
-
const
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1922
|
+
const currentSlot = slotIsolationKey(String(client._slotId ?? ''));
|
|
1923
|
+
const explicitHeadSlot = this.strictTargetString(head, 'target_slot_id');
|
|
1924
|
+
if (explicitHeadSlot.present && !explicitHeadSlot.valid)
|
|
1925
|
+
return false;
|
|
1926
|
+
const headSlot = explicitHeadSlot.present
|
|
1927
|
+
? explicitHeadSlot
|
|
1928
|
+
: this.strictBoundString(head, ['slot_id']);
|
|
1929
|
+
if (headSlot.present && (!headSlot.valid || slotIsolationKey(headSlot.value) !== currentSlot))
|
|
1930
|
+
return false;
|
|
1931
|
+
const explicitRowSlot = this.strictTargetString(inline, 'target_slot_id');
|
|
1932
|
+
if (explicitRowSlot.present && (!explicitRowSlot.valid
|
|
1933
|
+
|| slotIsolationKey(explicitRowSlot.value) !== currentSlot
|
|
1934
|
+
|| (headSlot.present
|
|
1935
|
+
&& slotIsolationKey(explicitRowSlot.value) !== slotIsolationKey(headSlot.value))))
|
|
1936
|
+
return false;
|
|
1937
|
+
if (!explicitRowSlot.present) {
|
|
1938
|
+
const rowSlot = this.strictBoundString(inline, ['slot_id']);
|
|
1939
|
+
if (rowSlot.present && (!rowSlot.valid
|
|
1940
|
+
|| slotIsolationKey(rowSlot.value) !== currentSlot
|
|
1941
|
+
|| (headSlot.present
|
|
1942
|
+
&& slotIsolationKey(rowSlot.value) !== slotIsolationKey(headSlot.value))))
|
|
1943
|
+
return false;
|
|
1327
1944
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1945
|
+
return true;
|
|
1946
|
+
}
|
|
1947
|
+
inlineV2Envelope(inline) {
|
|
1948
|
+
const raw = inline.envelope_json;
|
|
1949
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
1950
|
+
return null;
|
|
1951
|
+
try {
|
|
1952
|
+
const envelope = JSON.parse(raw);
|
|
1953
|
+
return isJsonObject(envelope)
|
|
1954
|
+
? envelope
|
|
1955
|
+
: null;
|
|
1331
1956
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1957
|
+
catch {
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
isInlineV2Row(inline) {
|
|
1962
|
+
return inline.version === 'v2'
|
|
1963
|
+
&& typeof inline.envelope_json === 'string'
|
|
1964
|
+
&& Boolean(inline.envelope_json.trim());
|
|
1965
|
+
}
|
|
1966
|
+
inlineRecipientMatchesCurrentInstance(envelope) {
|
|
1967
|
+
if (Object.prototype.hasOwnProperty.call(envelope, 'recipients'))
|
|
1968
|
+
return false;
|
|
1969
|
+
if (!isJsonObject(envelope.recipient))
|
|
1970
|
+
return false;
|
|
1971
|
+
const recipient = envelope.recipient;
|
|
1972
|
+
const client = this.runtime.client;
|
|
1973
|
+
if (typeof recipient.aid !== 'string' || typeof recipient.device_id !== 'string'
|
|
1974
|
+
|| typeof client._aid !== 'string' || typeof client._deviceId !== 'string'
|
|
1975
|
+
|| !recipient.aid || !client._aid || !client._deviceId
|
|
1976
|
+
|| recipient.aid !== client._aid)
|
|
1977
|
+
return false;
|
|
1978
|
+
const hasDeliveryDevice = Object.prototype.hasOwnProperty.call(envelope, 'delivery_device_id');
|
|
1979
|
+
const deliveryDevice = envelope.delivery_device_id;
|
|
1980
|
+
if (hasDeliveryDevice
|
|
1981
|
+
&& (typeof deliveryDevice !== 'string' || !deliveryDevice || deliveryDevice !== client._deviceId))
|
|
1982
|
+
return false;
|
|
1983
|
+
if (recipient.device_id)
|
|
1984
|
+
return recipient.device_id === client._deviceId;
|
|
1985
|
+
return hasDeliveryDevice;
|
|
1986
|
+
}
|
|
1987
|
+
inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) {
|
|
1988
|
+
if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
|
|
1989
|
+
|| typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
|
|
1990
|
+
|| inline.version !== expectedInlineVersion
|
|
1991
|
+
|| !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
|
|
1992
|
+
|| !this.sameRequiredBinding(head, ['from_aid', 'sender_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
|
|
1993
|
+
|| !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
|
|
1994
|
+
|| !this.inlineTargetMatchesCurrentInstance(head, inline))
|
|
1995
|
+
return false;
|
|
1996
|
+
const logicalTo = this.consistentBindingValue(head, ['to_aid', 'to']);
|
|
1997
|
+
if (!logicalTo.present || !logicalTo.value)
|
|
1998
|
+
return false;
|
|
1999
|
+
if (expectedInlineVersion === 'v1') {
|
|
2000
|
+
if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
|
|
2001
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'payload')
|
|
2002
|
+
|| !isJsonObject(inline.legacy_v1))
|
|
2003
|
+
return false;
|
|
2004
|
+
const legacy = inline.legacy_v1;
|
|
2005
|
+
const payload = legacy.payload;
|
|
2006
|
+
if (payload === undefined || payload === null)
|
|
2007
|
+
return false;
|
|
2008
|
+
if (isJsonObject(payload)) {
|
|
2009
|
+
const payloadType = String(payload.type ?? '').trim();
|
|
2010
|
+
if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
2011
|
+
return false;
|
|
1337
2012
|
}
|
|
1338
|
-
this.
|
|
1339
|
-
return;
|
|
2013
|
+
const legacyTo = this.strictBoundString(legacy, ['to']);
|
|
2014
|
+
return legacyTo.present && legacyTo.valid && legacyTo.value === logicalTo.value;
|
|
2015
|
+
}
|
|
2016
|
+
if (!this.isInlineV2Row(inline)
|
|
2017
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
|
|
2018
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'payload')
|
|
2019
|
+
|| !this.onlyEmptyOptionalStrings(head, ['group_id', 'group_aid'])
|
|
2020
|
+
|| !this.onlyEmptyOptionalStrings(inline, ['group_id', 'group_aid']))
|
|
2021
|
+
return false;
|
|
2022
|
+
const envelope = this.inlineV2Envelope(inline);
|
|
2023
|
+
if (!envelope
|
|
2024
|
+
|| envelope.type !== 'e2ee.p2p_encrypted'
|
|
2025
|
+
|| envelope.version !== 'v2'
|
|
2026
|
+
|| !this.onlyEmptyOptionalStrings(envelope, ['group_id'])
|
|
2027
|
+
|| !this.inlineRecipientMatchesCurrentInstance(envelope))
|
|
2028
|
+
return false;
|
|
2029
|
+
if (!isJsonObject(envelope.aad))
|
|
2030
|
+
return false;
|
|
2031
|
+
const aad = envelope.aad;
|
|
2032
|
+
if (!this.onlyEmptyOptionalStrings(aad, ['group_id']))
|
|
2033
|
+
return false;
|
|
2034
|
+
const aadMessageId = this.consistentBindingValue(aad, ['message_id']);
|
|
2035
|
+
const aadFrom = this.consistentBindingValue(aad, ['from']);
|
|
2036
|
+
const aadFromDevice = this.consistentBindingValue(aad, ['from_device']);
|
|
2037
|
+
const aadTo = this.consistentBindingValue(aad, ['to']);
|
|
2038
|
+
if (!aadMessageId.present || aadMessageId.value !== this.consistentBindingValue(inline, ['message_id']).value
|
|
2039
|
+
|| !aadFrom.present || aadFrom.value !== this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
|
|
2040
|
+
|| !aadFromDevice.present || aadFromDevice.value !== this.consistentBindingValue(inline, ['sender_device_id']).value
|
|
2041
|
+
|| !aadTo.present)
|
|
2042
|
+
return false;
|
|
2043
|
+
if (aadTo.value === logicalTo.value)
|
|
2044
|
+
return true;
|
|
2045
|
+
const recipient = envelope.recipient;
|
|
2046
|
+
return typeof recipient.role === 'string'
|
|
2047
|
+
&& recipient.role.trim() === 'self_sync'
|
|
2048
|
+
&& aadTo.value.length > 0
|
|
2049
|
+
&& aadFrom.value === String(this.runtime.client._aid ?? '').trim()
|
|
2050
|
+
&& logicalTo.value === String(this.runtime.client._aid ?? '').trim();
|
|
2051
|
+
}
|
|
2052
|
+
inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) {
|
|
2053
|
+
const headGroupId = this.strictBoundString(head, ['group_id']);
|
|
2054
|
+
const rowGroupId = this.strictBoundString(inline, ['group_id']);
|
|
2055
|
+
const headGroupAid = this.strictBoundString(head, ['group_aid']);
|
|
2056
|
+
const rowGroupAid = this.strictBoundString(inline, ['group_aid']);
|
|
2057
|
+
if (typeof head.seq !== 'number' || !Number.isSafeInteger(head.seq) || head.seq <= 0 || head.seq !== seq
|
|
2058
|
+
|| typeof inline.seq !== 'number' || !Number.isSafeInteger(inline.seq) || inline.seq <= 0 || inline.seq !== seq
|
|
2059
|
+
|| inline.version !== expectedInlineVersion
|
|
2060
|
+
|| !headGroupId.present || !headGroupId.valid || headGroupId.value !== groupId
|
|
2061
|
+
|| !rowGroupId.present || !rowGroupId.valid || rowGroupId.value !== groupId
|
|
2062
|
+
|| (headGroupAid.present && !headGroupAid.valid)
|
|
2063
|
+
|| (rowGroupAid.present && !rowGroupAid.valid)
|
|
2064
|
+
|| !this.sameRequiredBinding(head, ['message_id'], inline, ['message_id'])
|
|
2065
|
+
|| !this.sameRequiredBinding(head, ['sender_aid', 'from_aid', 'from'], inline, ['from_aid', 'sender_aid', 'from'])
|
|
2066
|
+
|| !this.sameRequiredBinding(head, ['sender_device_id'], inline, ['sender_device_id'])
|
|
2067
|
+
|| !this.inlineTargetMatchesCurrentInstance(head, inline))
|
|
2068
|
+
return false;
|
|
2069
|
+
if (expectedInlineVersion === 'v1') {
|
|
2070
|
+
if (Object.prototype.hasOwnProperty.call(inline, 'envelope_json')
|
|
2071
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
|
|
2072
|
+
|| !Object.prototype.hasOwnProperty.call(inline, 'payload')
|
|
2073
|
+
|| inline.payload === null
|
|
2074
|
+
|| inline.payload === undefined)
|
|
2075
|
+
return false;
|
|
2076
|
+
if (isJsonObject(inline.payload)) {
|
|
2077
|
+
const payloadType = String(inline.payload.type ?? '').trim();
|
|
2078
|
+
if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
2079
|
+
return false;
|
|
2080
|
+
}
|
|
2081
|
+
return true;
|
|
1340
2082
|
}
|
|
2083
|
+
if (!this.isInlineV2Row(inline)
|
|
2084
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'legacy_v1')
|
|
2085
|
+
|| Object.prototype.hasOwnProperty.call(inline, 'payload'))
|
|
2086
|
+
return false;
|
|
2087
|
+
const envelope = this.inlineV2Envelope(inline);
|
|
2088
|
+
const envelopeGroupId = envelope ? this.strictBoundString(envelope, ['group_id']) : null;
|
|
2089
|
+
if (!envelope
|
|
2090
|
+
|| envelope.type !== 'e2ee.group_encrypted'
|
|
2091
|
+
|| envelope.version !== 'v2'
|
|
2092
|
+
|| (envelopeGroupId?.present && (!envelopeGroupId.valid || envelopeGroupId.value !== groupId))
|
|
2093
|
+
|| !this.inlineRecipientMatchesCurrentInstance(envelope))
|
|
2094
|
+
return false;
|
|
2095
|
+
if (!isJsonObject(envelope.aad))
|
|
2096
|
+
return false;
|
|
2097
|
+
const aad = envelope.aad;
|
|
2098
|
+
return this.consistentBindingValue(aad, ['message_id']).value === this.consistentBindingValue(inline, ['message_id']).value
|
|
2099
|
+
&& this.consistentBindingValue(aad, ['from']).value === this.consistentBindingValue(inline, ['from_aid', 'sender_aid', 'from']).value
|
|
2100
|
+
&& this.consistentBindingValue(aad, ['from_device']).value === this.consistentBindingValue(inline, ['sender_device_id']).value
|
|
2101
|
+
&& this.consistentBindingValue(aad, ['group_id']).value === groupId;
|
|
2102
|
+
}
|
|
2103
|
+
inlineWindowReady(ns, seq) {
|
|
2104
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
2105
|
+
return false;
|
|
2106
|
+
const expected = seq - 1;
|
|
2107
|
+
const state = this.syncState(ns);
|
|
2108
|
+
if (state.ack !== expected || state.tail !== expected || state.head !== expected)
|
|
2109
|
+
return false;
|
|
2110
|
+
const client = this.runtime.client;
|
|
2111
|
+
// 入口会先登记当前 Push 的 Head,因此 maxSeen==seq 仍是合法的
|
|
2112
|
+
// 单条 inline 快路径;只有已经观察到更高序号时才退回同步路径。
|
|
2113
|
+
if (Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? expected) > seq)
|
|
2114
|
+
return false;
|
|
2115
|
+
const pending = client._pendingOrderedMsgs.get(ns);
|
|
2116
|
+
return !pending || pending.size === 0;
|
|
2117
|
+
}
|
|
2118
|
+
decodedInlineP2PMatches(head, decoded, seq) {
|
|
2119
|
+
const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
|
|
2120
|
+
const headMessageId = this.consistentBindingValue(head, ['message_id']);
|
|
2121
|
+
const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
|
|
2122
|
+
const headFrom = this.consistentBindingValue(head, ['from_aid', 'sender_aid', 'from']);
|
|
2123
|
+
const decodedTo = this.consistentBindingValue(decoded, ['to', 'to_aid']);
|
|
2124
|
+
const headTo = this.consistentBindingValue(head, ['to_aid', 'to']);
|
|
2125
|
+
return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
|
|
2126
|
+
&& decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
|
|
2127
|
+
&& decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
|
|
2128
|
+
&& decodedTo.present && headTo.present && decodedTo.value === headTo.value;
|
|
2129
|
+
}
|
|
2130
|
+
decodedInlineGroupMatches(head, decoded, groupId, seq) {
|
|
2131
|
+
const decodedMessageId = this.consistentBindingValue(decoded, ['message_id']);
|
|
2132
|
+
const headMessageId = this.consistentBindingValue(head, ['message_id']);
|
|
2133
|
+
const decodedFrom = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
|
|
2134
|
+
const headFrom = this.consistentBindingValue(head, ['sender_aid', 'from_aid', 'from']);
|
|
2135
|
+
const decodedGroupId = this.consistentBindingValue(decoded, ['group_id']);
|
|
2136
|
+
return typeof decoded.seq === 'number' && Number.isSafeInteger(decoded.seq) && decoded.seq === seq
|
|
2137
|
+
&& decodedMessageId.present && headMessageId.present && decodedMessageId.value === headMessageId.value
|
|
2138
|
+
&& decodedFrom.present && headFrom.present && decodedFrom.value === headFrom.value
|
|
2139
|
+
&& decodedGroupId.present && decodedGroupId.value === groupId;
|
|
2140
|
+
}
|
|
2141
|
+
async commitInlineMessage(ns, seq, event, payload, generation) {
|
|
2142
|
+
const client = this.runtime.client;
|
|
2143
|
+
const tracker = client._seqTracker;
|
|
2144
|
+
const snapshot = typeof tracker.snapshotNamespace === 'function'
|
|
2145
|
+
? tracker.snapshotNamespace(ns)
|
|
2146
|
+
: null;
|
|
1341
2147
|
try {
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if (
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
2148
|
+
const published = await this.publishPulledMessage(event, ns, seq, payload);
|
|
2149
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2150
|
+
return false;
|
|
2151
|
+
if (!published)
|
|
2152
|
+
return false;
|
|
2153
|
+
const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
|
|
2154
|
+
const committed = this.syncState(ns);
|
|
2155
|
+
if (needsPull || committed.ack !== seq || committed.tail !== seq || committed.head !== seq) {
|
|
2156
|
+
throw new Error('inline realtime cursor did not collapse after consume');
|
|
1349
2157
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
return;
|
|
2158
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2159
|
+
return false;
|
|
2160
|
+
await client._commitSeqTrackerState(ns);
|
|
2161
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2162
|
+
return false;
|
|
2163
|
+
return true;
|
|
2164
|
+
}
|
|
2165
|
+
catch (exc) {
|
|
2166
|
+
if (this.isInlineGenerationCurrent(generation) && typeof tracker.restoreNamespaceSnapshot === 'function') {
|
|
2167
|
+
tracker.restoreNamespaceSnapshot(ns, snapshot);
|
|
2168
|
+
}
|
|
2169
|
+
client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
async tryConsumeInlineP2P(head, inline, ns, seq, expectedInlineVersion, generation) {
|
|
2174
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2175
|
+
|| !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
|
|
2176
|
+
return false;
|
|
2177
|
+
const client = this.runtime.client;
|
|
2178
|
+
try {
|
|
2179
|
+
const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
|
|
2180
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2181
|
+
|| !this.inlineWindowReady(ns, seq)
|
|
2182
|
+
|| !this.inlineP2PBindingMatches(head, inline, seq, expectedInlineVersion)
|
|
2183
|
+
|| !this.reservePendingP2PInlineAck(ns, seq, inline, generation))
|
|
2184
|
+
return false;
|
|
2185
|
+
let committed = false;
|
|
2186
|
+
try {
|
|
2187
|
+
const decoded = await client._v2E2EE.decodeInlineP2PMessage(inline);
|
|
2188
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2189
|
+
|| !decoded || !this.decodedInlineP2PMatches(head, decoded, seq)
|
|
2190
|
+
|| !this.inlineWindowReady(ns, seq))
|
|
2191
|
+
return false;
|
|
2192
|
+
const appEvent = this.p2pAppEventForMessage(decoded);
|
|
2193
|
+
if (!await this.commitInlineMessage(ns, seq, appEvent.event, appEvent.payload, generation))
|
|
2194
|
+
return false;
|
|
2195
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2196
|
+
return false;
|
|
2197
|
+
committed = this.markPendingP2PInlineAckCommitted(ns, seq, generation);
|
|
2198
|
+
return committed;
|
|
2199
|
+
}
|
|
2200
|
+
finally {
|
|
2201
|
+
if (!committed && this.isInlineGenerationCurrent(generation))
|
|
2202
|
+
this.dropPendingP2PInlineAck(ns, seq, generation);
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
|
|
2206
|
+
return consumed;
|
|
2207
|
+
}
|
|
2208
|
+
catch (exc) {
|
|
2209
|
+
client._clientLog.debug(`inline P2P consume failed: seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
async tryConsumeInlineGroup(head, inline, groupId, ns, seq, expectedInlineVersion, generation) {
|
|
2214
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2215
|
+
|| !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion) || !this.inlineWindowReady(ns, seq))
|
|
2216
|
+
return false;
|
|
2217
|
+
const client = this.runtime.client;
|
|
2218
|
+
const groupAid = typeof head.group_aid === 'string' && head.group_aid.trim()
|
|
2219
|
+
? head.group_aid.trim()
|
|
2220
|
+
: groupId;
|
|
2221
|
+
try {
|
|
2222
|
+
const result = await client._rpcPipeline.tryRunInlineRealtime(ns, seq, async () => {
|
|
2223
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2224
|
+
|| !this.inlineWindowReady(ns, seq)
|
|
2225
|
+
|| !this.inlineGroupBindingMatches(head, inline, groupId, seq, expectedInlineVersion)
|
|
2226
|
+
|| !this.reservePendingGroupInlineAck(ns, seq, inline, groupAid, groupId, generation))
|
|
2227
|
+
return false;
|
|
2228
|
+
let committed = false;
|
|
2229
|
+
try {
|
|
2230
|
+
const decoded = await client._v2E2EE.decodeInlineGroupMessage(inline, groupId, groupAid);
|
|
2231
|
+
if (!this.isInlineGenerationCurrent(generation)
|
|
2232
|
+
|| !decoded || !this.decodedInlineGroupMatches(head, decoded, groupId, seq)
|
|
2233
|
+
|| !this.inlineWindowReady(ns, seq))
|
|
2234
|
+
return false;
|
|
2235
|
+
if (!await this.commitInlineMessage(ns, seq, 'group.message_created', decoded, generation))
|
|
2236
|
+
return false;
|
|
2237
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
2238
|
+
return false;
|
|
2239
|
+
committed = this.markPendingGroupInlineAckCommitted(ns, seq, generation);
|
|
2240
|
+
return committed;
|
|
2241
|
+
}
|
|
2242
|
+
finally {
|
|
2243
|
+
if (!committed && this.isInlineGenerationCurrent(generation))
|
|
2244
|
+
this.dropPendingGroupInlineAck(ns, seq, generation);
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
const consumed = this.isInlineGenerationCurrent(generation) && result.acquired === true && result.value === true;
|
|
2248
|
+
return consumed;
|
|
2249
|
+
}
|
|
2250
|
+
catch (exc) {
|
|
2251
|
+
client._clientLog.debug(`inline Group consume failed: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2252
|
+
return false;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
hasOutstandingRealtimeGap(ns) {
|
|
2256
|
+
const maxSeenSeq = this.runtime.client._seqTracker.getMaxSeenSeq(ns);
|
|
2257
|
+
return Number.isSafeInteger(maxSeenSeq) && maxSeenSeq > this.syncState(ns).ack;
|
|
2258
|
+
}
|
|
2259
|
+
scheduleP2PRealtimeFullGapFill(ns) {
|
|
2260
|
+
const client = this.runtime.client;
|
|
2261
|
+
const currentNs = client._aid ? `p2p:${client._aid}` : '';
|
|
2262
|
+
if (client._sessionOptions?.background_sync === false
|
|
2263
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2264
|
+
|| currentNs !== ns || !this.hasOutstandingRealtimeGap(ns))
|
|
2265
|
+
return;
|
|
2266
|
+
const scheduleKey = `p2p_realtime_full_gap:${ns}`;
|
|
2267
|
+
if (client._gapFillDone.has(scheduleKey))
|
|
2268
|
+
return;
|
|
2269
|
+
client._gapFillDone.add(scheduleKey);
|
|
2270
|
+
client._safeAsync(Promise.resolve().then(async () => {
|
|
2271
|
+
try {
|
|
2272
|
+
const liveNs = client._aid ? `p2p:${client._aid}` : '';
|
|
2273
|
+
if (client._sessionOptions?.background_sync === false
|
|
2274
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2275
|
+
|| liveNs !== ns || !this.hasOutstandingRealtimeGap(ns))
|
|
2276
|
+
return;
|
|
2277
|
+
await this.fillP2pGap();
|
|
2278
|
+
}
|
|
2279
|
+
finally {
|
|
2280
|
+
client._gapFillDone.delete(scheduleKey);
|
|
1355
2281
|
}
|
|
1356
|
-
|
|
2282
|
+
}));
|
|
2283
|
+
}
|
|
2284
|
+
scheduleGroupRealtimeFullGapFill(groupId, ns) {
|
|
2285
|
+
const client = this.runtime.client;
|
|
2286
|
+
groupId = String(groupId ?? '').trim();
|
|
2287
|
+
if (!groupId || client._sessionOptions?.background_sync === false
|
|
2288
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2289
|
+
|| !this.hasOutstandingRealtimeGap(ns))
|
|
2290
|
+
return;
|
|
2291
|
+
const scheduleKey = `group_realtime_full_gap:${ns}`;
|
|
2292
|
+
if (client._gapFillDone.has(scheduleKey))
|
|
2293
|
+
return;
|
|
2294
|
+
client._gapFillDone.add(scheduleKey);
|
|
2295
|
+
client._safeAsync(Promise.resolve().then(async () => {
|
|
1357
2296
|
try {
|
|
1358
|
-
client.
|
|
1359
|
-
|
|
1360
|
-
|
|
2297
|
+
if (client._sessionOptions?.background_sync === false
|
|
2298
|
+
|| client.state !== ConnectionState.READY || client._closing
|
|
2299
|
+
|| !this.hasOutstandingRealtimeGap(ns))
|
|
2300
|
+
return;
|
|
2301
|
+
await this.fillGroupGap(groupId);
|
|
1361
2302
|
}
|
|
1362
2303
|
finally {
|
|
1363
|
-
client._gapFillDone.delete(
|
|
2304
|
+
client._gapFillDone.delete(scheduleKey);
|
|
1364
2305
|
}
|
|
2306
|
+
}));
|
|
2307
|
+
}
|
|
2308
|
+
async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
2309
|
+
const client = this.runtime.client;
|
|
2310
|
+
const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
|
|
2311
|
+
let result = { messages: [], raw_count: 0 };
|
|
2312
|
+
let tailCompleted = false;
|
|
2313
|
+
const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
|
|
2314
|
+
const live = this.syncState(ns);
|
|
2315
|
+
if (tail && !bypassCoveredHead && pushSeq <= live.head)
|
|
2316
|
+
return { messages: [], raw_count: 0 };
|
|
2317
|
+
if (!tail && !forceForward && live.ack >= live.tail - 1)
|
|
2318
|
+
return { messages: [], raw_count: 0 };
|
|
2319
|
+
const request = { ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
|
|
2320
|
+
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
|
|
2321
|
+
return await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
2322
|
+
if (tail) {
|
|
2323
|
+
const tailResult = await client._v2E2EE.pullV2TailInternal({
|
|
2324
|
+
window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
|
|
2325
|
+
});
|
|
2326
|
+
tailCompleted = true;
|
|
2327
|
+
return tailResult;
|
|
2328
|
+
}
|
|
2329
|
+
const messages = await client._pullV2(live.ack, pageLimit, {
|
|
2330
|
+
gateLocked: true,
|
|
2331
|
+
...(maxPages ? { maxPages } : {}),
|
|
2332
|
+
});
|
|
2333
|
+
return { messages, raw_count: messages.length };
|
|
2334
|
+
}, background);
|
|
2335
|
+
};
|
|
2336
|
+
if (forceTail || order[0] === 'tail')
|
|
2337
|
+
result = await run(true, false, undefined, false, forceTail);
|
|
2338
|
+
const state = this.syncState(ns);
|
|
2339
|
+
// Tail 的连续性证明只覆盖其返回的 head。Tail 进行期间到达的新
|
|
2340
|
+
// Push 不能再次进入 realtime planner(那会再做一次 Tail);以已经
|
|
2341
|
+
// 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
|
|
2342
|
+
const tailMissedDuringPull = tailCompleted
|
|
2343
|
+
&& Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
|
|
2344
|
+
let boundedForwardForMissedHead = false;
|
|
2345
|
+
if (client._sessionOptions?.background_sync !== false
|
|
2346
|
+
&& (order[0] === 'forward'
|
|
2347
|
+
|| (tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull)))) {
|
|
2348
|
+
result = await run(false, true, order[0] === 'tail' ? 1 : undefined, tailMissedDuringPull);
|
|
2349
|
+
boundedForwardForMissedHead = order[0] === 'tail' && tailMissedDuringPull;
|
|
2350
|
+
}
|
|
2351
|
+
if (boundedForwardForMissedHead)
|
|
2352
|
+
this.scheduleP2PRealtimeFullGapFill(ns);
|
|
2353
|
+
if (tailCompleted)
|
|
2354
|
+
this.markRealtimeTailCompleted(result);
|
|
2355
|
+
return result;
|
|
2356
|
+
}
|
|
2357
|
+
async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
2358
|
+
const client = this.runtime.client;
|
|
2359
|
+
const order = client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit);
|
|
2360
|
+
let result = { messages: [], raw_count: 0 };
|
|
2361
|
+
let tailCompleted = false;
|
|
2362
|
+
const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
|
|
2363
|
+
const live = this.syncState(ns);
|
|
2364
|
+
if (tail && !bypassCoveredHead && pushSeq <= live.head)
|
|
2365
|
+
return { messages: [], raw_count: 0 };
|
|
2366
|
+
if (!tail && !forceForward && live.ack >= live.tail - 1)
|
|
2367
|
+
return { messages: [], raw_count: 0 };
|
|
2368
|
+
const request = { group_id: groupId, ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
|
|
2369
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
2370
|
+
return await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
2371
|
+
if (tail) {
|
|
2372
|
+
const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
|
|
2373
|
+
group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
|
|
2374
|
+
});
|
|
2375
|
+
tailCompleted = true;
|
|
2376
|
+
return tailResult;
|
|
2377
|
+
}
|
|
2378
|
+
const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
|
|
2379
|
+
gateLocked: true,
|
|
2380
|
+
...(maxPages ? { maxPages } : {}),
|
|
2381
|
+
});
|
|
2382
|
+
return { messages, raw_count: messages.length };
|
|
2383
|
+
}, background);
|
|
2384
|
+
};
|
|
2385
|
+
if (forceTail || order[0] === 'tail')
|
|
2386
|
+
result = await run(true, false, undefined, false, forceTail);
|
|
2387
|
+
const state = this.syncState(ns);
|
|
2388
|
+
// 与 P2P 相同:真实完成 Tail 后,至多用一页 Forward 收敛 Tail
|
|
2389
|
+
// 期间新到达的序号;绝不重新规划或再执行 Tail。
|
|
2390
|
+
const tailMissedDuringPull = tailCompleted
|
|
2391
|
+
&& Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
|
|
2392
|
+
let boundedForwardForMissedHead = false;
|
|
2393
|
+
if (client._sessionOptions?.background_sync !== false
|
|
2394
|
+
&& (order[0] === 'forward'
|
|
2395
|
+
|| (tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull)))) {
|
|
2396
|
+
result = await run(false, true, order[0] === 'tail' ? 1 : undefined, tailMissedDuringPull);
|
|
2397
|
+
boundedForwardForMissedHead = order[0] === 'tail' && tailMissedDuringPull;
|
|
2398
|
+
}
|
|
2399
|
+
if (boundedForwardForMissedHead)
|
|
2400
|
+
this.scheduleGroupRealtimeFullGapFill(groupId, ns);
|
|
2401
|
+
if (tailCompleted)
|
|
2402
|
+
this.markRealtimeTailCompleted(result);
|
|
2403
|
+
return result;
|
|
2404
|
+
}
|
|
2405
|
+
async runGroupOnlineUnreadHintForward(groupId, ns, pageLimit = 50) {
|
|
2406
|
+
const client = this.runtime.client;
|
|
2407
|
+
const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
|
|
2408
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
2409
|
+
return await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
2410
|
+
const live = this.syncState(ns);
|
|
2411
|
+
const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
|
|
2412
|
+
gateLocked: true, maxPages: 1,
|
|
2413
|
+
});
|
|
2414
|
+
return { messages, raw_count: messages.length };
|
|
2415
|
+
}, true);
|
|
2416
|
+
}
|
|
2417
|
+
async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
|
|
2418
|
+
const client = this.runtime.client;
|
|
2419
|
+
client._clientLog.warn(`V2 P2P inline Head has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
|
|
2420
|
+
if (!this.beginRealtimeSync(ns))
|
|
2421
|
+
return;
|
|
2422
|
+
try {
|
|
2423
|
+
await this.runP2PRealtimeSync(ns, this.syncState(ns).head, 50, true);
|
|
1365
2424
|
}
|
|
1366
2425
|
catch (exc) {
|
|
1367
|
-
client._clientLog.warn(`
|
|
2426
|
+
client._clientLog.warn(`V2 P2P invalid-seq Tail failed: ${formatDeliveryError(exc)}`);
|
|
1368
2427
|
}
|
|
1369
2428
|
finally {
|
|
1370
|
-
|
|
2429
|
+
this.endRealtimeSync(ns);
|
|
1371
2430
|
}
|
|
1372
2431
|
}
|
|
1373
|
-
async
|
|
2432
|
+
async recoverInvalidGroupRealtimeHead(groupId, ns, rawSeq) {
|
|
1374
2433
|
const client = this.runtime.client;
|
|
1375
|
-
|
|
2434
|
+
client._clientLog.warn(`V2 Group inline Head has invalid seq; forcing authoritative Tail: group=${groupId} seq=${String(rawSeq)}`);
|
|
2435
|
+
if (!this.beginRealtimeSync(ns))
|
|
1376
2436
|
return;
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
2437
|
+
try {
|
|
2438
|
+
await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, 50, true);
|
|
2439
|
+
}
|
|
2440
|
+
catch (exc) {
|
|
2441
|
+
client._clientLog.warn(`V2 Group invalid-seq Tail failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2442
|
+
}
|
|
2443
|
+
finally {
|
|
2444
|
+
this.endRealtimeSync(ns);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
async onRawGroupV2MessageCreated(data) {
|
|
2448
|
+
const client = this.runtime.client;
|
|
2449
|
+
const row = isJsonObject(data) ? data : null;
|
|
2450
|
+
if (!row)
|
|
2451
|
+
return;
|
|
2452
|
+
const groupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
|
|
2453
|
+
if (!groupId)
|
|
2454
|
+
return;
|
|
2455
|
+
const seq = typeof row.seq === 'number' ? row.seq : 0;
|
|
2456
|
+
const eventKind = String(row.kind ?? '').trim();
|
|
2457
|
+
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
2458
|
+
if (client._v2Session && eventKind !== 'group.online_unread_hint'
|
|
2459
|
+
&& Number.isSafeInteger(seq) && seq > 0) {
|
|
2460
|
+
// 必须在进入 namespace 队列前登记,才能让在途 Tail 看见后来 Push。
|
|
2461
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
2462
|
+
}
|
|
2463
|
+
const hasInlineObject = isJsonObject(row.inline_message)
|
|
2464
|
+
&& eventKind !== 'group.online_unread_hint';
|
|
2465
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
2466
|
+
const operation = () => this.processGroupV2MessageCreated(data, 'v2', inlineGeneration);
|
|
2467
|
+
if (hasInlineObject)
|
|
2468
|
+
await this.runPushProcessSerialized(ns, operation, inlineGeneration);
|
|
2469
|
+
else
|
|
2470
|
+
await operation();
|
|
2471
|
+
}
|
|
2472
|
+
async processGroupV2MessageCreated(data, expectedInlineVersion, queuedInlineGeneration) {
|
|
2473
|
+
if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
|
|
2474
|
+
return;
|
|
2475
|
+
const client = this.runtime.client;
|
|
2476
|
+
if (!isJsonObject(data) || !client._v2Session)
|
|
2477
|
+
return;
|
|
2478
|
+
const groupId = typeof data.group_id === 'string' ? data.group_id.trim() : '';
|
|
2479
|
+
const seq = positiveSafeSequenceHint(data.seq);
|
|
2480
|
+
const strictSeq = typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
|
|
2481
|
+
if (!groupId)
|
|
2482
|
+
return;
|
|
2483
|
+
const eventKind = String(data.kind ?? '').trim();
|
|
2484
|
+
// Hint 是恢复信号而不是消息 Head:不得消费 inline、推进水位或直接 ACK。
|
|
2485
|
+
if (eventKind === 'group.online_unread_hint') {
|
|
2486
|
+
if (!data._online_hint_drained) {
|
|
2487
|
+
if (client._sessionOptions?.background_sync === false)
|
|
2488
|
+
return;
|
|
2489
|
+
this.enqueueOnlineUnreadHint(data);
|
|
1389
2490
|
return;
|
|
1390
2491
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if (hasPayload && pushSeq > 0 && ns) {
|
|
2492
|
+
if (client._sessionOptions?.background_sync === false)
|
|
2493
|
+
return;
|
|
1394
2494
|
try {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const needPull = client._seqTracker.onMessageSeq(ns, pushSeq);
|
|
1398
|
-
const published = await client._publishOrderedMessage('message.received', ns, pushSeq, decrypted);
|
|
1399
|
-
const newContig = client._seqTracker.getContiguousSeq(ns);
|
|
1400
|
-
if (newContig !== contigBefore) {
|
|
1401
|
-
this.persistSeq(ns);
|
|
1402
|
-
}
|
|
1403
|
-
if (newContig > 0 && newContig !== contigBefore) {
|
|
1404
|
-
const ackSeq = this.clampAckSeq('message.v2.ack', 'up_to_seq', ns, newContig);
|
|
1405
|
-
try {
|
|
1406
|
-
await client._callRawV2Rpc('message.v2.ack', { up_to_seq: ackSeq, _rpc_background: true });
|
|
1407
|
-
}
|
|
1408
|
-
catch (e) {
|
|
1409
|
-
client._clientLog.debug(`V2 P2P push-ack failed: ${e}`);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
client._clientLog.debug(`_onV2PushNotification: push 带 payload 解密成功, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`);
|
|
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
|
-
}
|
|
2495
|
+
// 只从已提交 Ack 做一页普通 Forward,不能由 planner 把 hint 当 Tail 证据。
|
|
2496
|
+
await this.runGroupOnlineUnreadHintForward(groupId, `group:${groupId}`);
|
|
1418
2497
|
}
|
|
1419
2498
|
catch (exc) {
|
|
1420
|
-
client._clientLog.
|
|
2499
|
+
client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
1421
2500
|
}
|
|
2501
|
+
return;
|
|
1422
2502
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
2503
|
+
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
2504
|
+
const inlinePresent = Object.prototype.hasOwnProperty.call(data, 'inline_message');
|
|
2505
|
+
if (seq <= 0) {
|
|
2506
|
+
if (inlinePresent)
|
|
2507
|
+
await this.recoverInvalidGroupRealtimeHead(groupId, ns, data.seq);
|
|
2508
|
+
return;
|
|
1425
2509
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
2510
|
+
const inline = this.inlineMessage(data);
|
|
2511
|
+
const inlineMatches = inline !== null
|
|
2512
|
+
&& this.inlineGroupBindingMatches(data, inline, groupId, seq, expectedInlineVersion);
|
|
2513
|
+
const pendingInline = this.pendingGroupInlineAcks?.get(ns);
|
|
2514
|
+
const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== seq;
|
|
2515
|
+
const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
|
|
2516
|
+
if (inlineMatches && !pendingConflict
|
|
2517
|
+
&& await this.tryConsumeInlineGroup(data, inline, groupId, ns, seq, expectedInlineVersion, inlineGeneration)) {
|
|
2518
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
2519
|
+
if (pending)
|
|
2520
|
+
this.retryPendingGroupInlineAck(ns, pending.seq, 'inline Group ack');
|
|
1428
2521
|
return;
|
|
1429
2522
|
}
|
|
1430
|
-
this.
|
|
1431
|
-
|
|
1432
|
-
|
|
2523
|
+
if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
|
|
2524
|
+
return;
|
|
2525
|
+
const before = this.syncState(ns);
|
|
2526
|
+
// 无正文的旧服务端 Head 可把 JSON-safe 数字字符串作为恢复 hint;
|
|
2527
|
+
// inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
|
|
2528
|
+
if (strictSeq || !inlinePresent)
|
|
2529
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
2530
|
+
if ((!inlinePresent || inlineMatches) && !pendingConflict) {
|
|
2531
|
+
if (seq <= before.head && before.ack >= before.tail - 1) {
|
|
2532
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
2533
|
+
const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
|
|
2534
|
+
if (pending && seq >= pending.seq) {
|
|
2535
|
+
this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
|
|
2536
|
+
}
|
|
2537
|
+
else {
|
|
2538
|
+
this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
|
|
2539
|
+
}
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
if (!this.beginRealtimeSync(ns))
|
|
2544
|
+
return;
|
|
1433
2545
|
try {
|
|
1434
|
-
|
|
1435
|
-
|
|
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);
|
|
2546
|
+
// namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
|
|
2547
|
+
await this.runGroupRealtimeSync(groupId, ns, seq, 50, inlinePresent && (!inlineMatches || pendingConflict));
|
|
1440
2548
|
}
|
|
1441
2549
|
catch (exc) {
|
|
1442
|
-
|
|
1443
|
-
client._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${exc}`);
|
|
2550
|
+
client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
1444
2551
|
}
|
|
1445
2552
|
finally {
|
|
1446
|
-
this.
|
|
1447
|
-
|
|
2553
|
+
this.endRealtimeSync(ns);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
async onV2PushNotification(data) {
|
|
2557
|
+
const client = this.runtime.client;
|
|
2558
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
2559
|
+
if (!ns)
|
|
2560
|
+
return;
|
|
2561
|
+
const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
|
|
2562
|
+
if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
|
|
2563
|
+
// 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
|
|
2564
|
+
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
2565
|
+
}
|
|
2566
|
+
const hasInlineObject = isJsonObject(data)
|
|
2567
|
+
&& isJsonObject(data.inline_message);
|
|
2568
|
+
const inlineGeneration = hasInlineObject ? this.captureInlineGeneration() : undefined;
|
|
2569
|
+
const operation = () => this.processV2P2PNotification(data, 'v2', inlineGeneration);
|
|
2570
|
+
if (hasInlineObject)
|
|
2571
|
+
await this.runPushProcessSerialized(ns, operation, inlineGeneration);
|
|
2572
|
+
else
|
|
2573
|
+
await operation();
|
|
2574
|
+
}
|
|
2575
|
+
async processV2P2PNotification(data, expectedInlineVersion, queuedInlineGeneration) {
|
|
2576
|
+
if (queuedInlineGeneration !== undefined && !this.isInlineGenerationCurrent(queuedInlineGeneration))
|
|
2577
|
+
return;
|
|
2578
|
+
const client = this.runtime.client;
|
|
2579
|
+
if (!client._v2Session)
|
|
2580
|
+
return;
|
|
2581
|
+
const pushSeq = isJsonObject(data) ? positiveSafeSequenceHint(data.seq) : 0;
|
|
2582
|
+
const strictSeq = isJsonObject(data)
|
|
2583
|
+
&& typeof data.seq === 'number' && Number.isSafeInteger(data.seq) && data.seq > 0;
|
|
2584
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
2585
|
+
if (!ns)
|
|
2586
|
+
return;
|
|
2587
|
+
const inlinePresent = isJsonObject(data) && Object.prototype.hasOwnProperty.call(data, 'inline_message');
|
|
2588
|
+
if (pushSeq <= 0) {
|
|
2589
|
+
if (inlinePresent)
|
|
2590
|
+
await this.recoverInvalidP2PRealtimeHead(ns, isJsonObject(data) ? data.seq : undefined);
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const inline = isJsonObject(data) ? this.inlineMessage(data) : null;
|
|
2594
|
+
const inlineMatches = inline !== null
|
|
2595
|
+
&& this.inlineP2PBindingMatches(data, inline, pushSeq, expectedInlineVersion);
|
|
2596
|
+
const pendingInline = this.pendingP2PInlineAcks?.get(ns);
|
|
2597
|
+
const pendingConflict = inline !== null && pendingInline !== undefined && pendingInline.seq !== pushSeq;
|
|
2598
|
+
const inlineGeneration = inlineMatches ? (queuedInlineGeneration ?? this.inlineGeneration) : -1;
|
|
2599
|
+
if (inlineMatches && !pendingConflict
|
|
2600
|
+
&& await this.tryConsumeInlineP2P(data, inline, ns, pushSeq, expectedInlineVersion, inlineGeneration)) {
|
|
2601
|
+
this.retryPendingP2PInlineAck(ns, pushSeq, 'inline P2P ack');
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
if (inlineMatches && !this.isInlineGenerationCurrent(inlineGeneration))
|
|
2605
|
+
return;
|
|
2606
|
+
const before = this.syncState(ns);
|
|
2607
|
+
if (strictSeq || !inlinePresent)
|
|
2608
|
+
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
2609
|
+
if ((!inlinePresent || inlineMatches) && !pendingConflict) {
|
|
2610
|
+
if (pushSeq <= before.head && before.ack >= before.tail - 1) {
|
|
2611
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2612
|
+
if (pending && pushSeq >= pending.seq) {
|
|
2613
|
+
this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
|
|
2614
|
+
}
|
|
2615
|
+
else if (inlineMatches) {
|
|
2616
|
+
this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
|
|
2617
|
+
}
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (!this.beginRealtimeSync(ns))
|
|
2622
|
+
return;
|
|
2623
|
+
try {
|
|
2624
|
+
await this.runP2PRealtimeSync(ns, pushSeq, 50, inlinePresent && (!inlineMatches || pendingConflict));
|
|
2625
|
+
}
|
|
2626
|
+
catch (exc) {
|
|
2627
|
+
client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
|
|
2628
|
+
}
|
|
2629
|
+
finally {
|
|
2630
|
+
this.endRealtimeSync(ns);
|
|
1448
2631
|
}
|
|
1449
2632
|
}
|
|
1450
2633
|
async restoreSeqTrackerState() {
|
|
@@ -1458,6 +2641,17 @@ export class MessageDeliveryEngine {
|
|
|
1458
2641
|
const deviceId = client._deviceId;
|
|
1459
2642
|
const slotId = client._slotId;
|
|
1460
2643
|
try {
|
|
2644
|
+
const loadSync = client._tokenStore.loadAllSyncStates?.bind(client._tokenStore);
|
|
2645
|
+
if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
|
|
2646
|
+
let state = await loadSync(aid, deviceId, slotId);
|
|
2647
|
+
if (client._seqTrackerContext !== context)
|
|
2648
|
+
return;
|
|
2649
|
+
if (state && typeof state === 'object' && Object.keys(state).length > 0) {
|
|
2650
|
+
state = await this.migrateSeqStateGroupIds(state);
|
|
2651
|
+
client._seqTracker.restoreSyncState(state);
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
1461
2655
|
const loadAll = client._tokenStore.loadAllSeqs?.bind(client._tokenStore);
|
|
1462
2656
|
if (typeof loadAll === 'function') {
|
|
1463
2657
|
let state = await loadAll(aid, deviceId, slotId);
|
|
@@ -1494,71 +2688,7 @@ export class MessageDeliveryEngine {
|
|
|
1494
2688
|
}
|
|
1495
2689
|
}
|
|
1496
2690
|
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;
|
|
2691
|
+
return state;
|
|
1562
2692
|
}
|
|
1563
2693
|
async saveSeqTrackerState() {
|
|
1564
2694
|
const client = this.runtime.client;
|
|
@@ -1567,7 +2697,9 @@ export class MessageDeliveryEngine {
|
|
|
1567
2697
|
await this.flushSeqTrackerPending();
|
|
1568
2698
|
return;
|
|
1569
2699
|
}
|
|
1570
|
-
const state = client._seqTracker.
|
|
2700
|
+
const state = typeof client._seqTracker.exportSyncState === 'function'
|
|
2701
|
+
? client._seqTracker.exportSyncState()
|
|
2702
|
+
: Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
|
|
1571
2703
|
const pending = this.seqTrackerPending();
|
|
1572
2704
|
if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
|
|
1573
2705
|
const liveNamespaces = new Set(Object.keys(state));
|
|
@@ -1595,9 +2727,28 @@ export class MessageDeliveryEngine {
|
|
|
1595
2727
|
if (forceSeq !== undefined) {
|
|
1596
2728
|
client._seqTracker.forceContiguousSeq(ns, seq);
|
|
1597
2729
|
}
|
|
1598
|
-
|
|
2730
|
+
const state = typeof client._seqTracker.getSyncState === 'function'
|
|
2731
|
+
? client._seqTracker.getSyncState(ns)
|
|
2732
|
+
: { ack: seq, tail: seq, head: seq };
|
|
2733
|
+
this.mergeSeqTrackerPending(context, { [ns]: state });
|
|
1599
2734
|
this.scheduleSeqTrackerFlush();
|
|
1600
2735
|
}
|
|
2736
|
+
async commitSeqTrackerState(ns) {
|
|
2737
|
+
const client = this.runtime.client;
|
|
2738
|
+
const context = this.currentSeqTrackerContext();
|
|
2739
|
+
if (!context || !ns)
|
|
2740
|
+
return;
|
|
2741
|
+
const current = client._seqTracker.getContiguousSeq(ns);
|
|
2742
|
+
const state = typeof client._seqTracker.getSyncState === 'function'
|
|
2743
|
+
? client._seqTracker.getSyncState(ns)
|
|
2744
|
+
: { ack: current, tail: current, head: current };
|
|
2745
|
+
if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
|
|
2746
|
+
return;
|
|
2747
|
+
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
2748
|
+
await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
|
|
2749
|
+
this.dropSeqTrackerPending(ns);
|
|
2750
|
+
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
2751
|
+
}
|
|
1601
2752
|
async flushSeqTrackerPending() {
|
|
1602
2753
|
const client = this.runtime.client;
|
|
1603
2754
|
this.cancelSeqTrackerFlushTimer();
|
|
@@ -1643,10 +2794,12 @@ export class MessageDeliveryEngine {
|
|
|
1643
2794
|
void this.flushSeqTrackerPending();
|
|
1644
2795
|
}
|
|
1645
2796
|
client._seqTrackerPendingPersistContext = context;
|
|
1646
|
-
for (const [ns,
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
|
|
2797
|
+
for (const [ns, raw] of Object.entries(state)) {
|
|
2798
|
+
const ack = Number(raw.ack);
|
|
2799
|
+
const tail = Number(raw.tail);
|
|
2800
|
+
const head = Number(raw.head);
|
|
2801
|
+
if (ns && [ack, tail, head].every(Number.isFinite) && Math.max(ack, tail, head) > 0) {
|
|
2802
|
+
pending.set(ns, { ack, tail, head });
|
|
1650
2803
|
}
|
|
1651
2804
|
}
|
|
1652
2805
|
}
|
|
@@ -1669,14 +2822,19 @@ export class MessageDeliveryEngine {
|
|
|
1669
2822
|
client._seqTrackerFlushTimer = null;
|
|
1670
2823
|
}
|
|
1671
2824
|
}
|
|
1672
|
-
async writeSeqTrackerState(aid, deviceId, slotId, state) {
|
|
2825
|
+
async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
1673
2826
|
const client = this.runtime.client;
|
|
1674
2827
|
if (!aid || Object.keys(state).length === 0)
|
|
1675
2828
|
return;
|
|
1676
2829
|
try {
|
|
2830
|
+
const saveSync = client._tokenStore.saveSyncStates?.bind(client._tokenStore);
|
|
2831
|
+
if (typeof saveSync === 'function') {
|
|
2832
|
+
await saveSync(aid, deviceId, slotId, state);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
1677
2835
|
const saveFn = client._tokenStore.saveSeq?.bind(client._tokenStore);
|
|
1678
2836
|
if (typeof saveFn === 'function') {
|
|
1679
|
-
await Promise.all(Object.entries(state).map(([ns,
|
|
2837
|
+
await Promise.all(Object.entries(state).map(([ns, triplet]) => saveFn(aid, deviceId, slotId, ns, triplet.ack)));
|
|
1680
2838
|
return;
|
|
1681
2839
|
}
|
|
1682
2840
|
if (typeof client._tokenStore.updateInstanceState === 'function') {
|
|
@@ -1690,13 +2848,17 @@ export class MessageDeliveryEngine {
|
|
|
1690
2848
|
}
|
|
1691
2849
|
}
|
|
1692
2850
|
catch (exc) {
|
|
2851
|
+
const error = formatDeliveryError(exc);
|
|
2852
|
+
client._clientLog.warn(`save SeqTracker state failed: ${error}`);
|
|
1693
2853
|
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
1694
2854
|
phase: 'save',
|
|
1695
2855
|
aid,
|
|
1696
2856
|
device_id: deviceId,
|
|
1697
2857
|
slot_id: slotId,
|
|
1698
|
-
error: String(
|
|
2858
|
+
error: String(error),
|
|
1699
2859
|
}).catch(() => { });
|
|
2860
|
+
if (throwOnError)
|
|
2861
|
+
throw exc;
|
|
1700
2862
|
}
|
|
1701
2863
|
}
|
|
1702
2864
|
persistRepairedSeq(ns) {
|
|
@@ -1743,16 +2905,27 @@ export class MessageDeliveryEngine {
|
|
|
1743
2905
|
clampAckSeq(method, field, ns, seq) {
|
|
1744
2906
|
const client = this.runtime.client;
|
|
1745
2907
|
const original = seq;
|
|
1746
|
-
let next =
|
|
1747
|
-
if (!Number.
|
|
2908
|
+
let next = seq;
|
|
2909
|
+
if (typeof next !== 'number' || !Number.isSafeInteger(next))
|
|
1748
2910
|
return 0;
|
|
1749
2911
|
if (next < 0)
|
|
1750
2912
|
next = 0;
|
|
1751
2913
|
if (ns) {
|
|
1752
|
-
const
|
|
1753
|
-
if (
|
|
1754
|
-
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} >
|
|
1755
|
-
next =
|
|
2914
|
+
const committedAck = client._seqTracker.getContiguousSeq(ns);
|
|
2915
|
+
if (next > committedAck) {
|
|
2916
|
+
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
|
|
2917
|
+
next = committedAck;
|
|
2918
|
+
}
|
|
2919
|
+
if ((method === 'message.v2.ack' || method === 'message.ack')) {
|
|
2920
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2921
|
+
if (pending && !pending.committed && next >= pending.seq)
|
|
2922
|
+
next = pending.seq - 1;
|
|
2923
|
+
}
|
|
2924
|
+
else if (method === 'group.v2.ack' || method === 'group.ack_messages') {
|
|
2925
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
2926
|
+
const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
|
|
2927
|
+
if (pending && !pending.committed && next >= pending.seq)
|
|
2928
|
+
next = pending.seq - 1;
|
|
1756
2929
|
}
|
|
1757
2930
|
}
|
|
1758
2931
|
return next;
|
|
@@ -1761,7 +2934,8 @@ export class MessageDeliveryEngine {
|
|
|
1761
2934
|
const client = this.runtime.client;
|
|
1762
2935
|
const clampField = (field, ns) => {
|
|
1763
2936
|
const raw = params[field];
|
|
1764
|
-
|
|
2937
|
+
// 保留非法类型/数值,让 V2 ACK 严格入口失败关闭,不能先 clamp 成合法游标。
|
|
2938
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw))
|
|
1765
2939
|
return params;
|
|
1766
2940
|
const next = this.clampAckSeq(method, field, ns, raw);
|
|
1767
2941
|
return next === raw ? params : { ...params, [field]: next };
|
|
@@ -1775,7 +2949,7 @@ export class MessageDeliveryEngine {
|
|
|
1775
2949
|
return clampField('seq', ns);
|
|
1776
2950
|
}
|
|
1777
2951
|
if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
|
|
1778
|
-
const groupId =
|
|
2952
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1779
2953
|
const ns = groupId ? `group:${groupId}` : '';
|
|
1780
2954
|
if (method === 'group.v2.ack')
|
|
1781
2955
|
return clampField('up_to_seq', ns);
|
|
@@ -1786,7 +2960,7 @@ export class MessageDeliveryEngine {
|
|
|
1786
2960
|
}
|
|
1787
2961
|
return params;
|
|
1788
2962
|
}
|
|
1789
|
-
async drainOrderedMessages(ns, beforeSeq) {
|
|
2963
|
+
async drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
|
|
1790
2964
|
const client = this.runtime.client;
|
|
1791
2965
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
1792
2966
|
if (!queue || queue.size === 0)
|
|
@@ -1801,7 +2975,7 @@ export class MessageDeliveryEngine {
|
|
|
1801
2975
|
queue.delete(seq);
|
|
1802
2976
|
if (!item || client._pushedSeqs.get(ns)?.has(seq))
|
|
1803
2977
|
continue;
|
|
1804
|
-
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload);
|
|
2978
|
+
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse);
|
|
1805
2979
|
this.markPublishedSeq(ns, seq);
|
|
1806
2980
|
delivered = true;
|
|
1807
2981
|
}
|
|
@@ -1830,7 +3004,7 @@ export class MessageDeliveryEngine {
|
|
|
1830
3004
|
this.enqueueOrderedMessage(ns, event, seqNum, payload);
|
|
1831
3005
|
return false;
|
|
1832
3006
|
}
|
|
1833
|
-
await this.drainOrderedMessages(ns, seqNum);
|
|
3007
|
+
await this.drainOrderedMessages(ns, seqNum, true);
|
|
1834
3008
|
if (client._pushedSeqs.get(ns)?.has(seqNum))
|
|
1835
3009
|
return false;
|
|
1836
3010
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -1849,9 +3023,9 @@ export class MessageDeliveryEngine {
|
|
|
1849
3023
|
const seqNum = Number(seq);
|
|
1850
3024
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
|
|
1851
3025
|
if (event === 'message.recalled') {
|
|
1852
|
-
return this.publishMessageRecallTombstone(seq, payload);
|
|
3026
|
+
return await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
|
|
1853
3027
|
}
|
|
1854
|
-
await client._publishAppEvent(event, payload);
|
|
3028
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
1855
3029
|
return true;
|
|
1856
3030
|
}
|
|
1857
3031
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -1866,11 +3040,11 @@ export class MessageDeliveryEngine {
|
|
|
1866
3040
|
if (queue && queue.size === 0)
|
|
1867
3041
|
client._pendingOrderedMsgs.delete(ns);
|
|
1868
3042
|
if (event === 'message.recalled') {
|
|
1869
|
-
const published = await this.publishMessageRecallTombstone(seqNum, payload);
|
|
3043
|
+
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload));
|
|
1870
3044
|
this.markPublishedSeq(ns, seqNum);
|
|
1871
3045
|
return published;
|
|
1872
3046
|
}
|
|
1873
|
-
await client._publishAppEvent(event, payload);
|
|
3047
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
1874
3048
|
this.markPublishedSeq(ns, seqNum);
|
|
1875
3049
|
return true;
|
|
1876
3050
|
}
|