@agentunion/fastaun-browser 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/_packed_docs/CHANGELOG.md +45 -0
- package/_packed_docs/INDEX.md +46 -22
- package/_packed_docs/KITE_DOCS_GUIDE.md +16 -12
- package/_packed_docs/design/AUNClient/346/213/206/345/210/206/351/207/215/346/236/204/346/211/247/350/241/214/346/226/271/346/241/210.md +859 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -3
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +5 -5
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +4 -2
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +1 -1
- package/dist/agent-md.d.ts +1 -1
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +33 -8
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +8 -1
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +109 -16
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +1 -0
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +10 -4
- package/dist/aid.js.map +1 -1
- package/dist/bundle.js +9924 -8897
- package/dist/cert-utils.d.ts +5 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +52 -8
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +44 -0
- package/dist/client/delivery.d.ts.map +1 -0
- package/dist/client/delivery.js +1033 -0
- package/dist/client/delivery.js.map +1 -0
- package/dist/client/group-state.d.ts +32 -0
- package/dist/client/group-state.d.ts.map +1 -0
- package/dist/client/group-state.js +909 -0
- package/dist/client/group-state.js.map +1 -0
- package/dist/client/identity.d.ts +8 -0
- package/dist/client/identity.d.ts.map +1 -0
- package/dist/client/identity.js +34 -0
- package/dist/client/identity.js.map +1 -0
- package/dist/client/lifecycle.d.ts +10 -0
- package/dist/client/lifecycle.d.ts.map +1 -0
- package/dist/client/lifecycle.js +222 -0
- package/dist/client/lifecycle.js.map +1 -0
- package/dist/client/peers.d.ts +11 -0
- package/dist/client/peers.d.ts.map +1 -0
- package/dist/client/peers.js +42 -0
- package/dist/client/peers.js.map +1 -0
- package/dist/client/rpc-pipeline.d.ts +36 -0
- package/dist/client/rpc-pipeline.d.ts.map +1 -0
- package/dist/client/rpc-pipeline.js +446 -0
- package/dist/client/rpc-pipeline.js.map +1 -0
- package/dist/client/runtime.d.ts +6 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/client/runtime.js +7 -0
- package/dist/client/runtime.js.map +1 -0
- package/dist/client/v2-e2ee.d.ts +104 -0
- package/dist/client/v2-e2ee.d.ts.map +1 -0
- package/dist/client/v2-e2ee.js +1586 -0
- package/dist/client/v2-e2ee.js.map +1 -0
- package/dist/client.d.ts +26 -49
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +303 -3733
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -3
- package/dist/config.js.map +1 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +69 -10
- package/dist/discovery.js.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +23 -22
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/v2/crypto/dh-path.js +2 -2
- package/dist/v2/crypto/index.js +7 -7
- package/dist/v2/e2ee/decrypt.js +8 -8
- package/dist/v2/e2ee/encrypt-group.js +9 -9
- package/dist/v2/e2ee/encrypt-p2p.js +9 -9
- package/dist/v2/e2ee/index.js +5 -5
- package/dist/v2/e2ee/metadata-auth.js +1 -1
- package/dist/v2/session/index.js +2 -2
- package/dist/v2/session/session.js +1 -1
- package/dist/v2/state/commitment.js +1 -1
- package/dist/v2/state/index.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
import { slotIsolationKey } from '../config.js';
|
|
2
|
+
import { normalizeGroupId } from '../group-id.js';
|
|
3
|
+
import { isJsonObject } from '../types.js';
|
|
4
|
+
const PUSHED_SEQS_LIMIT = 50_000;
|
|
5
|
+
const PENDING_ORDERED_LIMIT = 50_000;
|
|
6
|
+
function formatDeliveryError(error) {
|
|
7
|
+
return error instanceof Error ? error : String(error);
|
|
8
|
+
}
|
|
9
|
+
export class MessageDeliveryEngine {
|
|
10
|
+
runtime;
|
|
11
|
+
constructor(runtime) {
|
|
12
|
+
this.runtime = runtime;
|
|
13
|
+
}
|
|
14
|
+
prunePushedSeqs(ns) {
|
|
15
|
+
const client = this.runtime.client;
|
|
16
|
+
const pushed = client._pushedSeqs.get(ns);
|
|
17
|
+
if (!pushed)
|
|
18
|
+
return;
|
|
19
|
+
if (pushed.size > PUSHED_SEQS_LIMIT) {
|
|
20
|
+
const keep = [...pushed].sort((a, b) => a - b).slice(-PUSHED_SEQS_LIMIT);
|
|
21
|
+
client._pushedSeqs.set(ns, new Set(keep));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
markPublishedSeq(ns, seq) {
|
|
25
|
+
const client = this.runtime.client;
|
|
26
|
+
let pushed = client._pushedSeqs.get(ns);
|
|
27
|
+
if (!pushed) {
|
|
28
|
+
pushed = new Set();
|
|
29
|
+
client._pushedSeqs.set(ns, pushed);
|
|
30
|
+
}
|
|
31
|
+
pushed.add(seq);
|
|
32
|
+
if (pushed.size > PUSHED_SEQS_LIMIT) {
|
|
33
|
+
const keep = [...pushed].sort((a, b) => a - b).slice(-PUSHED_SEQS_LIMIT);
|
|
34
|
+
client._pushedSeqs.set(ns, new Set(keep));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
enqueueOrderedMessage(ns, event, seq, payload) {
|
|
38
|
+
const client = this.runtime.client;
|
|
39
|
+
let queue = client._pendingOrderedMsgs.get(ns);
|
|
40
|
+
if (!queue) {
|
|
41
|
+
queue = new Map();
|
|
42
|
+
client._pendingOrderedMsgs.set(ns, queue);
|
|
43
|
+
}
|
|
44
|
+
queue.set(seq, { event, payload });
|
|
45
|
+
if (queue.size > PENDING_ORDERED_LIMIT) {
|
|
46
|
+
const drop = [...queue.keys()].sort((a, b) => a - b).slice(0, queue.size - PENDING_ORDERED_LIMIT);
|
|
47
|
+
for (const oldSeq of drop)
|
|
48
|
+
queue.delete(oldSeq);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
isInstanceScopedMessageEvent(event) {
|
|
52
|
+
return event === 'message.received'
|
|
53
|
+
|| event === 'message.recalled'
|
|
54
|
+
|| event === 'message.undecryptable'
|
|
55
|
+
|| event === 'group.message_created'
|
|
56
|
+
|| event === 'group.message_undecryptable';
|
|
57
|
+
}
|
|
58
|
+
attachCurrentInstanceContext(payload) {
|
|
59
|
+
if (!isJsonObject(payload))
|
|
60
|
+
return payload;
|
|
61
|
+
const client = this.runtime.client;
|
|
62
|
+
const result = { ...payload };
|
|
63
|
+
if (!('device_id' in result)) {
|
|
64
|
+
result.device_id = client._deviceId;
|
|
65
|
+
}
|
|
66
|
+
if (!('slot_id' in result)) {
|
|
67
|
+
result.slot_id = client._slotId;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
normalizePublishedMessagePayload(event, payload) {
|
|
72
|
+
if (!this.isInstanceScopedMessageEvent(event))
|
|
73
|
+
return payload;
|
|
74
|
+
return this.stripInternalSenderDeviceFields(this.attachCurrentInstanceContext(payload));
|
|
75
|
+
}
|
|
76
|
+
stripInternalSenderDeviceFields(payload) {
|
|
77
|
+
if (!isJsonObject(payload))
|
|
78
|
+
return payload;
|
|
79
|
+
const result = { ...payload };
|
|
80
|
+
delete result.sender_device_id;
|
|
81
|
+
delete result._sender_device_id;
|
|
82
|
+
delete result.from_device_id;
|
|
83
|
+
delete result.from_device;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
recallEventFromMessage(message) {
|
|
87
|
+
if (!isJsonObject(message))
|
|
88
|
+
return null;
|
|
89
|
+
const msg = message;
|
|
90
|
+
const rawPayload = msg.payload;
|
|
91
|
+
const payload = isJsonObject(rawPayload) ? rawPayload : {};
|
|
92
|
+
const msgType = String(msg.type ?? '').trim();
|
|
93
|
+
const payloadType = String(payload.type ?? payload.kind ?? '').trim();
|
|
94
|
+
if (msgType !== 'message.recalled' && payloadType !== 'message.recalled')
|
|
95
|
+
return null;
|
|
96
|
+
const event = { ...payload };
|
|
97
|
+
const rawIds = event.message_ids;
|
|
98
|
+
let messageIds = Array.isArray(rawIds)
|
|
99
|
+
? rawIds.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
100
|
+
: [];
|
|
101
|
+
if (messageIds.length === 0) {
|
|
102
|
+
for (const key of ['recalled_message_id', 'target_message_id', 'original_message_id']) {
|
|
103
|
+
const value = String(event[key] ?? '').trim();
|
|
104
|
+
if (value) {
|
|
105
|
+
messageIds = [value];
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
event.type = 'message.recalled';
|
|
111
|
+
event.kind = 'message.recalled';
|
|
112
|
+
event.message_ids = messageIds;
|
|
113
|
+
if (!('from' in event))
|
|
114
|
+
event.from = msg.from ?? msg.from_aid ?? '';
|
|
115
|
+
if (!('to' in event))
|
|
116
|
+
event.to = msg.to ?? msg.to_aid ?? '';
|
|
117
|
+
if (!('timestamp' in event))
|
|
118
|
+
event.timestamp = msg.timestamp ?? msg.t_server ?? event.recalled_at ?? 0;
|
|
119
|
+
if ('seq' in msg && !('seq' in event))
|
|
120
|
+
event.seq = msg.seq;
|
|
121
|
+
if ('message_id' in msg && !('tombstone_message_id' in event))
|
|
122
|
+
event.tombstone_message_id = msg.message_id;
|
|
123
|
+
if ('device_id' in msg && !('device_id' in event))
|
|
124
|
+
event.device_id = msg.device_id;
|
|
125
|
+
if ('slot_id' in msg && !('slot_id' in event))
|
|
126
|
+
event.slot_id = msg.slot_id;
|
|
127
|
+
return event;
|
|
128
|
+
}
|
|
129
|
+
p2pAppEventForMessage(message) {
|
|
130
|
+
const recall = this.recallEventFromMessage(message);
|
|
131
|
+
if (recall)
|
|
132
|
+
return { event: 'message.recalled', payload: recall };
|
|
133
|
+
return { event: 'message.received', payload: message };
|
|
134
|
+
}
|
|
135
|
+
async publishAppEvent(event, payload) {
|
|
136
|
+
const client = this.runtime.client;
|
|
137
|
+
if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
|
|
138
|
+
client._maybeAppendEchoTraceReceive(payload);
|
|
139
|
+
}
|
|
140
|
+
if (isJsonObject(payload)) {
|
|
141
|
+
try {
|
|
142
|
+
const snapshot = client._agentMdManager.eventSnapshot();
|
|
143
|
+
const localEtag = snapshot?.local_etag || '';
|
|
144
|
+
const remoteEtag = snapshot?.remote_etag || '';
|
|
145
|
+
if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
|
|
146
|
+
payload._agent_md = {
|
|
147
|
+
local_etag: localEtag,
|
|
148
|
+
remote_etag: remoteEtag,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (exc) {
|
|
153
|
+
client._clientLog.debug(`agent_md etag inject skipped: ${String(exc)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await client._dispatcher.publish(event, this.normalizePublishedMessagePayload(event, payload));
|
|
157
|
+
}
|
|
158
|
+
messageTargetsCurrentInstance(message) {
|
|
159
|
+
if (!isJsonObject(message))
|
|
160
|
+
return true;
|
|
161
|
+
const client = this.runtime.client;
|
|
162
|
+
if ('device_id' in message) {
|
|
163
|
+
const targetDeviceId = String(message.device_id ?? '').trim();
|
|
164
|
+
if (targetDeviceId !== client._deviceId) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if ('slot_id' in message) {
|
|
169
|
+
const targetSlotId = String(message.slot_id ?? '').trim();
|
|
170
|
+
if (slotIsolationKey(targetSlotId) !== slotIsolationKey(client._slotId)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
onRawMessageReceived(data) {
|
|
177
|
+
const client = this.runtime.client;
|
|
178
|
+
client._clientLog.debug(`_onRawMessageReceived enter: from=${data?.from ?? '-'} mid=${data?.message_id ?? '-'} seq=${data?.seq ?? '-'}`);
|
|
179
|
+
client._safeAsync(this.processAndPublishMessage(data));
|
|
180
|
+
client._clientLog.debug('_onRawMessageReceived exit: elapsed=0ms (dispatched async)');
|
|
181
|
+
}
|
|
182
|
+
async processAndPublishMessage(data) {
|
|
183
|
+
const client = this.runtime.client;
|
|
184
|
+
try {
|
|
185
|
+
if (!isJsonObject(data)) {
|
|
186
|
+
await client._publishAppEvent('message.received', data);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const msg = { ...data };
|
|
190
|
+
if (!this.messageTargetsCurrentInstance(msg)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const seq = msg.seq;
|
|
194
|
+
const encryptedPush = client._isEncryptedPushMessage(msg);
|
|
195
|
+
if (seq !== undefined && seq !== null && client._aid) {
|
|
196
|
+
const ns = `p2p:${client._aid}`;
|
|
197
|
+
if (seq > 0)
|
|
198
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
199
|
+
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
200
|
+
const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
|
|
201
|
+
const published = encryptedPush
|
|
202
|
+
? await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
|
|
203
|
+
: await this.publishOrderedMessage('message.received', ns, seq, msg);
|
|
204
|
+
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
205
|
+
const needPull = seqNeedsPull && !published;
|
|
206
|
+
if (needPull) {
|
|
207
|
+
client._safeAsync(this.fillP2pGap());
|
|
208
|
+
}
|
|
209
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
210
|
+
if (contig > 0) {
|
|
211
|
+
const ackSeq = this.clampAckSeq('message.ack', 'seq', ns, contig);
|
|
212
|
+
client._transport.call('message.ack', {
|
|
213
|
+
seq: ackSeq,
|
|
214
|
+
device_id: client._deviceId,
|
|
215
|
+
slot_id: client._slotId,
|
|
216
|
+
}).catch((e) => { client._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
|
|
217
|
+
}
|
|
218
|
+
if (contigAfter !== contigBefore)
|
|
219
|
+
this.saveSeqTrackerState();
|
|
220
|
+
if (encryptedPush)
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (encryptedPush) {
|
|
225
|
+
await client._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await client._publishAppEvent('message.received', msg);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (exc) {
|
|
232
|
+
client._clientLog.warn(`P2P push processing failed:${String(exc)}`);
|
|
233
|
+
if (isJsonObject(data)) {
|
|
234
|
+
const src = data;
|
|
235
|
+
const safeEvent = {
|
|
236
|
+
message_id: (src.message_id ?? null),
|
|
237
|
+
from: (src.from ?? null),
|
|
238
|
+
to: (src.to ?? null),
|
|
239
|
+
seq: (src.seq ?? null),
|
|
240
|
+
timestamp: (src.timestamp ?? null),
|
|
241
|
+
_decrypt_error: String(exc),
|
|
242
|
+
};
|
|
243
|
+
client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
|
|
244
|
+
await client._publishAppEvent('message.undecryptable', safeEvent);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
onRawGroupMessageCreated(data) {
|
|
249
|
+
const client = this.runtime.client;
|
|
250
|
+
client._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
|
|
251
|
+
client._safeAsync(this.processAndPublishGroupMessage(data));
|
|
252
|
+
client._clientLog.debug('_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)');
|
|
253
|
+
}
|
|
254
|
+
async processAndPublishGroupMessage(data) {
|
|
255
|
+
const client = this.runtime.client;
|
|
256
|
+
try {
|
|
257
|
+
if (!isJsonObject(data)) {
|
|
258
|
+
await client._publishAppEvent('group.message_created', data);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const msg = { ...data };
|
|
262
|
+
const groupId = (msg.group_id ?? '');
|
|
263
|
+
const seq = msg.seq;
|
|
264
|
+
const payload = msg.payload;
|
|
265
|
+
if (groupId) {
|
|
266
|
+
client._groupSynced.add(groupId);
|
|
267
|
+
}
|
|
268
|
+
if (payload === undefined || payload === null
|
|
269
|
+
|| (typeof payload === 'object' && Object.keys(payload).length === 0)) {
|
|
270
|
+
await this.autoPullGroupMessages(msg);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const encryptedPush = client._isEncryptedPushMessage(msg);
|
|
274
|
+
if (groupId && seq !== undefined && seq !== null) {
|
|
275
|
+
const ns = `group:${groupId}`;
|
|
276
|
+
if (seq > 0)
|
|
277
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
278
|
+
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
279
|
+
const seqNeedsPull = client._seqTracker.onMessageSeq(ns, seq);
|
|
280
|
+
const published = encryptedPush
|
|
281
|
+
? await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
|
|
282
|
+
: await this.publishOrderedMessage('group.message_created', ns, seq, msg);
|
|
283
|
+
const contigAfter = client._seqTracker.getContiguousSeq(ns);
|
|
284
|
+
const needPull = seqNeedsPull && !published;
|
|
285
|
+
if (needPull) {
|
|
286
|
+
client._safeAsync(this.fillGroupGap(groupId));
|
|
287
|
+
}
|
|
288
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
289
|
+
if (contig > 0) {
|
|
290
|
+
const ackSeq = this.clampAckSeq('group.ack_messages', 'msg_seq', ns, contig);
|
|
291
|
+
client._transport.call('group.ack_messages', {
|
|
292
|
+
group_id: groupId,
|
|
293
|
+
msg_seq: ackSeq,
|
|
294
|
+
device_id: client._deviceId,
|
|
295
|
+
slot_id: client._slotId,
|
|
296
|
+
}).catch((e) => { client._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
|
|
297
|
+
}
|
|
298
|
+
if (contigAfter !== contigBefore)
|
|
299
|
+
this.saveSeqTrackerState();
|
|
300
|
+
if (encryptedPush)
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
if (encryptedPush) {
|
|
305
|
+
await client._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
await client._publishAppEvent('group.message_created', msg);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (exc) {
|
|
312
|
+
client._clientLog.warn(`group push processing failed:${String(exc)}`);
|
|
313
|
+
if (isJsonObject(data)) {
|
|
314
|
+
const src = data;
|
|
315
|
+
const safeEvent = {
|
|
316
|
+
message_id: (src.message_id ?? null),
|
|
317
|
+
group_id: (src.group_id ?? null),
|
|
318
|
+
from: (src.from ?? null),
|
|
319
|
+
seq: (src.seq ?? null),
|
|
320
|
+
timestamp: (src.timestamp ?? null),
|
|
321
|
+
_decrypt_error: String(exc),
|
|
322
|
+
};
|
|
323
|
+
client._attachV2EnvelopeMetadataFromSource(safeEvent, data);
|
|
324
|
+
await client._publishAppEvent('group.message_undecryptable', safeEvent);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async autoPullGroupMessages(notification) {
|
|
329
|
+
const client = this.runtime.client;
|
|
330
|
+
const groupId = (notification.group_id ?? '');
|
|
331
|
+
if (!groupId) {
|
|
332
|
+
await client._publishAppEvent('group.message_created', notification);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const ns = `group:${groupId}`;
|
|
336
|
+
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
337
|
+
try {
|
|
338
|
+
if (client._v2Session) {
|
|
339
|
+
await client._pullGroupV2Internal({ group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const result = await client.call('group.pull', {
|
|
343
|
+
group_id: groupId,
|
|
344
|
+
after_message_seq: afterSeq,
|
|
345
|
+
device_id: client._deviceId,
|
|
346
|
+
limit: 50,
|
|
347
|
+
});
|
|
348
|
+
if (isJsonObject(result)) {
|
|
349
|
+
const messages = result.messages;
|
|
350
|
+
if (Array.isArray(messages)) {
|
|
351
|
+
const pushed = client._pushedSeqs.get(ns);
|
|
352
|
+
for (const msg of messages) {
|
|
353
|
+
if (isJsonObject(msg)) {
|
|
354
|
+
const s = msg.seq;
|
|
355
|
+
if (pushed && s !== undefined && s !== null && pushed.has(s)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (s !== undefined && s !== null) {
|
|
359
|
+
await client._publishPulledMessage('group.message_created', ns, s, msg);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
await client._publishAppEvent('group.message_created', msg);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.prunePushedSeqs(ns);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (exc) {
|
|
372
|
+
client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
|
|
373
|
+
}
|
|
374
|
+
await client._publishAppEvent('group.message_created', notification);
|
|
375
|
+
}
|
|
376
|
+
async fillP2pGap() {
|
|
377
|
+
const client = this.runtime.client;
|
|
378
|
+
if (client._state !== 'connected' || client._closing)
|
|
379
|
+
return;
|
|
380
|
+
if (!client._aid)
|
|
381
|
+
return;
|
|
382
|
+
const ns = `p2p:${client._aid}`;
|
|
383
|
+
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
384
|
+
const dedupKey = `p2p_pull:${ns}`;
|
|
385
|
+
if (client._gapFillDone.has(dedupKey))
|
|
386
|
+
return;
|
|
387
|
+
client._gapFillDone.add(dedupKey);
|
|
388
|
+
client._gapFillActive = true;
|
|
389
|
+
let filled = 0;
|
|
390
|
+
try {
|
|
391
|
+
const messages = await client._pullV2(afterSeq, 50);
|
|
392
|
+
filled = messages.length;
|
|
393
|
+
this.prunePushedSeqs(ns);
|
|
394
|
+
}
|
|
395
|
+
catch (exc) {
|
|
396
|
+
client._clientLog.warn(`P2P message gap-fill failed:${String(formatDeliveryError(exc))}`);
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
client._gapFillDone.delete(dedupKey);
|
|
400
|
+
client._gapFillActive = false;
|
|
401
|
+
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
402
|
+
client._safeAsync(this.fillP2pGap());
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async fillGroupGap(groupId) {
|
|
407
|
+
const client = this.runtime.client;
|
|
408
|
+
if (client._state !== 'connected' || client._closing)
|
|
409
|
+
return;
|
|
410
|
+
groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
411
|
+
if (!groupId)
|
|
412
|
+
return;
|
|
413
|
+
const ns = `group:${groupId}`;
|
|
414
|
+
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
415
|
+
const dedupKey = `group_pull:${ns}`;
|
|
416
|
+
if (client._gapFillDone.has(dedupKey))
|
|
417
|
+
return;
|
|
418
|
+
client._gapFillDone.add(dedupKey);
|
|
419
|
+
client._gapFillActive = true;
|
|
420
|
+
let filled = 0;
|
|
421
|
+
try {
|
|
422
|
+
const messages = await client._pullGroupV2(groupId, afterSeq, 50);
|
|
423
|
+
filled = messages.length;
|
|
424
|
+
this.prunePushedSeqs(ns);
|
|
425
|
+
}
|
|
426
|
+
catch (exc) {
|
|
427
|
+
client._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
client._gapFillDone.delete(dedupKey);
|
|
431
|
+
client._gapFillActive = false;
|
|
432
|
+
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
433
|
+
client._safeAsync(this.fillGroupGap(groupId));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async fillGroupEventGap(groupId) {
|
|
438
|
+
const client = this.runtime.client;
|
|
439
|
+
if (client._state !== 'connected' || client._closing)
|
|
440
|
+
return;
|
|
441
|
+
const ns = `group_event:${groupId}`;
|
|
442
|
+
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
443
|
+
const dedupKey = `group_event_pull:${ns}`;
|
|
444
|
+
if (client._gapFillDone.has(dedupKey))
|
|
445
|
+
return;
|
|
446
|
+
client._gapFillDone.add(dedupKey);
|
|
447
|
+
client._gapFillActive = true;
|
|
448
|
+
try {
|
|
449
|
+
let nextAfterSeq = afterSeq;
|
|
450
|
+
const maxPages = 100;
|
|
451
|
+
let pageCount = 0;
|
|
452
|
+
while (pageCount < maxPages) {
|
|
453
|
+
pageCount += 1;
|
|
454
|
+
const result = await client.call('group.pull_events', {
|
|
455
|
+
group_id: groupId,
|
|
456
|
+
after_event_seq: nextAfterSeq,
|
|
457
|
+
device_id: client._deviceId,
|
|
458
|
+
limit: 50,
|
|
459
|
+
});
|
|
460
|
+
if (!isJsonObject(result))
|
|
461
|
+
return;
|
|
462
|
+
const events = result.events;
|
|
463
|
+
if (!Array.isArray(events))
|
|
464
|
+
return;
|
|
465
|
+
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
466
|
+
const eventObjects = events.filter((evt) => isJsonObject(evt));
|
|
467
|
+
if (eventObjects.length > 0) {
|
|
468
|
+
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
|
|
469
|
+
}
|
|
470
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
471
|
+
const serverAck = cursor ? Number(cursor.current_seq ?? 0) : 0;
|
|
472
|
+
if (serverAck > 0) {
|
|
473
|
+
const contigBeforeFloor = client._seqTracker.getContiguousSeq(ns);
|
|
474
|
+
if (contigBeforeFloor < serverAck) {
|
|
475
|
+
client._clientLog.info('group.pull_events retention-floor advance: ns=' + ns + ' contiguous=' + contigBeforeFloor + ' -> cursor.current_seq=' + serverAck);
|
|
476
|
+
client._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const eventSeqs = [];
|
|
480
|
+
for (const evt of eventObjects) {
|
|
481
|
+
const eventSeq = Number(evt.event_seq ?? 0);
|
|
482
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0)
|
|
483
|
+
eventSeqs.push(eventSeq);
|
|
484
|
+
evt._from_gap_fill = true;
|
|
485
|
+
const et = String(evt.event_type ?? '');
|
|
486
|
+
if (et === 'group.message_created')
|
|
487
|
+
continue;
|
|
488
|
+
const cs = evt.client_signature;
|
|
489
|
+
if (cs && typeof cs === 'object') {
|
|
490
|
+
if (client._shouldSkipEventSignature(evt)) {
|
|
491
|
+
delete evt.client_signature;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
evt._verified = await client._verifyEventSignature(evt, cs);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
await client._dispatcher.publish('group.changed', evt);
|
|
498
|
+
}
|
|
499
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
500
|
+
if (contig !== pageContigBefore) {
|
|
501
|
+
this.saveSeqTrackerState();
|
|
502
|
+
}
|
|
503
|
+
if (eventObjects.length > 0 && contig > 0 && contig !== pageContigBefore) {
|
|
504
|
+
const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, contig);
|
|
505
|
+
client._transport.call('group.ack_events', {
|
|
506
|
+
group_id: groupId,
|
|
507
|
+
event_seq: ackSeq,
|
|
508
|
+
device_id: client._deviceId,
|
|
509
|
+
slot_id: client._slotId,
|
|
510
|
+
}).catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
|
|
511
|
+
}
|
|
512
|
+
const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
|
|
513
|
+
if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
514
|
+
break;
|
|
515
|
+
nextAfterSeq = nextAfter;
|
|
516
|
+
}
|
|
517
|
+
if (pageCount >= maxPages) {
|
|
518
|
+
client._clientLog.warn(`group event gap fill reached max_pages=${maxPages} group=${groupId} after_seq=${nextAfterSeq}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (exc) {
|
|
522
|
+
client._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
client._gapFillDone.delete(dedupKey);
|
|
526
|
+
client._gapFillActive = false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
handleGroupChangedEventSeq(data, groupId) {
|
|
530
|
+
const client = this.runtime.client;
|
|
531
|
+
let needPull = false;
|
|
532
|
+
const rawEventSeq = data.event_seq;
|
|
533
|
+
if (rawEventSeq != null && groupId) {
|
|
534
|
+
const es = Number(rawEventSeq);
|
|
535
|
+
if (Number.isFinite(es) && es > 0) {
|
|
536
|
+
needPull = client._seqTracker.onMessageSeq(`group_event:${groupId}`, es);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (needPull && groupId && !data._from_gap_fill) {
|
|
540
|
+
client._safeAsync(this.fillGroupEventGap(groupId));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
enqueueOnlineUnreadHint(data) {
|
|
544
|
+
const client = this.runtime.client;
|
|
545
|
+
const groupId = String(data.group_id ?? '').trim();
|
|
546
|
+
if (!groupId)
|
|
547
|
+
return;
|
|
548
|
+
client._onlineUnreadHintQueue.set(groupId, { ...data });
|
|
549
|
+
if (client._onlineUnreadHintTimer || client._onlineUnreadHintDrainActive)
|
|
550
|
+
return;
|
|
551
|
+
const delayMs = Math.max(0, Number(client._onlineUnreadHintInitialDelayMs ?? 750) || 0);
|
|
552
|
+
client._onlineUnreadHintTimer = setTimeout(() => {
|
|
553
|
+
client._onlineUnreadHintTimer = null;
|
|
554
|
+
client._safeAsync(this.drainOnlineUnreadHints());
|
|
555
|
+
}, delayMs);
|
|
556
|
+
}
|
|
557
|
+
async drainOnlineUnreadHints() {
|
|
558
|
+
const client = this.runtime.client;
|
|
559
|
+
if (client._onlineUnreadHintDrainActive)
|
|
560
|
+
return;
|
|
561
|
+
client._onlineUnreadHintDrainActive = true;
|
|
562
|
+
try {
|
|
563
|
+
while (client._onlineUnreadHintQueue.size > 0) {
|
|
564
|
+
if (client.state !== 'ready')
|
|
565
|
+
return;
|
|
566
|
+
if (client._sessionOptions?.background_sync === false)
|
|
567
|
+
return;
|
|
568
|
+
const groupId = client._onlineUnreadHintQueue.keys().next().value;
|
|
569
|
+
if (!groupId)
|
|
570
|
+
return;
|
|
571
|
+
const payload = client._onlineUnreadHintQueue.get(groupId);
|
|
572
|
+
client._onlineUnreadHintQueue.delete(groupId);
|
|
573
|
+
if (!payload)
|
|
574
|
+
continue;
|
|
575
|
+
await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
|
|
576
|
+
const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
|
|
577
|
+
if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
|
|
578
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
catch (exc) {
|
|
583
|
+
client._clientLog.debug(`online unread hint drain failed: ${formatDeliveryError(exc)}`);
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
client._onlineUnreadHintDrainActive = false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async onRawGroupV2MessageCreated(data) {
|
|
590
|
+
const client = this.runtime.client;
|
|
591
|
+
const tStart = Date.now();
|
|
592
|
+
if (!isJsonObject(data)) {
|
|
593
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: non-object type=${typeof data}`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const groupId = String(data.group_id ?? '').trim();
|
|
597
|
+
const seq = Number(data.seq ?? 0) || 0;
|
|
598
|
+
const messageId = String(data.message_id ?? '').trim();
|
|
599
|
+
const senderAid = String(data.sender_aid ?? '').trim();
|
|
600
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated group=${groupId} seq=${seq} message_id=${messageId} sender=${senderAid}`);
|
|
601
|
+
if (!groupId || seq <= 0) {
|
|
602
|
+
client._clientLog.debug('_onRawGroupV2MessageCreated skipped: missing group_id or seq');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (!client._v2Session) {
|
|
606
|
+
client._clientLog.debug('_onRawGroupV2MessageCreated skipped: V2 session not initialized');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const eventKind = String(data.kind ?? '').trim();
|
|
610
|
+
if (eventKind === 'group.online_unread_hint' && !data._online_hint_drained) {
|
|
611
|
+
if (client._sessionOptions?.background_sync === false) {
|
|
612
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated skipped online unread hint: group=${groupId} background_sync=false`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.enqueueOnlineUnreadHint(data);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
const ns = `group:${groupId}`;
|
|
620
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
621
|
+
const contigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
622
|
+
if (contigBefore === seq) {
|
|
623
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated duplicate push already covered: group=${groupId} seq=${seq}`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const afterSeq = client._repairPushContiguousBound(ns, seq, false, '_raw.group.v2.message_created');
|
|
627
|
+
const dedupKey = `group_pull:${ns}`;
|
|
628
|
+
if (client._gapFillDone.has(dedupKey)) {
|
|
629
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated skipped: dedupKey=${dedupKey} in flight`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
client._gapFillDone.add(dedupKey);
|
|
633
|
+
try {
|
|
634
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
|
|
635
|
+
const messages = await client._pullGroupV2(groupId, afterSeq, 50);
|
|
636
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
client._gapFillDone.delete(dedupKey);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (exc) {
|
|
643
|
+
client._clientLog.warn(`_onRawGroupV2MessageCreated pull failed group=${groupId}: ${String(exc)}`);
|
|
644
|
+
}
|
|
645
|
+
finally {
|
|
646
|
+
client._clientLog.debug(`_onRawGroupV2MessageCreated exit: elapsed=${Date.now() - tStart}ms`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async onV2PushNotification(data) {
|
|
650
|
+
const client = this.runtime.client;
|
|
651
|
+
if (!client._v2Session)
|
|
652
|
+
return;
|
|
653
|
+
const pushSeq = isJsonObject(data) ? Number(data.seq ?? 0) || 0 : 0;
|
|
654
|
+
const pushFrom = isJsonObject(data) ? String(data.from_aid ?? '') : '';
|
|
655
|
+
const pushMsgId = isJsonObject(data) ? String(data.message_id ?? '') : '';
|
|
656
|
+
const envelopeJson = isJsonObject(data) ? data.envelope_json : undefined;
|
|
657
|
+
const hasPayload = !!envelopeJson;
|
|
658
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
659
|
+
let contigBefore = ns ? client._seqTracker.getContiguousSeq(ns) : 0;
|
|
660
|
+
client._clientLog.debug(`_onV2PushNotification: push_seq=${pushSeq || 'null'} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${hasPayload} contiguous_seq=${contigBefore}`);
|
|
661
|
+
if (pushSeq > 0 && ns) {
|
|
662
|
+
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
663
|
+
if (contigBefore === pushSeq) {
|
|
664
|
+
client._clientLog.debug(`_onV2PushNotification: push seq=${pushSeq} already covered by contiguous_seq=${contigBefore}, ignore duplicate push`);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
contigBefore = client._repairPushContiguousBound(ns, pushSeq, hasPayload, '_raw.peer.v2.message_received');
|
|
668
|
+
}
|
|
669
|
+
if (hasPayload && pushSeq > 0 && ns) {
|
|
670
|
+
try {
|
|
671
|
+
const decrypted = await client._decryptV2PushMessage(data);
|
|
672
|
+
if (decrypted) {
|
|
673
|
+
const needPull = client._seqTracker.onMessageSeq(ns, pushSeq);
|
|
674
|
+
const published = await client._publishOrderedMessage('message.received', ns, pushSeq, decrypted);
|
|
675
|
+
const newContig = client._seqTracker.getContiguousSeq(ns);
|
|
676
|
+
if (newContig !== contigBefore) {
|
|
677
|
+
client._saveSeqTrackerState();
|
|
678
|
+
}
|
|
679
|
+
if (newContig > 0 && newContig !== contigBefore) {
|
|
680
|
+
const ackSeq = this.clampAckSeq('message.v2.ack', 'up_to_seq', ns, newContig);
|
|
681
|
+
try {
|
|
682
|
+
await client._callRawV2Rpc('message.v2.ack', { up_to_seq: ackSeq });
|
|
683
|
+
}
|
|
684
|
+
catch (e) {
|
|
685
|
+
client._clientLog.debug(`V2 P2P push-ack failed: ${e}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
client._clientLog.debug(`_onV2PushNotification: push 带 payload 解密成功, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`);
|
|
689
|
+
if (!needPull && (published || newContig >= pushSeq || pushSeq <= contigBefore)) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
client._clientLog.debug(`_onV2PushNotification: payload push seq=${pushSeq} 因空洞挂起,继续 pull 补齐 after_seq=${newContig}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (exc) {
|
|
696
|
+
client._clientLog.debug(`_onV2PushNotification: push payload 解密失败, fallback to pull: ${exc}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (pushSeq > 0 && ns) {
|
|
700
|
+
client._clientLog.debug(`_onV2PushNotification: 纯通知 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, 触发 pull(after_seq=${contigBefore})`);
|
|
701
|
+
}
|
|
702
|
+
if (client._v2PullInflight) {
|
|
703
|
+
client._v2PullPending = true;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
client._v2PullInflight = true;
|
|
707
|
+
const dedupKey = `p2p_pull:${ns}`;
|
|
708
|
+
client._gapFillDone.add(dedupKey);
|
|
709
|
+
try {
|
|
710
|
+
do {
|
|
711
|
+
client._v2PullPending = false;
|
|
712
|
+
await client._pullV2();
|
|
713
|
+
const newContig = ns ? client._seqTracker.getContiguousSeq(ns) : -1;
|
|
714
|
+
client._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
|
|
715
|
+
} while (client._v2PullPending);
|
|
716
|
+
}
|
|
717
|
+
catch (exc) {
|
|
718
|
+
const newContig = ns ? client._seqTracker.getContiguousSeq(ns) : -1;
|
|
719
|
+
client._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${exc}`);
|
|
720
|
+
}
|
|
721
|
+
finally {
|
|
722
|
+
client._v2PullInflight = false;
|
|
723
|
+
client._gapFillDone.delete(dedupKey);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async restoreSeqTrackerState() {
|
|
727
|
+
const client = this.runtime.client;
|
|
728
|
+
if (!client._aid)
|
|
729
|
+
return;
|
|
730
|
+
const context = client._seqTrackerContext;
|
|
731
|
+
if (!context)
|
|
732
|
+
return;
|
|
733
|
+
const aid = client._aid;
|
|
734
|
+
const deviceId = client._deviceId;
|
|
735
|
+
const slotId = client._slotId;
|
|
736
|
+
try {
|
|
737
|
+
const loadAll = client._tokenStore.loadAllSeqs?.bind(client._tokenStore);
|
|
738
|
+
if (typeof loadAll === 'function') {
|
|
739
|
+
let state = await loadAll(aid, deviceId, slotId);
|
|
740
|
+
if (client._seqTrackerContext !== context)
|
|
741
|
+
return;
|
|
742
|
+
if (state && typeof state === 'object' && Object.keys(state).length > 0) {
|
|
743
|
+
state = await this.migrateSeqStateGroupIds(state);
|
|
744
|
+
client._seqTracker.restoreState(state);
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const loader = client._tokenStore.loadInstanceState?.bind(client._tokenStore);
|
|
749
|
+
if (typeof loader !== 'function')
|
|
750
|
+
return;
|
|
751
|
+
const stateHolder = await loader(aid, deviceId, slotId);
|
|
752
|
+
if (client._seqTrackerContext !== context)
|
|
753
|
+
return;
|
|
754
|
+
if (stateHolder && typeof stateHolder === 'object') {
|
|
755
|
+
const state = stateHolder.seq_tracker_state;
|
|
756
|
+
if (isJsonObject(state)) {
|
|
757
|
+
const migrated = await this.migrateSeqStateGroupIds(state);
|
|
758
|
+
client._seqTracker.restoreState(migrated);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch (exc) {
|
|
763
|
+
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
764
|
+
phase: 'restore',
|
|
765
|
+
aid,
|
|
766
|
+
device_id: deviceId,
|
|
767
|
+
slot_id: slotId,
|
|
768
|
+
error: String(exc),
|
|
769
|
+
}).catch(() => { });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async migrateSeqStateGroupIds(state) {
|
|
773
|
+
const client = this.runtime.client;
|
|
774
|
+
if (!state || Object.keys(state).length === 0)
|
|
775
|
+
return state;
|
|
776
|
+
const renameMap = {};
|
|
777
|
+
for (const ns of Object.keys(state)) {
|
|
778
|
+
for (const prefix of ['group_event:', 'group_msg:']) {
|
|
779
|
+
if (ns.startsWith(prefix)) {
|
|
780
|
+
const oldGid = ns.slice(prefix.length);
|
|
781
|
+
const newGid = normalizeGroupId(oldGid);
|
|
782
|
+
if (newGid && newGid !== oldGid) {
|
|
783
|
+
renameMap[ns] = `${prefix}${newGid}`;
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (Object.keys(renameMap).length === 0)
|
|
790
|
+
return state;
|
|
791
|
+
const newState = { ...state };
|
|
792
|
+
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
793
|
+
const oldVal = Number(newState[oldNs] ?? 0);
|
|
794
|
+
const curVal = Number(newState[newNs] ?? 0);
|
|
795
|
+
delete newState[oldNs];
|
|
796
|
+
newState[newNs] = Math.max(oldVal, curVal);
|
|
797
|
+
}
|
|
798
|
+
if (!client._aid)
|
|
799
|
+
return newState;
|
|
800
|
+
const aid = client._aid;
|
|
801
|
+
const deviceId = client._deviceId;
|
|
802
|
+
const slotId = client._slotId;
|
|
803
|
+
const saver = client._tokenStore.saveSeq?.bind(client._tokenStore);
|
|
804
|
+
const deleter = client._tokenStore.deleteSeq?.bind(client._tokenStore);
|
|
805
|
+
if (typeof saver === 'function') {
|
|
806
|
+
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
807
|
+
if (typeof deleter === 'function') {
|
|
808
|
+
try {
|
|
809
|
+
await deleter(aid, deviceId, slotId, oldNs);
|
|
810
|
+
}
|
|
811
|
+
catch (e) {
|
|
812
|
+
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
813
|
+
phase: 'migrate_delete',
|
|
814
|
+
aid,
|
|
815
|
+
device_id: deviceId,
|
|
816
|
+
slot_id: slotId,
|
|
817
|
+
ns: oldNs,
|
|
818
|
+
error: String(e),
|
|
819
|
+
}).catch(() => { });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
await saver(aid, deviceId, slotId, newNs, newState[newNs]);
|
|
824
|
+
}
|
|
825
|
+
catch (e) {
|
|
826
|
+
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
827
|
+
phase: 'migrate_save',
|
|
828
|
+
aid,
|
|
829
|
+
device_id: deviceId,
|
|
830
|
+
slot_id: slotId,
|
|
831
|
+
ns: newNs,
|
|
832
|
+
error: String(e),
|
|
833
|
+
}).catch(() => { });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return newState;
|
|
838
|
+
}
|
|
839
|
+
saveSeqTrackerState() {
|
|
840
|
+
const client = this.runtime.client;
|
|
841
|
+
if (!client._aid)
|
|
842
|
+
return;
|
|
843
|
+
const state = client._seqTracker.exportState();
|
|
844
|
+
if (Object.keys(state).length === 0)
|
|
845
|
+
return;
|
|
846
|
+
try {
|
|
847
|
+
const saveFn = client._tokenStore.saveSeq?.bind(client._tokenStore);
|
|
848
|
+
if (typeof saveFn === 'function') {
|
|
849
|
+
for (const [ns, seq] of Object.entries(state)) {
|
|
850
|
+
saveFn(client._aid, client._deviceId, client._slotId, ns, seq).catch((exc) => {
|
|
851
|
+
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
852
|
+
phase: 'save',
|
|
853
|
+
aid: client._aid,
|
|
854
|
+
device_id: client._deviceId,
|
|
855
|
+
slot_id: client._slotId,
|
|
856
|
+
error: String(exc),
|
|
857
|
+
}).catch(() => { });
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (typeof client._tokenStore.updateInstanceState === 'function') {
|
|
863
|
+
client._tokenStore.updateInstanceState(client._aid, client._deviceId, client._slotId, (current) => {
|
|
864
|
+
current.seq_tracker_state = state;
|
|
865
|
+
return current;
|
|
866
|
+
}).catch((exc) => {
|
|
867
|
+
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
868
|
+
phase: 'save',
|
|
869
|
+
aid: client._aid,
|
|
870
|
+
device_id: client._deviceId,
|
|
871
|
+
slot_id: client._slotId,
|
|
872
|
+
error: String(exc),
|
|
873
|
+
}).catch(() => { });
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
catch (exc) {
|
|
878
|
+
client._dispatcher.publish('seq_tracker.persist_error', {
|
|
879
|
+
phase: 'save',
|
|
880
|
+
aid: client._aid,
|
|
881
|
+
device_id: client._deviceId,
|
|
882
|
+
slot_id: client._slotId,
|
|
883
|
+
error: String(exc),
|
|
884
|
+
}).catch(() => { });
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
persistRepairedSeq(ns) {
|
|
888
|
+
const client = this.runtime.client;
|
|
889
|
+
if (!client._aid || !ns)
|
|
890
|
+
return;
|
|
891
|
+
const seq = client._seqTracker.getContiguousSeq(ns);
|
|
892
|
+
try {
|
|
893
|
+
if (seq > 0 && typeof client._tokenStore.saveSeq === 'function') {
|
|
894
|
+
client._tokenStore.saveSeq(client._aid, client._deviceId, client._slotId, ns, seq).catch((exc) => {
|
|
895
|
+
client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
896
|
+
});
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const deleteSeq = client._tokenStore.deleteSeq;
|
|
900
|
+
if (seq <= 0 && typeof deleteSeq === 'function') {
|
|
901
|
+
deleteSeq.call(client._tokenStore, client._aid, client._deviceId, client._slotId, ns).catch((exc) => {
|
|
902
|
+
client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
903
|
+
});
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (seq > 0) {
|
|
907
|
+
this.saveSeqTrackerState();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch (exc) {
|
|
911
|
+
client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
clampAckSeq(method, field, ns, seq) {
|
|
915
|
+
const client = this.runtime.client;
|
|
916
|
+
const original = seq;
|
|
917
|
+
let next = Number(seq);
|
|
918
|
+
if (!Number.isFinite(next))
|
|
919
|
+
return 0;
|
|
920
|
+
if (next < 0)
|
|
921
|
+
next = 0;
|
|
922
|
+
if (ns) {
|
|
923
|
+
const maxSeen = client._seqTracker.getMaxSeenSeq(ns);
|
|
924
|
+
if (maxSeen > 0 && next > maxSeen) {
|
|
925
|
+
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > max_seen=${maxSeen}, clamp`);
|
|
926
|
+
next = maxSeen;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return next;
|
|
930
|
+
}
|
|
931
|
+
clampAckParams(method, params) {
|
|
932
|
+
const client = this.runtime.client;
|
|
933
|
+
const clampField = (field, ns) => {
|
|
934
|
+
const raw = params[field];
|
|
935
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw))
|
|
936
|
+
return params;
|
|
937
|
+
const next = this.clampAckSeq(method, field, ns, raw);
|
|
938
|
+
return next === raw ? params : { ...params, [field]: next };
|
|
939
|
+
};
|
|
940
|
+
if (method === 'message.v2.ack') {
|
|
941
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
942
|
+
return clampField('up_to_seq', ns);
|
|
943
|
+
}
|
|
944
|
+
if (method === 'message.ack') {
|
|
945
|
+
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
946
|
+
return clampField('seq', ns);
|
|
947
|
+
}
|
|
948
|
+
if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
|
|
949
|
+
const groupId = normalizeGroupId(String(params.group_id ?? '')) || String(params.group_id ?? '').trim();
|
|
950
|
+
const ns = groupId ? `group:${groupId}` : '';
|
|
951
|
+
if (method === 'group.v2.ack')
|
|
952
|
+
return clampField('up_to_seq', ns);
|
|
953
|
+
if (method === 'group.ack_messages')
|
|
954
|
+
return clampField('msg_seq', ns);
|
|
955
|
+
if (method === 'group.ack_events')
|
|
956
|
+
return clampField('event_seq', groupId ? `group_event:${groupId}` : '');
|
|
957
|
+
}
|
|
958
|
+
return params;
|
|
959
|
+
}
|
|
960
|
+
async drainOrderedMessages(ns, beforeSeq) {
|
|
961
|
+
const client = this.runtime.client;
|
|
962
|
+
const queue = client._pendingOrderedMsgs.get(ns);
|
|
963
|
+
if (!queue || queue.size === 0)
|
|
964
|
+
return;
|
|
965
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
966
|
+
const ready = [...queue.keys()]
|
|
967
|
+
.filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
|
|
968
|
+
.sort((a, b) => a - b);
|
|
969
|
+
for (const seq of ready) {
|
|
970
|
+
const item = queue.get(seq);
|
|
971
|
+
queue.delete(seq);
|
|
972
|
+
if (!item || client._pushedSeqs.get(ns)?.has(seq))
|
|
973
|
+
continue;
|
|
974
|
+
await client._publishAppEvent(item.event, item.payload);
|
|
975
|
+
this.markPublishedSeq(ns, seq);
|
|
976
|
+
}
|
|
977
|
+
if (queue.size === 0)
|
|
978
|
+
client._pendingOrderedMsgs.delete(ns);
|
|
979
|
+
}
|
|
980
|
+
async publishOrderedMessage(event, ns, seq, payload) {
|
|
981
|
+
const client = this.runtime.client;
|
|
982
|
+
const seqNum = Number(seq);
|
|
983
|
+
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
|
|
984
|
+
await client._publishAppEvent(event, payload);
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
if (client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
988
|
+
const queue = client._pendingOrderedMsgs.get(ns);
|
|
989
|
+
queue?.delete(seqNum);
|
|
990
|
+
if (queue && queue.size === 0)
|
|
991
|
+
client._pendingOrderedMsgs.delete(ns);
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
995
|
+
if (seqNum > contig) {
|
|
996
|
+
this.enqueueOrderedMessage(ns, event, seqNum, payload);
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
await this.drainOrderedMessages(ns, seqNum);
|
|
1000
|
+
if (client._pushedSeqs.get(ns)?.has(seqNum))
|
|
1001
|
+
return false;
|
|
1002
|
+
const queue = client._pendingOrderedMsgs.get(ns);
|
|
1003
|
+
queue?.delete(seqNum);
|
|
1004
|
+
if (queue && queue.size === 0)
|
|
1005
|
+
client._pendingOrderedMsgs.delete(ns);
|
|
1006
|
+
await client._publishAppEvent(event, payload);
|
|
1007
|
+
this.markPublishedSeq(ns, seqNum);
|
|
1008
|
+
await this.drainOrderedMessages(ns);
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
async publishPulledMessage(event, ns, seq, payload) {
|
|
1012
|
+
const client = this.runtime.client;
|
|
1013
|
+
const seqNum = Number(seq);
|
|
1014
|
+
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
|
|
1015
|
+
await client._publishAppEvent(event, payload);
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
const queue = client._pendingOrderedMsgs.get(ns);
|
|
1019
|
+
if (client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
1020
|
+
queue?.delete(seqNum);
|
|
1021
|
+
if (queue && queue.size === 0)
|
|
1022
|
+
client._pendingOrderedMsgs.delete(ns);
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
queue?.delete(seqNum);
|
|
1026
|
+
if (queue && queue.size === 0)
|
|
1027
|
+
client._pendingOrderedMsgs.delete(ns);
|
|
1028
|
+
await client._publishAppEvent(event, payload);
|
|
1029
|
+
this.markPublishedSeq(ns, seqNum);
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
//# sourceMappingURL=delivery.js.map
|