@agentunion/fastaun 0.5.3 → 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 +126 -52
- 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 +126 -52
- 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 +87 -41
- 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 +307 -267
- 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.js +8 -2
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +2 -0
- package/dist/aid-store.js +43 -7
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +97 -50
- package/dist/auth.js.map +1 -1
- package/dist/client/delivery.d.ts +70 -7
- package/dist/client/delivery.js +1334 -236
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +25 -22
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +46 -3
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/mention-mode.d.ts +6 -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.js +26 -3
- package/dist/client/peers.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +20 -1
- package/dist/client/rpc-pipeline.js +332 -73
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -1
- package/dist/client/v2-e2ee.js +625 -105
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +17 -5
- package/dist/client.js +332 -167
- package/dist/client.js.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +6 -0
- package/dist/facades.js +196 -80
- package/dist/facades.js.map +1 -1
- package/dist/group-index.d.ts +5 -0
- package/dist/group-index.js +65 -30
- package/dist/group-index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +12 -0
- package/dist/keystore/aid-db.js +68 -4
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +12 -0
- package/dist/keystore/local-identity-store.d.ts +10 -0
- package/dist/keystore/local-identity-store.js +8 -2
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +10 -0
- package/dist/keystore/local-token-store.js +8 -2
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/register-flow.d.ts +0 -1
- package/dist/register-flow.js +96 -38
- package/dist/register-flow.js.map +1 -1
- package/dist/secret-store/file-store.d.ts +3 -14
- package/dist/secret-store/file-store.js +7 -98
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/secret-store/index.d.ts +1 -4
- package/dist/secret-store/index.js +2 -2
- package/dist/secret-store/index.js.map +1 -1
- package/dist/seq-tracker.d.ts +29 -9
- package/dist/seq-tracker.js +233 -64
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.js +28 -4
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +360 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +19 -0
- package/dist/transport.js +197 -14
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.js +26 -23
- package/dist/v2/e2ee/decrypt.js.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.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 +5 -4
- package/dist/e2ee-group.d.ts +0 -249
- package/dist/e2ee-group.js +0 -1567
- package/dist/e2ee-group.js.map +0 -1
- package/dist/group-resources.d.ts +0 -31
- package/dist/group-resources.js +0 -84
- package/dist/group-resources.js.map +0 -1
- package/dist/keystore/file.d.ts +0 -109
- package/dist/keystore/file.js +0 -772
- package/dist/keystore/file.js.map +0 -1
- package/dist/keystore/sqlite-backup.d.ts +0 -44
- package/dist/keystore/sqlite-backup.js +0 -382
- package/dist/keystore/sqlite-backup.js.map +0 -1
- package/dist/namespaces/auth.d.ts +0 -118
- package/dist/namespaces/auth.js +0 -968
- package/dist/namespaces/auth.js.map +0 -1
- package/dist/namespaces/custody.d.ts +0 -48
- package/dist/namespaces/custody.js +0 -313
- package/dist/namespaces/custody.js.map +0 -1
- package/dist/namespaces/meta.d.ts +0 -76
- package/dist/namespaces/meta.js +0 -490
- package/dist/namespaces/meta.js.map +0 -1
package/dist/client/delivery.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
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;
|
|
7
7
|
const GROUP_RECALL_SEEN_LIMIT = 10_000;
|
|
8
8
|
const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
|
|
9
|
+
const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
|
|
9
10
|
const APP_MESSAGE_ENVELOPE_KEYS = [
|
|
10
11
|
'module_id', 'message_type', 'type', 'kind', 'version',
|
|
11
12
|
'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
|
|
12
13
|
'timestamp', 'created_at', 'encrypted',
|
|
13
|
-
'context', 'protected_headers', 'headers', 'payload_type',
|
|
14
|
+
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
|
|
14
15
|
];
|
|
15
16
|
const RECALL_PAYLOAD_KEYS = [
|
|
16
17
|
'message_ids', 'target_message_seqs',
|
|
@@ -38,9 +39,57 @@ function formatDeliveryError(error) {
|
|
|
38
39
|
}
|
|
39
40
|
export class MessageDeliveryEngine {
|
|
40
41
|
runtime;
|
|
42
|
+
realtimeTailHeads = null;
|
|
43
|
+
realtimeSyncing = null;
|
|
44
|
+
pendingP2PInlineAcks = null;
|
|
45
|
+
pendingGroupInlineAcks = null;
|
|
46
|
+
pendingGroupInlineAckNamespaces = null;
|
|
47
|
+
inlineAckGeneration = 0;
|
|
41
48
|
constructor(runtime) {
|
|
42
49
|
this.runtime = runtime;
|
|
43
50
|
}
|
|
51
|
+
resetInlineAckState() {
|
|
52
|
+
this.inlineAckGeneration += 1;
|
|
53
|
+
this.pendingP2PInlineAcks = null;
|
|
54
|
+
this.pendingGroupInlineAcks = null;
|
|
55
|
+
this.pendingGroupInlineAckNamespaces = null;
|
|
56
|
+
this.realtimeSyncing = null;
|
|
57
|
+
this.realtimeTailHeads = null;
|
|
58
|
+
}
|
|
59
|
+
inlineGenerationIsCurrent(generation) {
|
|
60
|
+
return this.inlineAckGeneration === generation;
|
|
61
|
+
}
|
|
62
|
+
captureInlineAckGeneration() {
|
|
63
|
+
return this.inlineAckGeneration;
|
|
64
|
+
}
|
|
65
|
+
captureP2PInlineAckGeneration() {
|
|
66
|
+
return this.pendingP2PInlineAcks?.size ? this.inlineAckGeneration : null;
|
|
67
|
+
}
|
|
68
|
+
captureGroupInlineAckGeneration() {
|
|
69
|
+
return this.pendingGroupInlineAcks?.size ? this.inlineAckGeneration : null;
|
|
70
|
+
}
|
|
71
|
+
pendingInlineAckNamespaceCount() {
|
|
72
|
+
return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
|
|
73
|
+
}
|
|
74
|
+
markRealtimeTailCompleted(result, tailHead) {
|
|
75
|
+
const heads = this.realtimeTailHeads ?? new WeakMap();
|
|
76
|
+
this.realtimeTailHeads = heads;
|
|
77
|
+
heads.set(result, tailHead);
|
|
78
|
+
}
|
|
79
|
+
realtimeTailHead(result) {
|
|
80
|
+
return this.realtimeTailHeads?.get(result) ?? null;
|
|
81
|
+
}
|
|
82
|
+
beginRealtimeSync(ns) {
|
|
83
|
+
if (!ns || this.realtimeSyncing?.has(ns))
|
|
84
|
+
return false;
|
|
85
|
+
const syncing = this.realtimeSyncing ?? new Set();
|
|
86
|
+
this.realtimeSyncing = syncing;
|
|
87
|
+
syncing.add(ns);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
endRealtimeSync(ns) {
|
|
91
|
+
this.realtimeSyncing?.delete(ns);
|
|
92
|
+
}
|
|
44
93
|
prunePushedSeqs(ns) {
|
|
45
94
|
const client = this.runtime.client;
|
|
46
95
|
const pushed = client._pushedSeqs.get(ns);
|
|
@@ -108,8 +157,8 @@ export class MessageDeliveryEngine {
|
|
|
108
157
|
const eventPayload = payload;
|
|
109
158
|
client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
|
|
110
159
|
if (eventPayload.action === 'dissolved') {
|
|
111
|
-
const rawGroupId = eventPayload.
|
|
112
|
-
const groupId =
|
|
160
|
+
const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
|
|
161
|
+
const groupId = String(rawGroupId).trim();
|
|
113
162
|
if (groupId)
|
|
114
163
|
client._cleanupDissolvedGroup?.(groupId);
|
|
115
164
|
}
|
|
@@ -140,7 +189,10 @@ export class MessageDeliveryEngine {
|
|
|
140
189
|
return result;
|
|
141
190
|
}
|
|
142
191
|
normalizePublishedMessagePayload(event, payload) {
|
|
192
|
+
payload = this.stripInlineInternalFields(payload);
|
|
143
193
|
if (this.isInstanceScopedMessageEvent(event)) {
|
|
194
|
+
if (event === 'group.message_created')
|
|
195
|
+
payload = normalizeGroupMentionMode(payload);
|
|
144
196
|
return this.attachAppMessageEnvelope(this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload)));
|
|
145
197
|
}
|
|
146
198
|
if (this.isGroupScopedEvent(event)) {
|
|
@@ -148,6 +200,18 @@ export class MessageDeliveryEngine {
|
|
|
148
200
|
}
|
|
149
201
|
return payload;
|
|
150
202
|
}
|
|
203
|
+
stripInlineInternalFields(payload) {
|
|
204
|
+
if (!isJsonObject(payload))
|
|
205
|
+
return payload;
|
|
206
|
+
const source = payload;
|
|
207
|
+
if (!Object.prototype.hasOwnProperty.call(source, 'inline_message')
|
|
208
|
+
&& !Object.prototype.hasOwnProperty.call(source, '_inline_message_candidates_v1'))
|
|
209
|
+
return payload;
|
|
210
|
+
const result = { ...source };
|
|
211
|
+
delete result.inline_message;
|
|
212
|
+
delete result._inline_message_candidates_v1;
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
151
215
|
envelopeMetadata(value) {
|
|
152
216
|
let source = value;
|
|
153
217
|
if (source && typeof source === 'object') {
|
|
@@ -205,6 +269,7 @@ export class MessageDeliveryEngine {
|
|
|
205
269
|
if (protectedHeaders)
|
|
206
270
|
envelope.protected_headers = protectedHeaders;
|
|
207
271
|
setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
|
|
272
|
+
setIfPresent('mention_mode', message.mention_mode);
|
|
208
273
|
return envelope;
|
|
209
274
|
}
|
|
210
275
|
isGroupScopedEvent(event) {
|
|
@@ -271,19 +336,30 @@ export class MessageDeliveryEngine {
|
|
|
271
336
|
if (protectedHeaders)
|
|
272
337
|
envelope.protected_headers = protectedHeaders;
|
|
273
338
|
setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
|
|
339
|
+
if (method === 'group.send')
|
|
340
|
+
setIfPresent('mention_mode', resultObj.mention_mode);
|
|
274
341
|
return envelope;
|
|
275
342
|
}
|
|
276
|
-
attachSendResultEnvelope(method, params, result, encrypted) {
|
|
343
|
+
attachSendResultEnvelope(method, params, result, encrypted, modeSource) {
|
|
277
344
|
if (!APP_SEND_ENVELOPE_METHODS.has(method) || !isJsonObject(result))
|
|
278
345
|
return result;
|
|
279
|
-
|
|
280
|
-
|
|
346
|
+
let out = { ...result };
|
|
347
|
+
const serverEnvelope = method === 'group.send' && isJsonObject(out.envelope)
|
|
348
|
+
? { ...out.envelope }
|
|
349
|
+
: undefined;
|
|
281
350
|
if ('payload' in params) {
|
|
282
351
|
out.payload = params.payload;
|
|
283
352
|
}
|
|
284
353
|
else if ('content' in params) {
|
|
285
354
|
out.payload = params.content;
|
|
286
355
|
}
|
|
356
|
+
const generatedEnvelope = this.sendResultEnvelope(method, params, out, encrypted);
|
|
357
|
+
out.envelope = serverEnvelope ? { ...generatedEnvelope, ...serverEnvelope } : generatedEnvelope;
|
|
358
|
+
if (method === 'group.send') {
|
|
359
|
+
const serverSource = modeSource ?? result;
|
|
360
|
+
const authoritativeSource = hasGroupMentionModeField(serverSource) ? serverSource : out;
|
|
361
|
+
out = normalizeGroupMentionMode(out, authoritativeSource);
|
|
362
|
+
}
|
|
287
363
|
return out;
|
|
288
364
|
}
|
|
289
365
|
attachAppGroupEventEnvelope(payload) {
|
|
@@ -473,8 +549,7 @@ export class MessageDeliveryEngine {
|
|
|
473
549
|
// (group_id, sorted message_ids) 已能唯一标识一次撤回;缺 message_ids 时按原消息 id/seq 兜底。
|
|
474
550
|
// 去重键不含 recalled_at:占位/通知 tombstone(pull,事务内时间)与在线 push(事务后重取时间)
|
|
475
551
|
// 三条通道对同一次撤回可能携带不同来源的时间戳,纳入 recalled_at 会使去重失效、回调多次。
|
|
476
|
-
|
|
477
|
-
const normalizedGroupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
552
|
+
const normalizedGroupId = String(groupId ?? '').trim();
|
|
478
553
|
const ids = payload.message_ids;
|
|
479
554
|
const idPart = Array.isArray(ids)
|
|
480
555
|
? ids.map((i) => String(i ?? '').trim()).filter(Boolean).sort().join(',')
|
|
@@ -503,9 +578,7 @@ export class MessageDeliveryEngine {
|
|
|
503
578
|
if (!eventPayload)
|
|
504
579
|
return false;
|
|
505
580
|
const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
|
|
506
|
-
const dedupGroupId =
|
|
507
|
-
if (dedupGroupId)
|
|
508
|
-
eventPayload.group_id = dedupGroupId;
|
|
581
|
+
const dedupGroupId = rawGroupId;
|
|
509
582
|
const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
|
|
510
583
|
let seen = client._groupRecallSeen;
|
|
511
584
|
if (!seen) {
|
|
@@ -643,11 +716,11 @@ export class MessageDeliveryEngine {
|
|
|
643
716
|
if (!isJsonObject(message)) {
|
|
644
717
|
return { value_type: typeof message };
|
|
645
718
|
}
|
|
646
|
-
const msg = message;
|
|
719
|
+
const msg = normalizeGroupMentionMode(message);
|
|
647
720
|
const keys = [
|
|
648
721
|
'message_id', 'id', 'from', 'from_aid', 'sender_aid', 'to', 'to_aid',
|
|
649
722
|
'group_id', 'seq', 'msg_seq', 'type', 'version', 'timestamp', 't_server',
|
|
650
|
-
'device_id', 'slot_id', 'encrypted', '
|
|
723
|
+
'device_id', 'slot_id', 'encrypted', 'mention_mode', 'dispatch',
|
|
651
724
|
'e2ee', 'headers', 'protected_headers', 'context', 'status',
|
|
652
725
|
'_decrypt_error', '_decrypt_stage',
|
|
653
726
|
];
|
|
@@ -684,24 +757,72 @@ export class MessageDeliveryEngine {
|
|
|
684
757
|
return true;
|
|
685
758
|
const client = this.runtime.client;
|
|
686
759
|
const msg = message;
|
|
687
|
-
if ('
|
|
688
|
-
|
|
689
|
-
|
|
760
|
+
if (Object.prototype.hasOwnProperty.call(msg, 'target_device_id')) {
|
|
761
|
+
if (typeof msg.target_device_id !== 'string')
|
|
762
|
+
return false;
|
|
763
|
+
const targetDeviceId = msg.target_device_id.trim();
|
|
764
|
+
if (!targetDeviceId || targetDeviceId !== client._deviceId) {
|
|
690
765
|
return false;
|
|
691
766
|
}
|
|
692
767
|
}
|
|
693
|
-
if ('
|
|
694
|
-
|
|
695
|
-
|
|
768
|
+
else if (Object.prototype.hasOwnProperty.call(msg, 'device_id')) {
|
|
769
|
+
if (typeof msg.device_id !== 'string')
|
|
770
|
+
return false;
|
|
771
|
+
const deviceId = msg.device_id.trim();
|
|
772
|
+
if (deviceId !== client._deviceId)
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
if (Object.prototype.hasOwnProperty.call(msg, 'target_slot_id')) {
|
|
776
|
+
if (typeof msg.target_slot_id !== 'string')
|
|
777
|
+
return false;
|
|
778
|
+
const targetSlotId = msg.target_slot_id.trim();
|
|
779
|
+
if (!targetSlotId || slotIsolationKey(targetSlotId) !== slotIsolationKey(client._slotId)) {
|
|
696
780
|
return false;
|
|
697
781
|
}
|
|
698
782
|
}
|
|
783
|
+
else if (Object.prototype.hasOwnProperty.call(msg, 'slot_id')) {
|
|
784
|
+
if (typeof msg.slot_id !== 'string')
|
|
785
|
+
return false;
|
|
786
|
+
const slotId = msg.slot_id.trim();
|
|
787
|
+
if (slotIsolationKey(slotId) !== slotIsolationKey(client._slotId))
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
699
790
|
return true;
|
|
700
791
|
}
|
|
701
|
-
async runPushProcessSerialized(ns, operation) {
|
|
792
|
+
async runPushProcessSerialized(ns, operation, inlineGeneration) {
|
|
702
793
|
const client = this.runtime.client;
|
|
703
|
-
if (
|
|
794
|
+
if (inlineGeneration === undefined) {
|
|
795
|
+
if (!ns) {
|
|
796
|
+
await operation();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
let queues = client._pushProcessQueues;
|
|
800
|
+
if (!queues) {
|
|
801
|
+
queues = new Map();
|
|
802
|
+
client._pushProcessQueues = queues;
|
|
803
|
+
}
|
|
804
|
+
const previous = queues.get(ns) ?? Promise.resolve();
|
|
805
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
806
|
+
const stored = current.then(() => undefined, () => undefined);
|
|
807
|
+
queues.set(ns, stored);
|
|
808
|
+
try {
|
|
809
|
+
await current;
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
if (queues.get(ns) === stored)
|
|
813
|
+
queues.delete(ns);
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const run = async () => {
|
|
818
|
+
if (!this.inlineGenerationIsCurrent(inlineGeneration))
|
|
819
|
+
return;
|
|
704
820
|
await operation();
|
|
821
|
+
if (!this.inlineGenerationIsCurrent(inlineGeneration))
|
|
822
|
+
return;
|
|
823
|
+
};
|
|
824
|
+
if (!ns) {
|
|
825
|
+
await run();
|
|
705
826
|
return;
|
|
706
827
|
}
|
|
707
828
|
let queues = client._pushProcessQueues;
|
|
@@ -710,7 +831,7 @@ export class MessageDeliveryEngine {
|
|
|
710
831
|
client._pushProcessQueues = queues;
|
|
711
832
|
}
|
|
712
833
|
const previous = queues.get(ns) ?? Promise.resolve();
|
|
713
|
-
const current = previous.catch(() => undefined).then(
|
|
834
|
+
const current = previous.catch(() => undefined).then(run);
|
|
714
835
|
const stored = current.then(() => undefined, () => undefined);
|
|
715
836
|
queues.set(ns, stored);
|
|
716
837
|
try {
|
|
@@ -734,7 +855,10 @@ export class MessageDeliveryEngine {
|
|
|
734
855
|
const ns = isJsonObject(data) && client._aid && data.seq !== undefined
|
|
735
856
|
? `p2p:${client._aid}`
|
|
736
857
|
: '';
|
|
737
|
-
|
|
858
|
+
const inlineGeneration = isJsonObject(data)
|
|
859
|
+
&& Object.prototype.hasOwnProperty.call(data, 'inline_message')
|
|
860
|
+
? this.captureInlineAckGeneration() : undefined;
|
|
861
|
+
this.runPushProcessSerialized(ns, () => this.processAndPublishMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
|
|
738
862
|
client._clientLog.warn(`P2P message decrypt failed: ${formatDeliveryError(exc)}`);
|
|
739
863
|
if (isJsonObject(data)) {
|
|
740
864
|
const src = data;
|
|
@@ -770,7 +894,7 @@ export class MessageDeliveryEngine {
|
|
|
770
894
|
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
771
895
|
const published = await this.publishOrderedMessage('message.recalled', ns, seq, msg);
|
|
772
896
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
773
|
-
if (Number(seq) > contigAfter && !published) {
|
|
897
|
+
if (Number(seq) > contigAfter && !published && client._sessionOptions?.background_sync !== false) {
|
|
774
898
|
this.fillP2pGap().catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
|
|
775
899
|
}
|
|
776
900
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -785,7 +909,7 @@ export class MessageDeliveryEngine {
|
|
|
785
909
|
}
|
|
786
910
|
await this.publishMessageRecallTombstone(seq, msg);
|
|
787
911
|
}
|
|
788
|
-
async processAndPublishMessage(data) {
|
|
912
|
+
async processAndPublishMessage(data, inlineGeneration) {
|
|
789
913
|
const client = this.runtime.client;
|
|
790
914
|
if (!isJsonObject(data)) {
|
|
791
915
|
await client._publishAppEvent('message.received', data, 'push');
|
|
@@ -796,6 +920,22 @@ export class MessageDeliveryEngine {
|
|
|
796
920
|
client._clientLog.debug(`P2P push filtered by instance: message_id=${String(msg.message_id ?? '')}, seq=${String(msg.seq ?? '')}, target_device=${String(msg.device_id ?? '')}, target_slot=${String(msg.slot_id ?? '')}, local_device=${client._deviceId}, local_slot=${client._slotId}`);
|
|
797
921
|
return;
|
|
798
922
|
}
|
|
923
|
+
// 新 SDK 的所有带合法 seq P2P Push 都只作为 Head 通知处理。
|
|
924
|
+
// 新服务端为兼容旧 SDK 仍可能发送 message.received;V2 会话下必须
|
|
925
|
+
// 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
|
|
926
|
+
const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
927
|
+
const legacyPushSeq = this.realtimeHeadSeq(msg.seq, inlinePresent);
|
|
928
|
+
if (client._v2Session && client._aid && msg.seq !== undefined && msg.seq !== null) {
|
|
929
|
+
if (legacyPushSeq === null) {
|
|
930
|
+
if (inlinePresent)
|
|
931
|
+
await this.recoverInvalidP2PRealtimeHead(`p2p:${client._aid}`, msg.seq);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
await this.onV2PushNotification({ ...msg, seq: legacyPushSeq }, 'v1', inlineGeneration);
|
|
935
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
936
|
+
return;
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
799
939
|
const encryptedPush = client._isEncryptedPushMessage(msg);
|
|
800
940
|
const seq = msg.seq;
|
|
801
941
|
if (seq !== undefined && seq !== null && client._aid) {
|
|
@@ -808,7 +948,7 @@ export class MessageDeliveryEngine {
|
|
|
808
948
|
: await this.publishOrderedMessage('message.received', ns, seq, msg);
|
|
809
949
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
810
950
|
const needPull = Number(seq) > contigAfter && !published;
|
|
811
|
-
if (needPull) {
|
|
951
|
+
if (needPull && client._sessionOptions?.background_sync !== false) {
|
|
812
952
|
client._clientLog.debug(`P2P seq gap detected: ns=${ns}, seq=${seq}, contiguous=${contigAfter}`);
|
|
813
953
|
this.fillP2pGap().catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
|
|
814
954
|
}
|
|
@@ -847,7 +987,10 @@ export class MessageDeliveryEngine {
|
|
|
847
987
|
const groupNs = isJsonObject(data)
|
|
848
988
|
? String(data.group_id ?? '').trim()
|
|
849
989
|
: '';
|
|
850
|
-
|
|
990
|
+
const inlineGeneration = isJsonObject(data)
|
|
991
|
+
&& Object.prototype.hasOwnProperty.call(data, 'inline_message')
|
|
992
|
+
? this.captureInlineAckGeneration() : undefined;
|
|
993
|
+
this.runPushProcessSerialized(groupNs ? `group:${groupNs}` : '', () => this.processAndPublishGroupMessage(data, inlineGeneration), inlineGeneration).catch((exc) => {
|
|
851
994
|
client._clientLog.warn(`group message decrypt failed: ${formatDeliveryError(exc)}`);
|
|
852
995
|
if (isJsonObject(data)) {
|
|
853
996
|
const src = data;
|
|
@@ -865,7 +1008,7 @@ export class MessageDeliveryEngine {
|
|
|
865
1008
|
});
|
|
866
1009
|
client._clientLog.debug(`_onRawGroupMessageCreated exit: elapsed=${Date.now() - tStart}ms (handler dispatched)`);
|
|
867
1010
|
}
|
|
868
|
-
async processAndPublishGroupMessage(data) {
|
|
1011
|
+
async processAndPublishGroupMessage(data, inlineGeneration) {
|
|
869
1012
|
const client = this.runtime.client;
|
|
870
1013
|
if (!isJsonObject(data)) {
|
|
871
1014
|
await client._publishAppEvent('group.message_created', data, 'group-push');
|
|
@@ -878,6 +1021,21 @@ export class MessageDeliveryEngine {
|
|
|
878
1021
|
if (groupId) {
|
|
879
1022
|
client._groupSynced.add(groupId);
|
|
880
1023
|
}
|
|
1024
|
+
// V2 会话中旧 group.message_created 也只是 Head 通知;必须先 Tail,
|
|
1025
|
+
// 不能让 payload 路径直接推进旧 ordered publish 状态。
|
|
1026
|
+
const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
1027
|
+
const legacyPushSeq = this.realtimeHeadSeq(seq, inlinePresent);
|
|
1028
|
+
if (client._v2Session && groupId && seq !== undefined && seq !== null) {
|
|
1029
|
+
if (legacyPushSeq === null) {
|
|
1030
|
+
if (inlinePresent)
|
|
1031
|
+
await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, seq);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
await this.onRawGroupV2MessageCreated({ ...msg, seq: legacyPushSeq }, 'v1', inlineGeneration);
|
|
1035
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
1036
|
+
return;
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
881
1039
|
if (payload === undefined || payload === null
|
|
882
1040
|
|| (typeof payload === 'object' && Object.keys(payload).length === 0)) {
|
|
883
1041
|
void this.autoPullGroupMessages(msg).catch((exc) => {
|
|
@@ -911,7 +1069,7 @@ export class MessageDeliveryEngine {
|
|
|
911
1069
|
: await this.publishOrderedMessage('group.message_created', ns, seq, msg);
|
|
912
1070
|
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
913
1071
|
const needPull = Number(seq) > contigAfter && !published;
|
|
914
|
-
if (needPull) {
|
|
1072
|
+
if (needPull && client._sessionOptions?.background_sync !== false) {
|
|
915
1073
|
client._clientLog.debug(`group message seq gap detected: group=${groupId}, seq=${seq}, contiguous=${contigAfter}`);
|
|
916
1074
|
this.fillGroupGap(groupId).catch((exc) => client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`));
|
|
917
1075
|
}
|
|
@@ -944,22 +1102,32 @@ export class MessageDeliveryEngine {
|
|
|
944
1102
|
await client._publishAppEvent('group.message_created', notification);
|
|
945
1103
|
return;
|
|
946
1104
|
}
|
|
947
|
-
|
|
1105
|
+
if (client._sessionOptions?.background_sync === false) {
|
|
1106
|
+
client._clientLog.debug(`auto pull group messages skipped: group=${groupId} background_sync=false`);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
groupId = String(groupId ?? '').trim();
|
|
948
1110
|
const ns = `group:${groupId}`;
|
|
949
1111
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
950
1112
|
client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
|
|
951
|
-
const
|
|
1113
|
+
const operation = async () => {
|
|
952
1114
|
const pullAfterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
953
1115
|
const messages = await client._pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
|
|
954
1116
|
this.prunePushedSeqs(ns);
|
|
955
1117
|
return messages.length;
|
|
956
|
-
}
|
|
957
|
-
if (
|
|
958
|
-
client.
|
|
1118
|
+
};
|
|
1119
|
+
if (client._rpcPipeline) {
|
|
1120
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1121
|
+
await client._rpcPipeline.runPullSerialized(key, operation, true);
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
await client._withBackgroundRpc(operation);
|
|
959
1125
|
}
|
|
960
1126
|
}
|
|
961
1127
|
async fillP2pGap() {
|
|
962
1128
|
const client = this.runtime.client;
|
|
1129
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1130
|
+
return;
|
|
963
1131
|
if (!client._aid)
|
|
964
1132
|
return;
|
|
965
1133
|
const ns = `p2p:${client._aid}`;
|
|
@@ -967,16 +1135,14 @@ export class MessageDeliveryEngine {
|
|
|
967
1135
|
const dedupKey = `p2p:${afterSeq}`;
|
|
968
1136
|
if (client._gapFillDone.has(dedupKey))
|
|
969
1137
|
return;
|
|
970
|
-
const token = client._tryAcquirePullGate(ns);
|
|
971
|
-
if (token === null) {
|
|
972
|
-
client._clientLog.debug(`P2P message gap fill skipped: pull in-flight ns=${ns}`);
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
1138
|
client._gapFillDone.set(dedupKey, Date.now());
|
|
976
1139
|
client._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
|
|
977
1140
|
let filled = 0;
|
|
978
1141
|
try {
|
|
979
|
-
const
|
|
1142
|
+
const operation = () => client._pullV2(afterSeq, 50, { skipAutoAck: true, gateLocked: true });
|
|
1143
|
+
const messages = client._rpcPipeline
|
|
1144
|
+
? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), operation, true)
|
|
1145
|
+
: await client._withBackgroundRpc(operation);
|
|
980
1146
|
filled = messages.length;
|
|
981
1147
|
this.prunePushedSeqs(ns);
|
|
982
1148
|
if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
|
|
@@ -994,12 +1160,13 @@ export class MessageDeliveryEngine {
|
|
|
994
1160
|
}
|
|
995
1161
|
finally {
|
|
996
1162
|
client._gapFillDone.delete(dedupKey);
|
|
997
|
-
client._releasePullGate(ns, token);
|
|
998
1163
|
}
|
|
999
1164
|
}
|
|
1000
1165
|
async fillGroupGap(groupId) {
|
|
1001
1166
|
const client = this.runtime.client;
|
|
1002
|
-
|
|
1167
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1168
|
+
return;
|
|
1169
|
+
groupId = String(groupId ?? '').trim();
|
|
1003
1170
|
if (!groupId)
|
|
1004
1171
|
return;
|
|
1005
1172
|
const ns = `group:${groupId}`;
|
|
@@ -1007,16 +1174,14 @@ export class MessageDeliveryEngine {
|
|
|
1007
1174
|
const dedupKey = `group_msg:${groupId}:${afterSeq}`;
|
|
1008
1175
|
if (client._gapFillDone.has(dedupKey))
|
|
1009
1176
|
return;
|
|
1010
|
-
const token = client._tryAcquirePullGate(ns);
|
|
1011
|
-
if (token === null) {
|
|
1012
|
-
client._clientLog.debug(`group message gap fill skipped: pull in-flight group=${groupId}`);
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
1177
|
client._gapFillDone.set(dedupKey, Date.now());
|
|
1016
1178
|
client._clientLog.debug(`group message gap fill start: group=${groupId}, after_seq=${afterSeq}`);
|
|
1017
1179
|
let filled = 0;
|
|
1018
1180
|
try {
|
|
1019
|
-
const
|
|
1181
|
+
const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
|
|
1182
|
+
const messages = client._rpcPipeline
|
|
1183
|
+
? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), operation, true)
|
|
1184
|
+
: await client._withBackgroundRpc(operation);
|
|
1020
1185
|
filled = messages.length;
|
|
1021
1186
|
this.prunePushedSeqs(ns);
|
|
1022
1187
|
if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
|
|
@@ -1030,7 +1195,6 @@ export class MessageDeliveryEngine {
|
|
|
1030
1195
|
}
|
|
1031
1196
|
finally {
|
|
1032
1197
|
client._gapFillDone.delete(dedupKey);
|
|
1033
|
-
client._releasePullGate(ns, token);
|
|
1034
1198
|
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
1035
1199
|
void this.fillGroupGap(groupId);
|
|
1036
1200
|
}
|
|
@@ -1038,7 +1202,9 @@ export class MessageDeliveryEngine {
|
|
|
1038
1202
|
}
|
|
1039
1203
|
async fillGroupEventGap(groupId) {
|
|
1040
1204
|
const client = this.runtime.client;
|
|
1041
|
-
|
|
1205
|
+
if (client._sessionOptions?.background_sync === false)
|
|
1206
|
+
return;
|
|
1207
|
+
groupId = String(groupId ?? '').trim();
|
|
1042
1208
|
if (!groupId)
|
|
1043
1209
|
return;
|
|
1044
1210
|
const ns = `group_event:${groupId}`;
|
|
@@ -1082,12 +1248,16 @@ export class MessageDeliveryEngine {
|
|
|
1082
1248
|
? result.cursor
|
|
1083
1249
|
: null;
|
|
1084
1250
|
const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
|
|
1085
|
-
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_event_seq', 'retention_floor_event_seq')
|
|
1086
|
-
|
|
1251
|
+
const retentionFloor = Math.max(client._pullRetentionFloor(result, 'retention_floor_event_seq', 'retention_floor_event_seq'));
|
|
1252
|
+
const serverAckFloor = Number.isFinite(cursorCurrentSeq) ? Math.max(0, cursorCurrentSeq) : 0;
|
|
1253
|
+
const earliestAvailable = Number(result.earliest_available_event_seq ?? 0);
|
|
1254
|
+
const visibilityFloor = Math.max(0, Number.isFinite(Number(cursor?.join_seq)) ? Number(cursor?.join_seq) : 0, Number.isFinite(earliestAvailable) && earliestAvailable > 0 ? earliestAvailable - 1 : 0);
|
|
1255
|
+
const effectiveFloor = Math.max(retentionFloor, serverAckFloor, visibilityFloor);
|
|
1256
|
+
if (effectiveFloor > 0) {
|
|
1087
1257
|
const contigBeforeFloor = client._seqTracker.getContiguousSeq(ns);
|
|
1088
|
-
if (contigBeforeFloor <
|
|
1089
|
-
client._clientLog.info(`group.pull_events
|
|
1090
|
-
client._seqTracker.forceContiguousSeq(ns,
|
|
1258
|
+
if (contigBeforeFloor < effectiveFloor) {
|
|
1259
|
+
client._clientLog.info(`group.pull_events floor advance: ns=${ns} contiguous=${contigBeforeFloor} -> effective_floor=${effectiveFloor} retention_floor=${retentionFloor} server_ack=${serverAckFloor} visibility_floor=${visibilityFloor}`);
|
|
1260
|
+
client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
|
|
1091
1261
|
}
|
|
1092
1262
|
}
|
|
1093
1263
|
const eventSeqs = [];
|
|
@@ -1155,7 +1325,7 @@ export class MessageDeliveryEngine {
|
|
|
1155
1325
|
}
|
|
1156
1326
|
async handleGroupChangedEventSeq(data, groupId) {
|
|
1157
1327
|
const client = this.runtime.client;
|
|
1158
|
-
groupId =
|
|
1328
|
+
groupId = String(groupId ?? '').trim();
|
|
1159
1329
|
let needPull = false;
|
|
1160
1330
|
const rawEventSeq = data.event_seq;
|
|
1161
1331
|
const eventSeq = Number(rawEventSeq);
|
|
@@ -1164,6 +1334,17 @@ export class MessageDeliveryEngine {
|
|
|
1164
1334
|
return;
|
|
1165
1335
|
}
|
|
1166
1336
|
const ns = `group_event:${groupId}`;
|
|
1337
|
+
if (String(data.kind ?? '').trim() === 'group.online_unread_hint') {
|
|
1338
|
+
if (client._sessionOptions?.background_sync === false) {
|
|
1339
|
+
client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
client._clientLog.debug(`group.changed online unread hint triggering gap fill: group=${groupId}, event_seq=${eventSeq}`);
|
|
1343
|
+
this.fillGroupEventGap(groupId).catch((exc) => {
|
|
1344
|
+
client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
|
|
1345
|
+
});
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1167
1348
|
if (this.isSelfJoinGroupChanged(data)) {
|
|
1168
1349
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
1169
1350
|
const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
|
|
@@ -1185,17 +1366,16 @@ export class MessageDeliveryEngine {
|
|
|
1185
1366
|
if (ackContig > 0 && ackContig !== contigBefore) {
|
|
1186
1367
|
if (data.action !== 'dissolved')
|
|
1187
1368
|
this.persistSeq(ns);
|
|
1188
|
-
const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
|
|
1189
1369
|
client._transport.call('group.ack_events', {
|
|
1190
1370
|
group_id: groupId,
|
|
1191
|
-
event_seq:
|
|
1371
|
+
event_seq: ackContig,
|
|
1192
1372
|
device_id: client._deviceId,
|
|
1193
1373
|
slot_id: client._slotId,
|
|
1194
1374
|
}, undefined, undefined, true).catch((e) => {
|
|
1195
1375
|
client._clientLog.debug(`group event push auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
|
|
1196
1376
|
});
|
|
1197
1377
|
}
|
|
1198
|
-
if (needPull && groupId && !data._from_gap_fill) {
|
|
1378
|
+
if (needPull && groupId && !data._from_gap_fill && client._sessionOptions?.background_sync !== false) {
|
|
1199
1379
|
this.fillGroupEventGap(groupId).catch((exc) => {
|
|
1200
1380
|
client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
|
|
1201
1381
|
});
|
|
@@ -1203,7 +1383,7 @@ export class MessageDeliveryEngine {
|
|
|
1203
1383
|
}
|
|
1204
1384
|
fireGroupEventAck(groupId, eventSeq, reason) {
|
|
1205
1385
|
const client = this.runtime.client;
|
|
1206
|
-
const gid =
|
|
1386
|
+
const gid = String(groupId ?? '').trim();
|
|
1207
1387
|
if (!gid)
|
|
1208
1388
|
return;
|
|
1209
1389
|
const ns = `group_event:${gid}`;
|
|
@@ -1226,21 +1406,25 @@ export class MessageDeliveryEngine {
|
|
|
1226
1406
|
client._clientLog.debug(`group event ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
|
|
1227
1407
|
}
|
|
1228
1408
|
}
|
|
1229
|
-
fireGroupV2Ack(groupId, upToSeq, reason) {
|
|
1409
|
+
fireGroupV2Ack(groupId, upToSeq, reason, groupAid = '') {
|
|
1230
1410
|
const client = this.runtime.client;
|
|
1231
|
-
const gid =
|
|
1411
|
+
const gid = String(groupId ?? '').trim();
|
|
1232
1412
|
if (!gid)
|
|
1233
1413
|
return;
|
|
1234
1414
|
const ns = `group:${gid}`;
|
|
1235
1415
|
const ackSeq = this.clampAckSeq('group.v2.ack', 'up_to_seq', ns, Number(upToSeq) || 0);
|
|
1236
1416
|
if (ackSeq <= 0)
|
|
1237
1417
|
return;
|
|
1418
|
+
const generation = this.inlineAckGeneration;
|
|
1238
1419
|
try {
|
|
1239
1420
|
Promise.resolve(client.call('group.v2.ack', {
|
|
1240
1421
|
group_id: gid,
|
|
1422
|
+
...(groupAid ? { group_aid: groupAid } : {}),
|
|
1241
1423
|
up_to_seq: ackSeq,
|
|
1242
1424
|
_rpc_background: true,
|
|
1243
|
-
})).
|
|
1425
|
+
})).then((result) => {
|
|
1426
|
+
this.onGroupV2AckResult(gid, result, ackSeq, generation);
|
|
1427
|
+
}).catch((e) => {
|
|
1244
1428
|
client._clientLog.debug(`group.v2 ${reason} ack failed: group=${gid} ${formatDeliveryError(e)}`);
|
|
1245
1429
|
});
|
|
1246
1430
|
}
|
|
@@ -1261,84 +1445,948 @@ export class MessageDeliveryEngine {
|
|
|
1261
1445
|
const actorAid = String(data.actor_aid ?? '').trim();
|
|
1262
1446
|
return !joinedAid && ['joined', 'invite_code_used'].includes(action) && actorAid === selfAid;
|
|
1263
1447
|
}
|
|
1264
|
-
|
|
1448
|
+
inlinePostAckRow(row) {
|
|
1449
|
+
const result = {};
|
|
1450
|
+
for (const key of ['envelope_json', 'spk_id', 'group_id', 'message_id']) {
|
|
1451
|
+
const value = row[key];
|
|
1452
|
+
if (typeof value === 'string' && value)
|
|
1453
|
+
result[key] = value;
|
|
1454
|
+
}
|
|
1455
|
+
const seq = this.inlineSafePositiveSeq(row.seq);
|
|
1456
|
+
if (seq !== null)
|
|
1457
|
+
result.seq = seq;
|
|
1458
|
+
return result;
|
|
1459
|
+
}
|
|
1460
|
+
reservePendingGroupInlineAck(ns, seq, row, groupAid) {
|
|
1461
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingGroupInlineAcks?.has(ns)
|
|
1462
|
+
|| this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
|
|
1463
|
+
return false;
|
|
1464
|
+
const normalizedGroupAid = typeof groupAid === 'string' ? groupAid.trim() : '';
|
|
1465
|
+
const aliasNs = normalizedGroupAid ? `group:${normalizedGroupAid}` : '';
|
|
1466
|
+
const namespaces = aliasNs && aliasNs !== ns ? [ns, aliasNs] : [ns];
|
|
1467
|
+
const pendingMap = this.pendingGroupInlineAcks ?? new Map();
|
|
1468
|
+
const namespaceMap = this.pendingGroupInlineAckNamespaces ?? new Map();
|
|
1469
|
+
this.pendingGroupInlineAcks = pendingMap;
|
|
1470
|
+
this.pendingGroupInlineAckNamespaces = namespaceMap;
|
|
1471
|
+
pendingMap.set(ns, {
|
|
1472
|
+
seq,
|
|
1473
|
+
row: this.inlinePostAckRow(row),
|
|
1474
|
+
groupAid: normalizedGroupAid,
|
|
1475
|
+
namespaces,
|
|
1476
|
+
acking: false,
|
|
1477
|
+
committed: false,
|
|
1478
|
+
observedAckSeq: 0,
|
|
1479
|
+
});
|
|
1480
|
+
for (const namespace of namespaces) {
|
|
1481
|
+
let candidates = namespaceMap.get(namespace);
|
|
1482
|
+
if (!candidates) {
|
|
1483
|
+
candidates = new Set();
|
|
1484
|
+
namespaceMap.set(namespace, candidates);
|
|
1485
|
+
}
|
|
1486
|
+
candidates.add(ns);
|
|
1487
|
+
}
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
removePendingGroupInlineAck(ns, pending) {
|
|
1491
|
+
const pendingMap = this.pendingGroupInlineAcks;
|
|
1492
|
+
if (pendingMap?.get(ns) !== pending)
|
|
1493
|
+
return false;
|
|
1494
|
+
pendingMap.delete(ns);
|
|
1495
|
+
for (const namespace of pending.namespaces) {
|
|
1496
|
+
const aliases = this.pendingGroupInlineAckNamespaces?.get(namespace);
|
|
1497
|
+
if (!aliases)
|
|
1498
|
+
continue;
|
|
1499
|
+
aliases.delete(ns);
|
|
1500
|
+
if (aliases.size === 0)
|
|
1501
|
+
this.pendingGroupInlineAckNamespaces?.delete(namespace);
|
|
1502
|
+
}
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
dropPendingGroupInlineAck(ns, seq) {
|
|
1506
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
1507
|
+
if (pending?.seq === seq)
|
|
1508
|
+
this.removePendingGroupInlineAck(ns, pending);
|
|
1509
|
+
}
|
|
1510
|
+
resolvePendingGroupInlineAckNamespace(ns) {
|
|
1511
|
+
const candidates = this.pendingGroupInlineAckNamespaces?.get(ns);
|
|
1512
|
+
if (!candidates || candidates.size !== 1)
|
|
1513
|
+
return ns;
|
|
1514
|
+
const canonicalNs = candidates.values().next().value;
|
|
1515
|
+
return typeof canonicalNs === 'string' && Boolean(this.pendingGroupInlineAcks?.has(canonicalNs))
|
|
1516
|
+
? canonicalNs
|
|
1517
|
+
: ns;
|
|
1518
|
+
}
|
|
1519
|
+
completePendingGroupInlineAck(ns, upToSeq) {
|
|
1520
|
+
if (!ns || !Number.isSafeInteger(upToSeq) || upToSeq <= 0)
|
|
1521
|
+
return;
|
|
1522
|
+
const candidates = this.pendingGroupInlineAckNamespaces?.get(ns);
|
|
1523
|
+
if (!candidates || candidates.size !== 1)
|
|
1524
|
+
return;
|
|
1525
|
+
const canonicalNs = candidates.values().next().value;
|
|
1526
|
+
if (typeof canonicalNs !== 'string')
|
|
1527
|
+
return;
|
|
1528
|
+
const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
|
|
1529
|
+
if (!pending || upToSeq < pending.seq)
|
|
1530
|
+
return;
|
|
1531
|
+
if (!pending.committed) {
|
|
1532
|
+
pending.observedAckSeq = Math.max(pending.observedAckSeq, upToSeq);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
this.removePendingGroupInlineAck(canonicalNs, pending);
|
|
1536
|
+
this.runPostInlineCommit(pending.row);
|
|
1537
|
+
}
|
|
1538
|
+
markPendingGroupInlineAckCommitted(ns, seq) {
|
|
1539
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
1540
|
+
if (!pending || pending.seq !== seq)
|
|
1541
|
+
return false;
|
|
1542
|
+
pending.committed = true;
|
|
1543
|
+
if (pending.observedAckSeq < seq)
|
|
1544
|
+
return true;
|
|
1545
|
+
this.removePendingGroupInlineAck(ns, pending);
|
|
1546
|
+
this.runPostInlineCommit(pending.row);
|
|
1547
|
+
return true;
|
|
1548
|
+
}
|
|
1549
|
+
retryPendingGroupInlineAck(ns, seq, reason, ackThrough = seq) {
|
|
1550
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
1551
|
+
if (!pending || pending.seq !== seq)
|
|
1552
|
+
return false;
|
|
1553
|
+
if (pending.acking || !pending.committed)
|
|
1554
|
+
return true;
|
|
1555
|
+
const groupId = ns.startsWith('group:') ? ns.slice('group:'.length) : '';
|
|
1556
|
+
if (!groupId)
|
|
1557
|
+
return true;
|
|
1558
|
+
pending.acking = true;
|
|
1559
|
+
const requestedSeq = Math.max(seq, ackThrough);
|
|
1560
|
+
const generation = this.inlineAckGeneration;
|
|
1561
|
+
try {
|
|
1562
|
+
Promise.resolve(this.runtime.client.call('group.v2.ack', {
|
|
1563
|
+
group_id: groupId,
|
|
1564
|
+
...(pending.groupAid ? { group_aid: pending.groupAid } : {}),
|
|
1565
|
+
up_to_seq: requestedSeq,
|
|
1566
|
+
_rpc_background: true,
|
|
1567
|
+
})).then((result) => {
|
|
1568
|
+
this.onGroupV2AckResult(groupId, result, requestedSeq, generation);
|
|
1569
|
+
}).catch((error) => {
|
|
1570
|
+
const current = this.pendingGroupInlineAcks?.get(ns);
|
|
1571
|
+
if (current === pending)
|
|
1572
|
+
current.acking = false;
|
|
1573
|
+
this.runtime.client._clientLog.debug(`group.v2 ${reason} ack failed: group=${groupId} ${formatDeliveryError(error)}`);
|
|
1574
|
+
}).finally(() => {
|
|
1575
|
+
const current = this.pendingGroupInlineAcks?.get(ns);
|
|
1576
|
+
if (current === pending)
|
|
1577
|
+
current.acking = false;
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
catch (error) {
|
|
1581
|
+
pending.acking = false;
|
|
1582
|
+
this.runtime.client._clientLog.debug(`group.v2 ${reason} ack failed: group=${groupId} ${formatDeliveryError(error)}`);
|
|
1583
|
+
}
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
runPostInlineCommit(row) {
|
|
1265
1587
|
const client = this.runtime.client;
|
|
1266
|
-
if (
|
|
1588
|
+
if (typeof client._postInlineV2CommitAndAck === 'function') {
|
|
1589
|
+
client._postInlineV2CommitAndAck(row);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
onGroupV2AckSucceeded(groupId, upToSeq, generation = this.inlineAckGeneration) {
|
|
1593
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
1267
1594
|
return;
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1595
|
+
const gid = String(groupId ?? '').trim();
|
|
1596
|
+
if (!gid || !Number.isSafeInteger(upToSeq) || upToSeq <= 0)
|
|
1597
|
+
return;
|
|
1598
|
+
this.completePendingGroupInlineAck(`group:${gid}`, upToSeq);
|
|
1599
|
+
}
|
|
1600
|
+
onGroupV2AckResult(groupId, result, requestedSeq, generation = this.inlineAckGeneration) {
|
|
1601
|
+
if (!this.pendingGroupInlineAcks?.size || !this.inlineGenerationIsCurrent(generation))
|
|
1602
|
+
return;
|
|
1603
|
+
this.onGroupV2AckSucceeded(groupId, this.resolveActualAckSeq(result, requestedSeq), generation);
|
|
1604
|
+
}
|
|
1605
|
+
onGroupV2PullAckResult(groupId, result, requestedSeq, generation) {
|
|
1606
|
+
if (!this.pendingGroupInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
|
|
1607
|
+
|| !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
1608
|
+
return;
|
|
1609
|
+
let actualAckSeq = requestedSeq;
|
|
1610
|
+
const response = isJsonObject(result) ? result : null;
|
|
1611
|
+
const cursor = response && isJsonObject(response.cursor)
|
|
1612
|
+
? response.cursor
|
|
1613
|
+
: null;
|
|
1614
|
+
if (cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq')) {
|
|
1615
|
+
const raw = cursor.current_seq;
|
|
1616
|
+
actualAckSeq = typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
|
|
1617
|
+
}
|
|
1618
|
+
this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
|
|
1619
|
+
}
|
|
1620
|
+
resolveActualAckSeq(result, requestedSeq) {
|
|
1621
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
1622
|
+
return 0;
|
|
1623
|
+
const response = isJsonObject(result) ? result : null;
|
|
1624
|
+
if (!response)
|
|
1625
|
+
return requestedSeq;
|
|
1626
|
+
let raw;
|
|
1627
|
+
if (Object.prototype.hasOwnProperty.call(response, 'effective_ack_seq'))
|
|
1628
|
+
raw = response.effective_ack_seq;
|
|
1629
|
+
else if (Object.prototype.hasOwnProperty.call(response, 'ack_seq'))
|
|
1630
|
+
raw = response.ack_seq;
|
|
1631
|
+
else if (Object.prototype.hasOwnProperty.call(response, 'cursor')) {
|
|
1632
|
+
const cursor = response.cursor;
|
|
1633
|
+
const cursorObject = isJsonObject(cursor) ? cursor : null;
|
|
1634
|
+
if (cursorObject) {
|
|
1635
|
+
if (!Object.prototype.hasOwnProperty.call(cursorObject, 'current_seq'))
|
|
1636
|
+
return 0;
|
|
1637
|
+
raw = cursorObject.current_seq;
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
raw = cursor;
|
|
1281
1641
|
}
|
|
1282
|
-
contigBefore = client._repairPushContiguousBound(ns, pushSeq, hasPayload, '_raw.peer.v2.message_received');
|
|
1283
1642
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1643
|
+
else
|
|
1644
|
+
return requestedSeq;
|
|
1645
|
+
return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
|
|
1646
|
+
}
|
|
1647
|
+
async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
1648
|
+
const client = this.runtime.client;
|
|
1649
|
+
const before = this.syncState(ns);
|
|
1650
|
+
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
1651
|
+
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
1652
|
+
: (pushSeq > before.ack ? ['tail'] : []);
|
|
1653
|
+
let result = { messages: [], raw_count: 0 };
|
|
1654
|
+
let tailHead = null;
|
|
1655
|
+
const run = async (request, background) => {
|
|
1656
|
+
const invoke = async () => {
|
|
1657
|
+
const live = this.syncState(ns);
|
|
1658
|
+
if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
|
|
1659
|
+
return { messages: [], raw_count: 0 };
|
|
1660
|
+
if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
|
|
1661
|
+
return { messages: [], raw_count: 0 };
|
|
1662
|
+
if (request.window_mode === 'tail') {
|
|
1663
|
+
const tailResult = await client._v2E2EE.pullV2TailInternal({
|
|
1664
|
+
window_mode: 'tail',
|
|
1665
|
+
after_seq: live.ack,
|
|
1666
|
+
limit: pageLimit,
|
|
1667
|
+
});
|
|
1668
|
+
tailHead = this.syncState(ns).head;
|
|
1669
|
+
return tailResult;
|
|
1670
|
+
}
|
|
1671
|
+
const after = live.ack;
|
|
1672
|
+
const opts = { gateLocked: true };
|
|
1673
|
+
if (request.max_pages !== undefined)
|
|
1674
|
+
opts.maxPages = request.max_pages;
|
|
1675
|
+
const messages = await client._pullV2(after, pageLimit, opts);
|
|
1676
|
+
return { messages, raw_count: messages.length };
|
|
1677
|
+
};
|
|
1678
|
+
if (!client._rpcPipeline)
|
|
1679
|
+
return await invoke();
|
|
1680
|
+
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
|
|
1681
|
+
return await client._rpcPipeline.runPullSerialized(key, invoke, background);
|
|
1682
|
+
};
|
|
1683
|
+
if (order.length === 1 && order[0] === 'tail') {
|
|
1684
|
+
result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
|
|
1685
|
+
}
|
|
1686
|
+
const state = this.syncState(ns);
|
|
1687
|
+
const needsForward = state.ack < state.tail - 1;
|
|
1688
|
+
const backgroundEnabled = client._sessionOptions?.background_sync !== false;
|
|
1689
|
+
if (backgroundEnabled && (order.length === 1 && order[0] === 'forward' || (order.length === 1 && order[0] === 'tail' && needsForward))) {
|
|
1690
|
+
result = await run({ after_seq: state.ack, limit: pageLimit, ...(order[0] === 'tail' ? { max_pages: 1 } : {}) }, true);
|
|
1691
|
+
}
|
|
1692
|
+
if (tailHead !== null) {
|
|
1693
|
+
this.markRealtimeTailCompleted(result, tailHead);
|
|
1694
|
+
}
|
|
1695
|
+
return result;
|
|
1696
|
+
}
|
|
1697
|
+
async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
1698
|
+
const client = this.runtime.client;
|
|
1699
|
+
const before = this.syncState(ns);
|
|
1700
|
+
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
1701
|
+
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
1702
|
+
: (pushSeq > before.ack ? ['tail'] : []);
|
|
1703
|
+
let result = { messages: [], raw_count: 0 };
|
|
1704
|
+
let tailHead = null;
|
|
1705
|
+
const run = async (request, background) => {
|
|
1706
|
+
const invoke = async () => {
|
|
1707
|
+
const live = this.syncState(ns);
|
|
1708
|
+
if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
|
|
1709
|
+
return { messages: [], raw_count: 0 };
|
|
1710
|
+
if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
|
|
1711
|
+
return { messages: [], raw_count: 0 };
|
|
1712
|
+
if (request.window_mode === 'tail') {
|
|
1713
|
+
const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
|
|
1714
|
+
group_id: groupId,
|
|
1715
|
+
window_mode: 'tail',
|
|
1716
|
+
after_seq: live.ack,
|
|
1717
|
+
limit: pageLimit,
|
|
1718
|
+
});
|
|
1719
|
+
tailHead = this.syncState(ns).head;
|
|
1720
|
+
return tailResult;
|
|
1308
1721
|
}
|
|
1722
|
+
const opts = { gateLocked: true };
|
|
1723
|
+
if (request.max_pages !== undefined)
|
|
1724
|
+
opts.maxPages = request.max_pages;
|
|
1725
|
+
const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, opts);
|
|
1726
|
+
return { messages, raw_count: messages.length };
|
|
1727
|
+
};
|
|
1728
|
+
if (!client._rpcPipeline)
|
|
1729
|
+
return await invoke();
|
|
1730
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
|
|
1731
|
+
return await client._rpcPipeline.runPullSerialized(key, invoke, background);
|
|
1732
|
+
};
|
|
1733
|
+
if (order.length === 1 && order[0] === 'tail') {
|
|
1734
|
+
result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
|
|
1735
|
+
}
|
|
1736
|
+
const state = this.syncState(ns);
|
|
1737
|
+
const needsForward = state.ack < state.tail - 1;
|
|
1738
|
+
const backgroundEnabled = client._sessionOptions?.background_sync !== false;
|
|
1739
|
+
if (backgroundEnabled && (order.length === 1 && order[0] === 'forward' || (order.length === 1 && order[0] === 'tail' && needsForward))) {
|
|
1740
|
+
result = await run({ after_seq: state.ack, limit: pageLimit, ...(order[0] === 'tail' ? { max_pages: 1 } : {}) }, true);
|
|
1741
|
+
}
|
|
1742
|
+
if (tailHead !== null) {
|
|
1743
|
+
this.markRealtimeTailCompleted(result, tailHead);
|
|
1744
|
+
}
|
|
1745
|
+
return result;
|
|
1746
|
+
}
|
|
1747
|
+
async runP2PForwardRecovery(ns, pageLimit) {
|
|
1748
|
+
const client = this.runtime.client;
|
|
1749
|
+
const request = { after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
|
|
1750
|
+
const invoke = async () => {
|
|
1751
|
+
const after = this.syncState(ns).ack;
|
|
1752
|
+
const messages = await client._pullV2(after, pageLimit, { gateLocked: true, maxPages: 1 });
|
|
1753
|
+
return { messages, raw_count: messages.length };
|
|
1754
|
+
};
|
|
1755
|
+
if (!client._rpcPipeline)
|
|
1756
|
+
return await invoke();
|
|
1757
|
+
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
|
|
1758
|
+
return await client._rpcPipeline.runPullSerialized(key, invoke, true);
|
|
1759
|
+
}
|
|
1760
|
+
async runGroupForwardRecovery(groupId, ns, pageLimit) {
|
|
1761
|
+
const client = this.runtime.client;
|
|
1762
|
+
const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
|
|
1763
|
+
const invoke = async () => {
|
|
1764
|
+
const after = this.syncState(ns).ack;
|
|
1765
|
+
const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages: 1 });
|
|
1766
|
+
return { messages, raw_count: messages.length };
|
|
1767
|
+
};
|
|
1768
|
+
if (!client._rpcPipeline)
|
|
1769
|
+
return await invoke();
|
|
1770
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
1771
|
+
return await client._rpcPipeline.runPullSerialized(key, invoke, true);
|
|
1772
|
+
}
|
|
1773
|
+
syncState(ns) {
|
|
1774
|
+
const tracker = this.runtime.client._seqTracker;
|
|
1775
|
+
if (typeof tracker.getSyncState === 'function')
|
|
1776
|
+
return tracker.getSyncState(ns);
|
|
1777
|
+
const ack = Number(tracker.getContiguousSeq(ns) || 0);
|
|
1778
|
+
const maxSeen = Number(tracker.getMaxSeenSeq?.(ns) || 0);
|
|
1779
|
+
return { ack, tail: ack, head: Math.max(ack, maxSeen) };
|
|
1780
|
+
}
|
|
1781
|
+
inlineEnvelope(row) {
|
|
1782
|
+
const raw = row.envelope_json;
|
|
1783
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
1784
|
+
return null;
|
|
1785
|
+
try {
|
|
1786
|
+
const parsed = JSON.parse(raw);
|
|
1787
|
+
return isJsonObject(parsed) ? parsed : null;
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
inlineRowKind(row) {
|
|
1794
|
+
if (row.version === 'v1') {
|
|
1795
|
+
if (Object.prototype.hasOwnProperty.call(row, 'envelope_json'))
|
|
1796
|
+
return 'invalid';
|
|
1797
|
+
return 'v1';
|
|
1798
|
+
}
|
|
1799
|
+
if (row.version === 'v2') {
|
|
1800
|
+
if (typeof row.envelope_json !== 'string' || row.envelope_json.length === 0)
|
|
1801
|
+
return 'invalid';
|
|
1802
|
+
if (Object.prototype.hasOwnProperty.call(row, 'legacy_v1')
|
|
1803
|
+
|| Object.prototype.hasOwnProperty.call(row, 'payload'))
|
|
1804
|
+
return 'invalid';
|
|
1805
|
+
return 'v2';
|
|
1806
|
+
}
|
|
1807
|
+
return 'invalid';
|
|
1808
|
+
}
|
|
1809
|
+
inlineTargetBindingMatches(head, row, envelope) {
|
|
1810
|
+
const client = this.runtime.client;
|
|
1811
|
+
if (!this.messageTargetsCurrentInstance(head))
|
|
1812
|
+
return false;
|
|
1813
|
+
const targetValue = (source, targetKey, legacyKey) => {
|
|
1814
|
+
if (Object.prototype.hasOwnProperty.call(source, targetKey)) {
|
|
1815
|
+
return typeof source[targetKey] === 'string' ? source[targetKey].trim() : '';
|
|
1309
1816
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1817
|
+
if (Object.prototype.hasOwnProperty.call(source, legacyKey)) {
|
|
1818
|
+
return typeof source[legacyKey] === 'string' ? source[legacyKey].trim() : '';
|
|
1819
|
+
}
|
|
1820
|
+
return null;
|
|
1821
|
+
};
|
|
1822
|
+
const headDevice = targetValue(head, 'target_device_id', 'device_id');
|
|
1823
|
+
if (!headDevice || headDevice !== client._deviceId)
|
|
1824
|
+
return false;
|
|
1825
|
+
const rowDevice = targetValue(row, 'target_device_id', 'device_id');
|
|
1826
|
+
if (rowDevice !== null && (!rowDevice || rowDevice !== headDevice))
|
|
1827
|
+
return false;
|
|
1828
|
+
const localSlot = slotIsolationKey(client._slotId);
|
|
1829
|
+
const headSlot = targetValue(head, 'target_slot_id', 'slot_id');
|
|
1830
|
+
if (headSlot !== null && slotIsolationKey(headSlot) !== localSlot)
|
|
1831
|
+
return false;
|
|
1832
|
+
const rowSlot = targetValue(row, 'target_slot_id', 'slot_id');
|
|
1833
|
+
if (rowSlot !== null && (!rowSlot || slotIsolationKey(rowSlot) !== localSlot))
|
|
1834
|
+
return false;
|
|
1835
|
+
if (headSlot !== null && rowSlot !== null && slotIsolationKey(rowSlot) !== slotIsolationKey(headSlot))
|
|
1836
|
+
return false;
|
|
1837
|
+
if (!envelope)
|
|
1838
|
+
return true;
|
|
1839
|
+
if (Object.prototype.hasOwnProperty.call(envelope, 'recipients'))
|
|
1840
|
+
return false;
|
|
1841
|
+
if (!isJsonObject(envelope.recipient))
|
|
1842
|
+
return false;
|
|
1843
|
+
const recipient = envelope.recipient;
|
|
1844
|
+
if (typeof recipient.aid !== 'string' || typeof recipient.device_id !== 'string'
|
|
1845
|
+
|| typeof client._aid !== 'string' || typeof client._deviceId !== 'string')
|
|
1846
|
+
return false;
|
|
1847
|
+
const recipientAid = recipient.aid;
|
|
1848
|
+
const recipientDevice = recipient.device_id;
|
|
1849
|
+
if (!recipientAid || recipientAid !== client._aid || !client._deviceId)
|
|
1850
|
+
return false;
|
|
1851
|
+
const hasDeliveryDevice = Object.prototype.hasOwnProperty.call(envelope, 'delivery_device_id');
|
|
1852
|
+
const deliveryDevice = envelope.delivery_device_id;
|
|
1853
|
+
if (hasDeliveryDevice
|
|
1854
|
+
&& (typeof deliveryDevice !== 'string' || !deliveryDevice || deliveryDevice !== client._deviceId))
|
|
1855
|
+
return false;
|
|
1856
|
+
if (recipientDevice)
|
|
1857
|
+
return recipientDevice === client._deviceId;
|
|
1858
|
+
return hasDeliveryDevice;
|
|
1859
|
+
}
|
|
1860
|
+
inlineRequiredBinding(left, leftKeys, right, rightKeys) {
|
|
1861
|
+
const leftValue = this.consistentBindingValue(left, leftKeys);
|
|
1862
|
+
const rightValue = this.consistentBindingValue(right, rightKeys);
|
|
1863
|
+
return leftValue !== null && rightValue !== null && leftValue === rightValue;
|
|
1864
|
+
}
|
|
1865
|
+
consistentBindingValue(source, keys) {
|
|
1866
|
+
let value = null;
|
|
1867
|
+
for (const key of keys) {
|
|
1868
|
+
if (!Object.prototype.hasOwnProperty.call(source, key))
|
|
1869
|
+
continue;
|
|
1870
|
+
if (typeof source[key] !== 'string')
|
|
1871
|
+
return null;
|
|
1872
|
+
const candidate = source[key].trim();
|
|
1873
|
+
if (!candidate || (value !== null && candidate !== value))
|
|
1874
|
+
return null;
|
|
1875
|
+
value = candidate;
|
|
1876
|
+
}
|
|
1877
|
+
return value;
|
|
1878
|
+
}
|
|
1879
|
+
inlineHeadMatches(head, row, pushSeq, groupId = '') {
|
|
1880
|
+
const rowSeq = this.inlineSafePositiveSeq(row.seq);
|
|
1881
|
+
if (rowSeq === null || rowSeq !== pushSeq)
|
|
1882
|
+
return false;
|
|
1883
|
+
if (!this.inlineRequiredBinding(head, ['message_id'], row, ['message_id']))
|
|
1884
|
+
return false;
|
|
1885
|
+
if (groupId) {
|
|
1886
|
+
const rowGroupId = typeof row.group_id === 'string' ? row.group_id.trim() : '';
|
|
1887
|
+
if (!rowGroupId || rowGroupId !== groupId)
|
|
1888
|
+
return false;
|
|
1889
|
+
if (Object.prototype.hasOwnProperty.call(head, 'group_aid')
|
|
1890
|
+
&& (typeof head.group_aid !== 'string' || !head.group_aid.trim()))
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
if (!this.inlineRequiredBinding(head, ['from_aid', 'sender_aid', 'from'], row, ['from_aid', 'sender_aid', 'from'])
|
|
1894
|
+
|| !this.inlineRequiredBinding(head, ['sender_device_id'], row, ['sender_device_id']))
|
|
1895
|
+
return false;
|
|
1896
|
+
const kind = this.inlineRowKind(row);
|
|
1897
|
+
if (kind === 'invalid')
|
|
1898
|
+
return false;
|
|
1899
|
+
const envelope = kind === 'v2' ? this.inlineEnvelope(row) : null;
|
|
1900
|
+
if (kind === 'v2' && !envelope)
|
|
1901
|
+
return false;
|
|
1902
|
+
if (!this.inlineTargetBindingMatches(head, row, envelope))
|
|
1903
|
+
return false;
|
|
1904
|
+
const logicalTo = groupId ? null : this.consistentBindingValue(head, ['to_aid', 'to']);
|
|
1905
|
+
if (!groupId && !logicalTo)
|
|
1906
|
+
return false;
|
|
1907
|
+
if (kind === 'v1') {
|
|
1908
|
+
if (groupId) {
|
|
1909
|
+
if (Object.prototype.hasOwnProperty.call(row, 'legacy_v1'))
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
else {
|
|
1913
|
+
if (Object.prototype.hasOwnProperty.call(row, 'payload'))
|
|
1914
|
+
return false;
|
|
1915
|
+
const legacy = isJsonObject(row.legacy_v1)
|
|
1916
|
+
? row.legacy_v1
|
|
1917
|
+
: null;
|
|
1918
|
+
const legacyTo = legacy ? this.consistentBindingValue(legacy, ['to']) : null;
|
|
1919
|
+
if (!legacyTo || legacyTo !== logicalTo)
|
|
1920
|
+
return false;
|
|
1312
1921
|
}
|
|
1313
1922
|
}
|
|
1314
|
-
if (
|
|
1315
|
-
|
|
1923
|
+
if (envelope) {
|
|
1924
|
+
const envelopeType = typeof envelope.type === 'string' ? envelope.type.trim() : '';
|
|
1925
|
+
const envelopeVersion = typeof envelope.version === 'string' ? envelope.version.trim() : '';
|
|
1926
|
+
const hasEnvelopeGroupId = Object.prototype.hasOwnProperty.call(envelope, 'group_id');
|
|
1927
|
+
const envelopeGroupId = typeof envelope.group_id === 'string' ? envelope.group_id.trim() : '';
|
|
1928
|
+
if (groupId) {
|
|
1929
|
+
if (envelopeType !== 'e2ee.group_encrypted'
|
|
1930
|
+
|| envelopeVersion !== 'v2'
|
|
1931
|
+
|| (hasEnvelopeGroupId && envelopeGroupId !== groupId))
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
else if (envelopeType !== 'e2ee.p2p_encrypted'
|
|
1935
|
+
|| envelopeVersion !== 'v2'
|
|
1936
|
+
|| (hasEnvelopeGroupId
|
|
1937
|
+
&& (typeof envelope.group_id !== 'string' || envelopeGroupId !== '')))
|
|
1938
|
+
return false;
|
|
1939
|
+
if (!isJsonObject(envelope.aad))
|
|
1940
|
+
return false;
|
|
1941
|
+
const aad = envelope.aad;
|
|
1942
|
+
if (!this.inlineRequiredBinding(aad, ['message_id'], row, ['message_id'])
|
|
1943
|
+
|| !this.inlineRequiredBinding(aad, ['from'], row, ['from_aid', 'sender_aid', 'from'])
|
|
1944
|
+
|| !this.inlineRequiredBinding(aad, ['from_device'], row, ['sender_device_id']))
|
|
1945
|
+
return false;
|
|
1946
|
+
const aadGroupId = this.consistentBindingValue(aad, ['group_id']);
|
|
1947
|
+
if (groupId) {
|
|
1948
|
+
if (aadGroupId !== groupId)
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
else {
|
|
1952
|
+
if ((Object.prototype.hasOwnProperty.call(aad, 'group_id') && aadGroupId !== '')
|
|
1953
|
+
|| this.consistentBindingValue(aad, ['to']) !== logicalTo)
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1316
1956
|
}
|
|
1317
|
-
|
|
1957
|
+
return true;
|
|
1958
|
+
}
|
|
1959
|
+
inlineSafePositiveSeq(value) {
|
|
1960
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
1961
|
+
}
|
|
1962
|
+
realtimeHeadSeq(value, inlinePresent) {
|
|
1963
|
+
const strict = this.inlineSafePositiveSeq(value);
|
|
1964
|
+
if (strict !== null || inlinePresent || typeof value !== 'string' || !value.trim())
|
|
1965
|
+
return strict;
|
|
1966
|
+
return this.inlineSafePositiveSeq(Number(value));
|
|
1967
|
+
}
|
|
1968
|
+
async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
|
|
1969
|
+
const client = this.runtime.client;
|
|
1970
|
+
client._clientLog.warn(`V2 P2P push has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
|
|
1971
|
+
if (!this.beginRealtimeSync(ns))
|
|
1318
1972
|
return;
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1973
|
+
try {
|
|
1974
|
+
await this.runP2PRealtimeSync(ns, this.syncState(ns).head, 50, true);
|
|
1975
|
+
}
|
|
1976
|
+
catch (exc) {
|
|
1977
|
+
client._clientLog.warn(`V2 P2P invalid-seq Tail failed: ${formatDeliveryError(exc)}`);
|
|
1978
|
+
}
|
|
1979
|
+
finally {
|
|
1980
|
+
this.endRealtimeSync(ns);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async recoverInvalidGroupRealtimeHead(groupId, ns, rawSeq) {
|
|
1984
|
+
const client = this.runtime.client;
|
|
1985
|
+
client._clientLog.warn(`V2 group push has invalid seq; forcing authoritative Tail: group=${groupId} seq=${String(rawSeq)}`);
|
|
1986
|
+
if (!this.beginRealtimeSync(ns))
|
|
1987
|
+
return;
|
|
1988
|
+
try {
|
|
1989
|
+
await this.runGroupRealtimeSync(groupId, ns, this.syncState(ns).head, 50, true);
|
|
1990
|
+
}
|
|
1991
|
+
catch (exc) {
|
|
1992
|
+
client._clientLog.warn(`V2 group invalid-seq Tail failed: group=${groupId} ${formatDeliveryError(exc)}`);
|
|
1993
|
+
}
|
|
1994
|
+
finally {
|
|
1995
|
+
this.endRealtimeSync(ns);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
inlineStateIsCaughtUp(ns, pushSeq) {
|
|
1999
|
+
const client = this.runtime.client;
|
|
2000
|
+
const state = this.syncState(ns);
|
|
2001
|
+
if (state.ack !== state.tail || state.tail !== state.head || pushSeq !== state.ack + 1)
|
|
2002
|
+
return false;
|
|
2003
|
+
if (Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? state.head) > state.head)
|
|
2004
|
+
return false;
|
|
2005
|
+
const pending = client._pendingOrderedMsgs.get(ns);
|
|
2006
|
+
return !pending || pending.size === 0;
|
|
2007
|
+
}
|
|
2008
|
+
async decodeInlineP2P(row) {
|
|
2009
|
+
const client = this.runtime.client;
|
|
2010
|
+
if (this.inlineRowKind(row) === 'v2') {
|
|
2011
|
+
const decrypted = await client._decryptV2PushMessage(row);
|
|
2012
|
+
return isJsonObject(decrypted) ? decrypted : null;
|
|
2013
|
+
}
|
|
2014
|
+
const legacy = isJsonObject(row.legacy_v1) ? row.legacy_v1 : null;
|
|
2015
|
+
const payload = legacy?.payload;
|
|
2016
|
+
if (!legacy || payload === undefined || payload === null)
|
|
2017
|
+
return null;
|
|
2018
|
+
const payloadType = isJsonObject(payload)
|
|
2019
|
+
? String(payload.type ?? '').trim()
|
|
2020
|
+
: '';
|
|
2021
|
+
if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
2022
|
+
return null;
|
|
2023
|
+
const fromAid = String(row.from_aid ?? '').trim();
|
|
2024
|
+
const seq = this.inlineSafePositiveSeq(row.seq);
|
|
2025
|
+
if (seq === null)
|
|
2026
|
+
return null;
|
|
2027
|
+
const result = {
|
|
2028
|
+
message_id: String(row.message_id ?? ''),
|
|
2029
|
+
from: fromAid,
|
|
2030
|
+
to: String(legacy.to ?? client._aid ?? ''),
|
|
2031
|
+
seq,
|
|
2032
|
+
type: String(row.type ?? ''),
|
|
2033
|
+
timestamp: row.t_server,
|
|
2034
|
+
payload: payload,
|
|
2035
|
+
encrypted: false,
|
|
2036
|
+
direction: String(row.direction ?? '').trim() || (fromAid && fromAid === client._aid ? 'outbound_sync' : 'inbound'),
|
|
2037
|
+
};
|
|
2038
|
+
if (row.sender_device_id !== undefined)
|
|
2039
|
+
result.sender_device_id = row.sender_device_id;
|
|
2040
|
+
return result;
|
|
2041
|
+
}
|
|
2042
|
+
async decodeInlineGroup(row, groupId, groupAid) {
|
|
2043
|
+
const client = this.runtime.client;
|
|
2044
|
+
if (this.inlineRowKind(row) === 'v2') {
|
|
2045
|
+
const decrypted = await client._decryptV2PushMessage(row);
|
|
2046
|
+
if (!isJsonObject(decrypted))
|
|
2047
|
+
return null;
|
|
2048
|
+
const decoded = decrypted;
|
|
2049
|
+
if (Object.prototype.hasOwnProperty.call(decoded, 'group_id')) {
|
|
2050
|
+
const decryptedGroupId = decoded.group_id;
|
|
2051
|
+
if (typeof decryptedGroupId !== 'string' || !decryptedGroupId.trim() || decryptedGroupId.trim() !== groupId)
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
return normalizeGroupMentionMode({
|
|
2055
|
+
...decoded,
|
|
2056
|
+
group_id: groupId,
|
|
2057
|
+
group_aid: groupAid || groupId,
|
|
2058
|
+
}, row, row.envelope_json);
|
|
2059
|
+
}
|
|
2060
|
+
if (this.recallEventFromGroupMessage(row))
|
|
2061
|
+
return null;
|
|
2062
|
+
const payload = row.payload;
|
|
2063
|
+
if (payload === undefined || payload === null)
|
|
2064
|
+
return null;
|
|
2065
|
+
const payloadType = isJsonObject(payload)
|
|
2066
|
+
? String(payload.type ?? '').trim()
|
|
2067
|
+
: '';
|
|
2068
|
+
if (payloadType === 'e2ee.encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
2069
|
+
return null;
|
|
2070
|
+
const seq = this.inlineSafePositiveSeq(row.seq);
|
|
2071
|
+
if (seq === null)
|
|
2072
|
+
return null;
|
|
2073
|
+
const result = {
|
|
2074
|
+
message_id: String(row.message_id ?? ''),
|
|
2075
|
+
from: String(row.from_aid ?? ''),
|
|
2076
|
+
group_id: groupId,
|
|
2077
|
+
group_aid: groupAid || groupId,
|
|
2078
|
+
seq,
|
|
2079
|
+
type: String(row.type ?? ''),
|
|
2080
|
+
timestamp: row.t_server,
|
|
2081
|
+
payload: payload,
|
|
2082
|
+
encrypted: false,
|
|
2083
|
+
};
|
|
2084
|
+
if (row.sender_device_id !== undefined)
|
|
2085
|
+
result.sender_device_id = row.sender_device_id;
|
|
2086
|
+
return normalizeGroupMentionMode(result, row, row.envelope_json);
|
|
2087
|
+
}
|
|
2088
|
+
decodedInlineMatches(head, decoded, pushSeq, groupId = '') {
|
|
2089
|
+
if (this.inlineSafePositiveSeq(decoded.seq) !== pushSeq)
|
|
2090
|
+
return false;
|
|
2091
|
+
if (!this.inlineRequiredBinding(head, ['message_id'], decoded, ['message_id']))
|
|
2092
|
+
return false;
|
|
2093
|
+
const headSender = this.consistentBindingValue(head, ['from_aid', 'sender_aid', 'from']);
|
|
2094
|
+
const decodedSender = this.consistentBindingValue(decoded, ['from', 'from_aid', 'sender_aid']);
|
|
2095
|
+
if (!headSender || decodedSender !== headSender)
|
|
2096
|
+
return false;
|
|
2097
|
+
if (groupId)
|
|
2098
|
+
return this.consistentBindingValue(decoded, ['group_id']) === groupId;
|
|
2099
|
+
const headTo = this.consistentBindingValue(head, ['to_aid', 'to']);
|
|
2100
|
+
const decodedTo = this.consistentBindingValue(decoded, ['to', 'to_aid']);
|
|
2101
|
+
return Boolean(headTo) && decodedTo === headTo;
|
|
2102
|
+
}
|
|
2103
|
+
async publishInlineMessage(event, ns, seq, payload, generation) {
|
|
2104
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2105
|
+
return false;
|
|
2106
|
+
const client = this.runtime.client;
|
|
2107
|
+
const tracker = client._seqTracker;
|
|
2108
|
+
const snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
|
|
2109
|
+
let delivered = false;
|
|
2110
|
+
try {
|
|
2111
|
+
const published = client._publishAppEvent(event, payload, 'inline-push');
|
|
2112
|
+
if (isPromiseLike(published))
|
|
2113
|
+
await published;
|
|
2114
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2115
|
+
return false;
|
|
2116
|
+
delivered = true;
|
|
2117
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2118
|
+
return false;
|
|
2119
|
+
const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
|
|
2120
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2121
|
+
return false;
|
|
2122
|
+
tracker.commitTailWindow(ns, {
|
|
2123
|
+
windowStartSeq: seq,
|
|
2124
|
+
headSeq: seq,
|
|
2125
|
+
coveredThroughSeq: seq,
|
|
2126
|
+
pageLimit: 1,
|
|
2127
|
+
});
|
|
2128
|
+
const committed = this.syncState(ns);
|
|
2129
|
+
if (needsPull || committed.ack !== seq || committed.tail !== seq || committed.head !== seq) {
|
|
2130
|
+
throw new Error('inline realtime cursor did not collapse after consume');
|
|
2131
|
+
}
|
|
2132
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2133
|
+
return false;
|
|
2134
|
+
client._commitSeqTrackerState(ns);
|
|
2135
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2136
|
+
return false;
|
|
2137
|
+
this.markPublishedSeq(ns, seq);
|
|
2138
|
+
return true;
|
|
2139
|
+
}
|
|
2140
|
+
catch (exc) {
|
|
2141
|
+
if (this.inlineGenerationIsCurrent(generation)
|
|
2142
|
+
&& typeof tracker.restoreNamespaceSnapshot === 'function')
|
|
2143
|
+
tracker.restoreNamespaceSnapshot(ns, snapshot);
|
|
2144
|
+
if (this.inlineGenerationIsCurrent(generation) && delivered)
|
|
2145
|
+
this.markPublishedSeq(ns, seq);
|
|
2146
|
+
client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
async tryConsumeInlineP2P(head, ns, pushSeq, expectedVersion, inlineGeneration) {
|
|
2151
|
+
const client = this.runtime.client;
|
|
2152
|
+
if (!isJsonObject(head.inline_message))
|
|
2153
|
+
return false;
|
|
2154
|
+
const row = { ...head.inline_message };
|
|
2155
|
+
if (expectedVersion !== undefined && this.inlineRowKind(row) !== expectedVersion)
|
|
2156
|
+
return false;
|
|
2157
|
+
const runner = client._tryRunInlineRealtime;
|
|
2158
|
+
if (typeof runner !== 'function')
|
|
2159
|
+
return false;
|
|
2160
|
+
const generation = inlineGeneration ?? this.inlineAckGeneration;
|
|
2161
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2162
|
+
return false;
|
|
2163
|
+
const result = await runner.call(client, ns, pushSeq, async () => {
|
|
2164
|
+
if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, pushSeq) || !this.inlineHeadMatches(head, row, pushSeq)
|
|
2165
|
+
|| !this.reservePendingP2PInlineAck(ns, pushSeq, row))
|
|
2166
|
+
return false;
|
|
2167
|
+
let committed = false;
|
|
2168
|
+
try {
|
|
2169
|
+
const decoded = await this.decodeInlineP2P(row);
|
|
2170
|
+
if (!this.inlineGenerationIsCurrent(generation) || !decoded || !this.inlineStateIsCaughtUp(ns, pushSeq)
|
|
2171
|
+
|| !this.decodedInlineMatches(head, decoded, pushSeq))
|
|
2172
|
+
return false;
|
|
2173
|
+
const appEvent = this.p2pAppEventForMessage(decoded);
|
|
2174
|
+
const published = await this.publishInlineMessage(appEvent.event, ns, pushSeq, appEvent.payload, generation);
|
|
2175
|
+
if (!this.inlineGenerationIsCurrent(generation) || !published)
|
|
2176
|
+
return false;
|
|
2177
|
+
committed = this.markPendingP2PInlineAckCommitted(ns, pushSeq);
|
|
2178
|
+
return committed;
|
|
2179
|
+
}
|
|
2180
|
+
finally {
|
|
2181
|
+
if (!committed && this.inlineGenerationIsCurrent(generation))
|
|
2182
|
+
this.dropPendingP2PInlineAck(ns, pushSeq);
|
|
1325
2183
|
}
|
|
1326
|
-
|
|
2184
|
+
});
|
|
2185
|
+
return this.inlineGenerationIsCurrent(generation) && result?.acquired === true && result.value === true;
|
|
2186
|
+
}
|
|
2187
|
+
async tryConsumeInlineGroup(head, groupId, ns, pushSeq, expectedVersion, inlineGeneration) {
|
|
2188
|
+
const client = this.runtime.client;
|
|
2189
|
+
if (!isJsonObject(head.inline_message))
|
|
2190
|
+
return false;
|
|
2191
|
+
const row = { ...head.inline_message };
|
|
2192
|
+
if (expectedVersion !== undefined && this.inlineRowKind(row) !== expectedVersion)
|
|
2193
|
+
return false;
|
|
2194
|
+
const runner = client._tryRunInlineRealtime;
|
|
2195
|
+
if (typeof runner !== 'function')
|
|
2196
|
+
return false;
|
|
2197
|
+
const groupAid = typeof head.group_aid === 'string' ? head.group_aid.trim() : '';
|
|
2198
|
+
const generation = inlineGeneration ?? this.inlineAckGeneration;
|
|
2199
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2200
|
+
return false;
|
|
2201
|
+
const result = await runner.call(client, ns, pushSeq, async () => {
|
|
2202
|
+
if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, pushSeq) || !this.inlineHeadMatches(head, row, pushSeq, groupId)
|
|
2203
|
+
|| !this.reservePendingGroupInlineAck(ns, pushSeq, row, groupAid))
|
|
2204
|
+
return false;
|
|
2205
|
+
let committed = false;
|
|
1327
2206
|
try {
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2207
|
+
const decoded = await this.decodeInlineGroup(row, groupId, groupAid || groupId);
|
|
2208
|
+
if (!this.inlineGenerationIsCurrent(generation) || !decoded || !this.inlineStateIsCaughtUp(ns, pushSeq)
|
|
2209
|
+
|| !this.decodedInlineMatches(head, decoded, pushSeq, groupId))
|
|
2210
|
+
return false;
|
|
2211
|
+
const published = await this.publishInlineMessage('group.message_created', ns, pushSeq, decoded, generation);
|
|
2212
|
+
if (!this.inlineGenerationIsCurrent(generation) || !published)
|
|
2213
|
+
return false;
|
|
2214
|
+
committed = this.markPendingGroupInlineAckCommitted(ns, pushSeq);
|
|
2215
|
+
return committed;
|
|
1334
2216
|
}
|
|
1335
2217
|
finally {
|
|
1336
|
-
|
|
2218
|
+
if (!committed && this.inlineGenerationIsCurrent(generation))
|
|
2219
|
+
this.dropPendingGroupInlineAck(ns, pushSeq);
|
|
1337
2220
|
}
|
|
1338
|
-
}, true, () => client._recordPendingP2pPull(ns, pushSeq)).catch((exc) => {
|
|
1339
|
-
const newContig = client._seqTracker.getContiguousSeq(ns);
|
|
1340
|
-
client._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${formatDeliveryError(exc)}`);
|
|
1341
2221
|
});
|
|
2222
|
+
return this.inlineGenerationIsCurrent(generation) && result?.acquired === true && result.value === true;
|
|
2223
|
+
}
|
|
2224
|
+
reservePendingP2PInlineAck(ns, seq, row) {
|
|
2225
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0 || this.pendingP2PInlineAcks?.has(ns)
|
|
2226
|
+
|| this.pendingInlineAckNamespaceCount() >= MAX_INLINE_PENDING_ACK_NAMESPACES)
|
|
2227
|
+
return false;
|
|
2228
|
+
const pendingMap = this.pendingP2PInlineAcks ?? new Map();
|
|
2229
|
+
this.pendingP2PInlineAcks = pendingMap;
|
|
2230
|
+
pendingMap.set(ns, {
|
|
2231
|
+
seq,
|
|
2232
|
+
row: this.inlinePostAckRow(row),
|
|
2233
|
+
acking: false,
|
|
2234
|
+
committed: false,
|
|
2235
|
+
observedAckSeq: 0,
|
|
2236
|
+
});
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
dropPendingP2PInlineAck(ns, seq) {
|
|
2240
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2241
|
+
if (pending?.seq === seq)
|
|
2242
|
+
this.pendingP2PInlineAcks?.delete(ns);
|
|
2243
|
+
}
|
|
2244
|
+
completePendingP2PInlineAck(ns, seq) {
|
|
2245
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
2246
|
+
return;
|
|
2247
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2248
|
+
if (!pending || seq < pending.seq)
|
|
2249
|
+
return;
|
|
2250
|
+
if (!pending.committed) {
|
|
2251
|
+
pending.observedAckSeq = Math.max(pending.observedAckSeq, seq);
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
this.pendingP2PInlineAcks?.delete(ns);
|
|
2255
|
+
this.runPostInlineCommit(pending.row);
|
|
2256
|
+
}
|
|
2257
|
+
markPendingP2PInlineAckCommitted(ns, seq) {
|
|
2258
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2259
|
+
if (!pending || pending.seq !== seq)
|
|
2260
|
+
return false;
|
|
2261
|
+
pending.committed = true;
|
|
2262
|
+
if (pending.observedAckSeq < seq)
|
|
2263
|
+
return true;
|
|
2264
|
+
this.pendingP2PInlineAcks?.delete(ns);
|
|
2265
|
+
this.runPostInlineCommit(pending.row);
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
retryPendingP2PInlineAck(ns, seq, reason, ackThrough = seq) {
|
|
2269
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2270
|
+
if (!pending || pending.seq !== seq)
|
|
2271
|
+
return false;
|
|
2272
|
+
if (pending.acking || !pending.committed)
|
|
2273
|
+
return true;
|
|
2274
|
+
pending.acking = true;
|
|
2275
|
+
const requestedSeq = Math.max(seq, ackThrough);
|
|
2276
|
+
const generation = this.inlineAckGeneration;
|
|
2277
|
+
this.runtime.client._withBackgroundRpc(() => this.runtime.client._ackV2(requestedSeq))
|
|
2278
|
+
.then((result) => {
|
|
2279
|
+
this.onP2PV2AckResult(result, requestedSeq, generation);
|
|
2280
|
+
}).catch((error) => {
|
|
2281
|
+
const current = this.pendingP2PInlineAcks?.get(ns);
|
|
2282
|
+
if (current === pending)
|
|
2283
|
+
current.acking = false;
|
|
2284
|
+
this.runtime.client._clientLog.debug(`message.v2 ${reason} ack failed: ${formatDeliveryError(error)}`);
|
|
2285
|
+
}).finally(() => {
|
|
2286
|
+
const current = this.pendingP2PInlineAcks?.get(ns);
|
|
2287
|
+
if (current === pending)
|
|
2288
|
+
current.acking = false;
|
|
2289
|
+
});
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
onP2PV2AckSucceeded(upToSeq, generation = this.inlineAckGeneration) {
|
|
2293
|
+
if (!this.inlineGenerationIsCurrent(generation))
|
|
2294
|
+
return;
|
|
2295
|
+
const ns = this.runtime.client._aid ? `p2p:${this.runtime.client._aid}` : '';
|
|
2296
|
+
if (!ns || !Number.isSafeInteger(upToSeq) || upToSeq <= 0)
|
|
2297
|
+
return;
|
|
2298
|
+
this.completePendingP2PInlineAck(ns, upToSeq);
|
|
2299
|
+
}
|
|
2300
|
+
onP2PV2AckResult(result, requestedSeq, generation = this.inlineAckGeneration) {
|
|
2301
|
+
if (!this.pendingP2PInlineAcks?.size || !this.inlineGenerationIsCurrent(generation))
|
|
2302
|
+
return;
|
|
2303
|
+
this.onP2PV2AckSucceeded(this.resolveActualAckSeq(result, requestedSeq), generation);
|
|
2304
|
+
}
|
|
2305
|
+
onP2PV2PullAckResult(result, requestedSeq, generation) {
|
|
2306
|
+
if (!this.pendingP2PInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
|
|
2307
|
+
|| !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
2308
|
+
return;
|
|
2309
|
+
let actualAckSeq = requestedSeq;
|
|
2310
|
+
const response = isJsonObject(result) ? result : null;
|
|
2311
|
+
if (response && Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')) {
|
|
2312
|
+
const raw = response.server_ack_seq;
|
|
2313
|
+
actualAckSeq = typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
|
|
2314
|
+
}
|
|
2315
|
+
this.onP2PV2AckSucceeded(actualAckSeq, generation);
|
|
2316
|
+
}
|
|
2317
|
+
async onV2PushNotification(data, expectedVersion, inlineGeneration) {
|
|
2318
|
+
const client = this.runtime.client;
|
|
2319
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2320
|
+
return;
|
|
2321
|
+
if (!client._v2Session)
|
|
2322
|
+
return;
|
|
2323
|
+
const pushData = isJsonObject(data) ? data : null;
|
|
2324
|
+
const rawPushSeq = pushData?.seq;
|
|
2325
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
2326
|
+
if (!pushData || !ns)
|
|
2327
|
+
return;
|
|
2328
|
+
const inlinePresent = Object.prototype.hasOwnProperty.call(pushData, 'inline_message');
|
|
2329
|
+
const pushSeq = this.realtimeHeadSeq(rawPushSeq, inlinePresent);
|
|
2330
|
+
if (pushSeq === null) {
|
|
2331
|
+
if (inlinePresent && rawPushSeq !== undefined && rawPushSeq !== null) {
|
|
2332
|
+
await this.recoverInvalidP2PRealtimeHead(ns, rawPushSeq);
|
|
2333
|
+
}
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
let inlineConsumed = false;
|
|
2337
|
+
const pendingInlineAck = this.pendingP2PInlineAcks?.get(ns);
|
|
2338
|
+
const inlineBlockedByPendingAck = Boolean(pendingInlineAck && pendingInlineAck.seq !== pushSeq);
|
|
2339
|
+
if (inlinePresent && !inlineBlockedByPendingAck && isJsonObject(pushData.inline_message))
|
|
2340
|
+
try {
|
|
2341
|
+
inlineConsumed = await this.tryConsumeInlineP2P(pushData, ns, pushSeq, expectedVersion, inlineGeneration);
|
|
2342
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2343
|
+
return;
|
|
2344
|
+
if (inlineConsumed) {
|
|
2345
|
+
client._clientLog.debug(`V2 P2P inline push consumed: seq=${pushSeq}`);
|
|
2346
|
+
this.retryPendingP2PInlineAck(ns, pushSeq, 'inline push');
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
catch (exc) {
|
|
2351
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2352
|
+
return;
|
|
2353
|
+
client._clientLog.warn(`V2 P2P inline push rejected, falling back to Tail: seq=${pushSeq} err=${formatDeliveryError(exc)}`);
|
|
2354
|
+
}
|
|
2355
|
+
const before = this.syncState(ns);
|
|
2356
|
+
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
2357
|
+
const hasGap = before.ack < before.tail - 1;
|
|
2358
|
+
if (pushSeq <= before.head && !hasGap && !inlinePresent) {
|
|
2359
|
+
client._clientLog.debug(`V2 P2P duplicate push ignored: seq=${pushSeq} A/T/H=${JSON.stringify(before)}`);
|
|
2360
|
+
this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (!this.beginRealtimeSync(ns)) {
|
|
2364
|
+
client._clientLog.debug(`V2 P2P push coalesced into in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
try {
|
|
2368
|
+
const syncResult = await this.runP2PRealtimeSync(ns, pushSeq, 50, inlinePresent && !inlineConsumed);
|
|
2369
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2370
|
+
return;
|
|
2371
|
+
const tailHead = this.realtimeTailHead(syncResult);
|
|
2372
|
+
const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
|
|
2373
|
+
if (client._sessionOptions?.background_sync !== false
|
|
2374
|
+
&& tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
|
|
2375
|
+
await this.runP2PForwardRecovery(ns, 50).catch((exc) => {
|
|
2376
|
+
client._clientLog.warn(`V2 P2P stale Tail Forward recovery failed: ${formatDeliveryError(exc)}`);
|
|
2377
|
+
});
|
|
2378
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
catch (exc) {
|
|
2383
|
+
client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
|
|
2384
|
+
}
|
|
2385
|
+
finally {
|
|
2386
|
+
if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
|
|
2387
|
+
this.endRealtimeSync(ns);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
1342
2390
|
}
|
|
1343
2391
|
enqueueOnlineUnreadHint(data) {
|
|
1344
2392
|
const client = this.runtime.client;
|
|
@@ -1359,12 +2407,11 @@ export class MessageDeliveryEngine {
|
|
|
1359
2407
|
if (client._onlineUnreadHintDrainActive)
|
|
1360
2408
|
return;
|
|
1361
2409
|
this.runtime.delivery.setOnlineUnreadHintDrainActive(true);
|
|
2410
|
+
const attemptedAckByGroup = new Map();
|
|
1362
2411
|
try {
|
|
1363
2412
|
while (client._onlineUnreadHintQueue.size > 0) {
|
|
1364
2413
|
if (client.state !== ConnectionState.READY)
|
|
1365
2414
|
return;
|
|
1366
|
-
if (client._sessionOptions?.background_sync === false)
|
|
1367
|
-
return;
|
|
1368
2415
|
const groupId = client._onlineUnreadHintQueue.keys().next().value;
|
|
1369
2416
|
if (!groupId)
|
|
1370
2417
|
return;
|
|
@@ -1372,6 +2419,11 @@ export class MessageDeliveryEngine {
|
|
|
1372
2419
|
client._onlineUnreadHintQueue.delete(groupId);
|
|
1373
2420
|
if (!payload)
|
|
1374
2421
|
continue;
|
|
2422
|
+
const currentAck = this.syncState(`group:${groupId}`).ack;
|
|
2423
|
+
const attemptedAck = attemptedAckByGroup.get(groupId);
|
|
2424
|
+
if (attemptedAck !== undefined && currentAck <= attemptedAck)
|
|
2425
|
+
continue;
|
|
2426
|
+
attemptedAckByGroup.set(groupId, currentAck);
|
|
1375
2427
|
await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
|
|
1376
2428
|
const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
|
|
1377
2429
|
if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
|
|
@@ -1386,62 +2438,110 @@ export class MessageDeliveryEngine {
|
|
|
1386
2438
|
this.runtime.delivery.setOnlineUnreadHintDrainActive(false);
|
|
1387
2439
|
}
|
|
1388
2440
|
}
|
|
1389
|
-
async onRawGroupV2MessageCreated(data) {
|
|
2441
|
+
async onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
|
|
1390
2442
|
const client = this.runtime.client;
|
|
2443
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2444
|
+
return;
|
|
1391
2445
|
if (!isJsonObject(data) || !client._v2Session) {
|
|
1392
2446
|
client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: is_object=${String(isJsonObject(data))}, has_v2_session=${String(!!client._v2Session)}`);
|
|
1393
2447
|
return;
|
|
1394
2448
|
}
|
|
1395
2449
|
const d = data;
|
|
1396
2450
|
client._logMessageDebug('server-push', '_raw.group.v2.message_created', 'group.message_created', d);
|
|
1397
|
-
const
|
|
1398
|
-
const
|
|
1399
|
-
const
|
|
1400
|
-
|
|
2451
|
+
const groupId = typeof d.group_id === 'string' ? d.group_id.trim() : '';
|
|
2452
|
+
const rawSeq = d.seq;
|
|
2453
|
+
const inlinePresent = Object.prototype.hasOwnProperty.call(d, 'inline_message');
|
|
2454
|
+
const seq = this.realtimeHeadSeq(rawSeq, inlinePresent);
|
|
2455
|
+
if (!groupId) {
|
|
1401
2456
|
client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: group=${groupId || '<empty>'}, seq=${String(d.seq ?? '')}`);
|
|
1402
2457
|
return;
|
|
1403
2458
|
}
|
|
2459
|
+
if (seq === null) {
|
|
2460
|
+
if (inlinePresent && rawSeq !== undefined && rawSeq !== null) {
|
|
2461
|
+
await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, rawSeq);
|
|
2462
|
+
}
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
1404
2465
|
const eventKind = String(d.kind ?? '').trim();
|
|
1405
|
-
if (eventKind === 'group.online_unread_hint'
|
|
1406
|
-
if (
|
|
1407
|
-
client.
|
|
2466
|
+
if (eventKind === 'group.online_unread_hint') {
|
|
2467
|
+
if (!d._online_hint_drained) {
|
|
2468
|
+
if (client._sessionOptions.background_sync === false)
|
|
2469
|
+
return;
|
|
2470
|
+
this.enqueueOnlineUnreadHint(d);
|
|
1408
2471
|
return;
|
|
1409
2472
|
}
|
|
1410
|
-
|
|
2473
|
+
// Hint 不是真实消息 Head:不消费 inline、不推进水位、也不直接 ACK。
|
|
2474
|
+
// 只在后台同步开启时,从已提交 Ack 强制做一次普通 Forward,绝不进入 Tail planner。
|
|
2475
|
+
if (client._sessionOptions.background_sync === false)
|
|
2476
|
+
return;
|
|
2477
|
+
try {
|
|
2478
|
+
await this.runGroupForwardRecovery(groupId, `group:${groupId}`, 50);
|
|
2479
|
+
}
|
|
2480
|
+
catch (exc) {
|
|
2481
|
+
client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2482
|
+
}
|
|
1411
2483
|
return;
|
|
1412
2484
|
}
|
|
1413
2485
|
const ns = `group:${groupId}`;
|
|
2486
|
+
let inlineConsumed = false;
|
|
2487
|
+
const pendingInlineAck = this.pendingGroupInlineAcks?.get(ns);
|
|
2488
|
+
const inlineBlockedByPendingAck = Boolean(pendingInlineAck && pendingInlineAck.seq !== seq);
|
|
2489
|
+
if (inlinePresent && !inlineBlockedByPendingAck && isJsonObject(d.inline_message)) {
|
|
2490
|
+
try {
|
|
2491
|
+
inlineConsumed = await this.tryConsumeInlineGroup(d, groupId, ns, seq, expectedVersion, inlineGeneration);
|
|
2492
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2493
|
+
return;
|
|
2494
|
+
if (inlineConsumed) {
|
|
2495
|
+
client._clientLog.debug(`V2 group inline push consumed: group=${groupId} seq=${seq}`);
|
|
2496
|
+
this.retryPendingGroupInlineAck(ns, seq, 'inline push');
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
catch (exc) {
|
|
2501
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2502
|
+
return;
|
|
2503
|
+
client._clientLog.warn(`V2 group inline push rejected, falling back to Tail: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
const before = this.syncState(ns);
|
|
1414
2507
|
client._seqTracker.updateMaxSeen(ns, seq);
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
2508
|
+
const hasGap = before.ack < before.tail - 1;
|
|
2509
|
+
if (seq <= before.head && !hasGap && !inlinePresent) {
|
|
2510
|
+
client._clientLog.debug(`V2 group duplicate push ignored: group=${groupId} seq=${seq} A/T/H=${JSON.stringify(before)}`);
|
|
2511
|
+
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
2512
|
+
if (pending?.seq === seq) {
|
|
2513
|
+
this.retryPendingGroupInlineAck(ns, seq, 'covered inline push');
|
|
2514
|
+
}
|
|
2515
|
+
else {
|
|
2516
|
+
this.fireGroupV2Ack(groupId, seq, 'covered push');
|
|
2517
|
+
}
|
|
1420
2518
|
return;
|
|
1421
2519
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
return
|
|
1429
|
-
|
|
1430
|
-
client.
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
if (
|
|
1437
|
-
return
|
|
1438
|
-
return pulled.length;
|
|
2520
|
+
if (!this.beginRealtimeSync(ns)) {
|
|
2521
|
+
client._clientLog.debug(`V2 group push coalesced into in-flight realtime sync: ns=${ns} seq=${seq}`);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const pullTask = this.runGroupRealtimeSync(groupId, ns, seq, 50, inlinePresent && !inlineConsumed).then(async (syncResult) => {
|
|
2525
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2526
|
+
return;
|
|
2527
|
+
const tailHead = this.realtimeTailHead(syncResult);
|
|
2528
|
+
const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
|
|
2529
|
+
if (client._sessionOptions?.background_sync !== false
|
|
2530
|
+
&& tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
|
|
2531
|
+
await this.runGroupForwardRecovery(groupId, ns, 50).catch((exc) => {
|
|
2532
|
+
client._clientLog.warn(`V2 group stale Tail Forward recovery failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2533
|
+
});
|
|
2534
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2535
|
+
return;
|
|
1439
2536
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
2537
|
+
}).catch((exc) => {
|
|
2538
|
+
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2539
|
+
return;
|
|
2540
|
+
client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2541
|
+
}).finally(() => {
|
|
2542
|
+
if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
|
|
2543
|
+
this.endRealtimeSync(ns);
|
|
1442
2544
|
}
|
|
1443
|
-
}, true).catch((exc) => {
|
|
1444
|
-
client._clientLog.warn(`V2 group push auto-pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
1445
2545
|
});
|
|
1446
2546
|
if (d._online_hint_drained) {
|
|
1447
2547
|
await pullTask;
|
|
@@ -1452,6 +2552,15 @@ export class MessageDeliveryEngine {
|
|
|
1452
2552
|
if (!client._aid)
|
|
1453
2553
|
return;
|
|
1454
2554
|
try {
|
|
2555
|
+
const loadSync = client._tokenStore.loadAllSyncStates;
|
|
2556
|
+
if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
|
|
2557
|
+
let state = loadSync.call(client._tokenStore, client._aid, client._deviceId, client._slotId);
|
|
2558
|
+
if (state && Object.keys(state).length > 0) {
|
|
2559
|
+
state = this.migrateSeqStateGroupIds(state);
|
|
2560
|
+
client._seqTracker.restoreSyncState(state);
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
1455
2564
|
const loadAll = client._tokenStore.loadAllSeqs;
|
|
1456
2565
|
if (typeof loadAll === 'function') {
|
|
1457
2566
|
let state = loadAll.call(client._tokenStore, client._aid, client._deviceId, client._slotId);
|
|
@@ -1467,7 +2576,10 @@ export class MessageDeliveryEngine {
|
|
|
1467
2576
|
if (instanceState && typeof instanceState.seq_tracker_state === 'object') {
|
|
1468
2577
|
let state = instanceState.seq_tracker_state;
|
|
1469
2578
|
state = this.migrateSeqStateGroupIds(state);
|
|
1470
|
-
client._seqTracker.
|
|
2579
|
+
if (typeof client._seqTracker.restoreSyncState === 'function')
|
|
2580
|
+
client._seqTracker.restoreSyncState(state);
|
|
2581
|
+
else
|
|
2582
|
+
client._seqTracker.restoreState(state);
|
|
1471
2583
|
}
|
|
1472
2584
|
}
|
|
1473
2585
|
}
|
|
@@ -1484,53 +2596,7 @@ export class MessageDeliveryEngine {
|
|
|
1484
2596
|
}
|
|
1485
2597
|
}
|
|
1486
2598
|
migrateSeqStateGroupIds(state) {
|
|
1487
|
-
|
|
1488
|
-
if (!state || Object.keys(state).length === 0)
|
|
1489
|
-
return state;
|
|
1490
|
-
const renameMap = {};
|
|
1491
|
-
for (const ns of Object.keys(state)) {
|
|
1492
|
-
for (const prefix of ['group_event:', 'group_msg:']) {
|
|
1493
|
-
if (ns.startsWith(prefix)) {
|
|
1494
|
-
const oldGid = ns.slice(prefix.length);
|
|
1495
|
-
const newGid = normalizeGroupId(oldGid);
|
|
1496
|
-
if (newGid && newGid !== oldGid) {
|
|
1497
|
-
renameMap[ns] = `${prefix}${newGid}`;
|
|
1498
|
-
}
|
|
1499
|
-
break;
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
if (Object.keys(renameMap).length === 0)
|
|
1504
|
-
return state;
|
|
1505
|
-
const newState = { ...state };
|
|
1506
|
-
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
1507
|
-
const oldVal = Number(newState[oldNs] ?? 0);
|
|
1508
|
-
const curVal = Number(newState[newNs] ?? 0);
|
|
1509
|
-
delete newState[oldNs];
|
|
1510
|
-
newState[newNs] = Math.max(oldVal, curVal);
|
|
1511
|
-
}
|
|
1512
|
-
client._clientLog.info(`SeqTracker group_id migration: ${Object.keys(renameMap).length} namespaces rewritten`);
|
|
1513
|
-
const saver = client._tokenStore.saveSeq;
|
|
1514
|
-
const deleter = client._tokenStore.deleteSeq;
|
|
1515
|
-
if (typeof saver === 'function' && client._aid) {
|
|
1516
|
-
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
1517
|
-
if (typeof deleter === 'function') {
|
|
1518
|
-
try {
|
|
1519
|
-
deleter.call(client._tokenStore, client._aid, client._deviceId, client._slotId, oldNs);
|
|
1520
|
-
}
|
|
1521
|
-
catch (e) {
|
|
1522
|
-
client._clientLog.debug(`delete old seq ns failed: ns=${oldNs} err=${formatDeliveryError(e)}`);
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
try {
|
|
1526
|
-
saver.call(client._tokenStore, client._aid, client._deviceId, client._slotId, newNs, newState[newNs]);
|
|
1527
|
-
}
|
|
1528
|
-
catch (e) {
|
|
1529
|
-
client._clientLog.debug(`write new seq ns failed: ns=${newNs} err=${formatDeliveryError(e)}`);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
return newState;
|
|
2599
|
+
return state;
|
|
1534
2600
|
}
|
|
1535
2601
|
saveSeqTrackerState() {
|
|
1536
2602
|
const client = this.runtime.client;
|
|
@@ -1539,7 +2605,9 @@ export class MessageDeliveryEngine {
|
|
|
1539
2605
|
this.flushSeqTrackerPending();
|
|
1540
2606
|
return;
|
|
1541
2607
|
}
|
|
1542
|
-
const state = client._seqTracker.
|
|
2608
|
+
const state = typeof client._seqTracker.exportSyncState === 'function'
|
|
2609
|
+
? client._seqTracker.exportSyncState()
|
|
2610
|
+
: Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
|
|
1543
2611
|
const pending = this.seqTrackerPending();
|
|
1544
2612
|
if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
|
|
1545
2613
|
const liveNamespaces = new Set(Object.keys(state));
|
|
@@ -1567,9 +2635,27 @@ export class MessageDeliveryEngine {
|
|
|
1567
2635
|
if (forceSeq !== undefined) {
|
|
1568
2636
|
client._seqTracker.forceContiguousSeq(ns, seq);
|
|
1569
2637
|
}
|
|
1570
|
-
|
|
2638
|
+
const state = typeof client._seqTracker.getSyncState === 'function'
|
|
2639
|
+
? client._seqTracker.getSyncState(ns)
|
|
2640
|
+
: { ack: seq, tail: seq, head: seq };
|
|
2641
|
+
this.mergeSeqTrackerPending(context, { [ns]: state });
|
|
1571
2642
|
this.scheduleSeqTrackerFlush();
|
|
1572
2643
|
}
|
|
2644
|
+
commitSeqTrackerState(ns) {
|
|
2645
|
+
const client = this.runtime.client;
|
|
2646
|
+
const context = this.currentSeqTrackerContext();
|
|
2647
|
+
if (!context || !ns)
|
|
2648
|
+
return;
|
|
2649
|
+
const state = typeof client._seqTracker.getSyncState === 'function'
|
|
2650
|
+
? client._seqTracker.getSyncState(ns)
|
|
2651
|
+
: { ack: client._seqTracker.getContiguousSeq(ns), tail: client._seqTracker.getContiguousSeq(ns), head: client._seqTracker.getContiguousSeq(ns) };
|
|
2652
|
+
if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
|
|
2653
|
+
return;
|
|
2654
|
+
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
2655
|
+
this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
|
|
2656
|
+
this.dropSeqTrackerPending(ns);
|
|
2657
|
+
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
2658
|
+
}
|
|
1573
2659
|
flushSeqTrackerPending() {
|
|
1574
2660
|
const client = this.runtime.client;
|
|
1575
2661
|
this.cancelSeqTrackerFlushTimer();
|
|
@@ -1615,10 +2701,12 @@ export class MessageDeliveryEngine {
|
|
|
1615
2701
|
this.flushSeqTrackerPending();
|
|
1616
2702
|
}
|
|
1617
2703
|
client._seqTrackerPendingPersistContext = context;
|
|
1618
|
-
for (const [ns,
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
2704
|
+
for (const [ns, raw] of Object.entries(state)) {
|
|
2705
|
+
const ack = Number(raw.ack);
|
|
2706
|
+
const tail = Number(raw.tail);
|
|
2707
|
+
const head = Number(raw.head);
|
|
2708
|
+
if (ns && [ack, tail, head].every(Number.isFinite) && Math.max(ack, tail, head) > 0) {
|
|
2709
|
+
pending.set(ns, { ack, tail, head });
|
|
1622
2710
|
}
|
|
1623
2711
|
}
|
|
1624
2712
|
}
|
|
@@ -1641,15 +2729,20 @@ export class MessageDeliveryEngine {
|
|
|
1641
2729
|
client._seqTrackerFlushTimer = null;
|
|
1642
2730
|
}
|
|
1643
2731
|
}
|
|
1644
|
-
writeSeqTrackerState(aid, deviceId, slotId, state) {
|
|
2732
|
+
writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
1645
2733
|
const client = this.runtime.client;
|
|
1646
2734
|
if (!aid || Object.keys(state).length === 0)
|
|
1647
2735
|
return;
|
|
1648
2736
|
try {
|
|
2737
|
+
const saveSync = client._tokenStore.saveSyncStates;
|
|
2738
|
+
if (typeof saveSync === 'function') {
|
|
2739
|
+
saveSync.call(client._tokenStore, aid, deviceId, slotId, state);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
1649
2742
|
const saveFn = client._tokenStore.saveSeq;
|
|
1650
2743
|
if (typeof saveFn === 'function') {
|
|
1651
|
-
for (const [ns,
|
|
1652
|
-
saveFn.call(client._tokenStore, aid, deviceId, slotId, ns,
|
|
2744
|
+
for (const [ns, triplet] of Object.entries(state)) {
|
|
2745
|
+
saveFn.call(client._tokenStore, aid, deviceId, slotId, ns, triplet.ack);
|
|
1653
2746
|
}
|
|
1654
2747
|
return;
|
|
1655
2748
|
}
|
|
@@ -1674,6 +2767,8 @@ export class MessageDeliveryEngine {
|
|
|
1674
2767
|
slot_id: slotId,
|
|
1675
2768
|
error: String(error),
|
|
1676
2769
|
}).catch(() => { });
|
|
2770
|
+
if (throwOnError)
|
|
2771
|
+
throw exc;
|
|
1677
2772
|
}
|
|
1678
2773
|
}
|
|
1679
2774
|
persistRepairedSeq(ns) {
|
|
@@ -1721,10 +2816,21 @@ export class MessageDeliveryEngine {
|
|
|
1721
2816
|
if (next < 0)
|
|
1722
2817
|
next = 0;
|
|
1723
2818
|
if (ns) {
|
|
1724
|
-
const
|
|
1725
|
-
if (
|
|
1726
|
-
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} >
|
|
1727
|
-
next =
|
|
2819
|
+
const committedAck = client._seqTracker.getContiguousSeq(ns);
|
|
2820
|
+
if (next > committedAck) {
|
|
2821
|
+
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
|
|
2822
|
+
next = committedAck;
|
|
2823
|
+
}
|
|
2824
|
+
if (method === 'message.v2.ack' || method === 'message.ack') {
|
|
2825
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
2826
|
+
if (pending && !pending.committed && next >= pending.seq)
|
|
2827
|
+
next = pending.seq - 1;
|
|
2828
|
+
}
|
|
2829
|
+
else if (method === 'group.v2.ack' || method === 'group.ack_messages') {
|
|
2830
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
2831
|
+
const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
|
|
2832
|
+
if (pending && !pending.committed && next >= pending.seq)
|
|
2833
|
+
next = pending.seq - 1;
|
|
1728
2834
|
}
|
|
1729
2835
|
}
|
|
1730
2836
|
return next;
|
|
@@ -1747,7 +2853,7 @@ export class MessageDeliveryEngine {
|
|
|
1747
2853
|
return clampField('seq', ns);
|
|
1748
2854
|
}
|
|
1749
2855
|
if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
|
|
1750
|
-
const groupId =
|
|
2856
|
+
const groupId = String(params.group_id ?? params.group_aid ?? '').trim();
|
|
1751
2857
|
const ns = groupId ? `group:${groupId}` : '';
|
|
1752
2858
|
if (method === 'group.v2.ack')
|
|
1753
2859
|
return clampField('up_to_seq', ns);
|
|
@@ -1818,15 +2924,7 @@ export class MessageDeliveryEngine {
|
|
|
1818
2924
|
return false;
|
|
1819
2925
|
}
|
|
1820
2926
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
1821
|
-
if (seqNum
|
|
1822
|
-
client._clientLog.debug(`publish ordered stale covered: event=${event}, ns=${ns}, seq=${seqNum}, contiguous=${contig}`);
|
|
1823
|
-
const queue = client._pendingOrderedMsgs.get(ns);
|
|
1824
|
-
queue?.delete(seqNum);
|
|
1825
|
-
if (queue && queue.size === 0)
|
|
1826
|
-
client._pendingOrderedMsgs.delete(ns);
|
|
1827
|
-
return false;
|
|
1828
|
-
}
|
|
1829
|
-
if (seqNum !== contig + 1) {
|
|
2927
|
+
if (seqNum > contig && seqNum !== contig + 1) {
|
|
1830
2928
|
client._clientLog.debug(`publish ordered enqueue(gap): event=${event}, ns=${ns}, seq=${seqNum}, contiguous=${contig}`);
|
|
1831
2929
|
this.enqueueOrderedMessage(ns, event, seqNum, payload);
|
|
1832
2930
|
return false;
|
|
@@ -1898,16 +2996,16 @@ export class MessageDeliveryEngine {
|
|
|
1898
2996
|
if (event === 'message.recalled') {
|
|
1899
2997
|
const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload);
|
|
1900
2998
|
this.markPublishedSeq(ns, seqNum);
|
|
1901
|
-
client._markPulledSeqDelivered(ns, seqNum);
|
|
1902
2999
|
client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
3000
|
+
await this.drainOrderedMessages(ns, undefined, true);
|
|
1903
3001
|
return recallPublished;
|
|
1904
3002
|
}
|
|
1905
3003
|
const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
|
|
1906
3004
|
if (isPromiseLike(published))
|
|
1907
3005
|
await published;
|
|
1908
3006
|
this.markPublishedSeq(ns, seqNum);
|
|
1909
|
-
client._markPulledSeqDelivered(ns, seqNum);
|
|
1910
3007
|
client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
3008
|
+
await this.drainOrderedMessages(ns, undefined, true);
|
|
1911
3009
|
return true;
|
|
1912
3010
|
}
|
|
1913
3011
|
}
|