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