@agentunion/fastaun-browser 0.5.6 → 0.5.9
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 +104 -0
- package/_packed_docs/CHANGELOG.md +104 -0
- package/_packed_docs/INDEX.md +12 -11
- package/_packed_docs/KITE_DOCS_GUIDE.md +4 -3
- package/_packed_docs/agent.md/SCHEMA.md +83 -58
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +1 -1
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +1 -1
- package/_packed_docs/agent.md/examples//347/276/244/347/273/204-/345/274/200/345/217/221/345/233/242/351/230/237.md +21 -0
- package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +3 -3
- package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +7 -0
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +422 -331
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +598 -398
- package/_packed_docs/group-message-rpc-alignment-gaps.md +741 -0
- package/_packed_docs/message-online-push-alignment.md +572 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +10 -10
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +30 -10
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +23 -20
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +3 -3
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +5 -6
- package/_packed_docs/sdk/09-group-rpc-manual.md +11 -7
- package/_packed_docs/sdk/09-message-rpc-manual.md +5 -3
- package/_packed_docs/sdk/09-storage-rpc-manual.md +14 -3
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
- package/_packed_docs/sdk/CHANGELOG-0.5.6.md +73 -0
- package/_packed_docs/sdk/INDEX.md +22 -22
- package/_packed_docs/sdk/README.md +6 -5
- package/dist/agent-md-schema.d.ts +14 -0
- package/dist/agent-md-schema.d.ts.map +1 -0
- package/dist/agent-md-schema.js +103 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +7 -0
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +230 -10
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +5 -1
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +24 -4
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +29 -7
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +30253 -21955
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +36 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +40 -5
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +719 -221
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +2 -2
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.d.ts +15 -1
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +463 -151
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +25 -6
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +407 -123
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.d.ts.map +1 -1
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -2
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +434 -112
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +22 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +500 -116
- package/dist/client.js.map +1 -1
- package/dist/facades.d.ts +23 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +55 -7
- package/dist/facades.js.map +1 -1
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +2 -17
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +8 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +23 -1
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +8 -1
- package/dist/transport.js.map +1 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +8 -14
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
package/dist/client/delivery.js
CHANGED
|
@@ -57,6 +57,25 @@ function positiveSafeSequenceHint(value) {
|
|
|
57
57
|
const parsed = Number(value);
|
|
58
58
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
59
59
|
}
|
|
60
|
+
function nonNegativeSafeSequenceHint(value) {
|
|
61
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
62
|
+
return null;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function deduplicatePlainForwardPageMessages(value, afterSeq) {
|
|
66
|
+
if (!Array.isArray(value))
|
|
67
|
+
return [];
|
|
68
|
+
const bySeq = new Map();
|
|
69
|
+
for (const raw of value) {
|
|
70
|
+
if (!isJsonObject(raw))
|
|
71
|
+
continue;
|
|
72
|
+
const seq = positiveSafeSequenceHint(raw.seq);
|
|
73
|
+
if (seq <= afterSeq || bySeq.has(seq))
|
|
74
|
+
continue;
|
|
75
|
+
bySeq.set(seq, { ...raw, seq });
|
|
76
|
+
}
|
|
77
|
+
return [...bySeq.entries()].sort(([left], [right]) => left - right).map(([, message]) => message);
|
|
78
|
+
}
|
|
60
79
|
function p2pAppEventFromPlainPullMessage(message) {
|
|
61
80
|
const payload = message.payload;
|
|
62
81
|
const payloadType = isJsonObject(payload)
|
|
@@ -77,18 +96,31 @@ export class MessageDeliveryEngine {
|
|
|
77
96
|
runtime;
|
|
78
97
|
realtimeTailResults = null;
|
|
79
98
|
realtimeSyncing = null;
|
|
99
|
+
pendingP2pPullUpper = null;
|
|
100
|
+
pendingGroupPullUpper = null;
|
|
101
|
+
pendingPullNoProgressAcks = null;
|
|
102
|
+
onlineUnreadHintTargets = null;
|
|
103
|
+
onlineUnreadHintOwners = null;
|
|
80
104
|
realtimeAcking = null;
|
|
81
105
|
pendingP2PInlineAcks = null;
|
|
82
106
|
pendingGroupInlineAcks = null;
|
|
83
107
|
pendingGroupInlineAckNamespaces = null;
|
|
84
108
|
// reset 后旧 inline Promise 只能自行结束,不能触碰新身份上下文的状态。
|
|
85
109
|
inlineGeneration = 0;
|
|
110
|
+
// 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
|
|
111
|
+
seqTrackerWriteChain = Promise.resolve();
|
|
86
112
|
constructor(runtime) {
|
|
87
113
|
this.runtime = runtime;
|
|
88
114
|
}
|
|
89
115
|
resetInlineAckState() {
|
|
90
116
|
this.inlineGeneration += 1;
|
|
117
|
+
void this.runtime.client._rpcPipeline?.invalidatePulls?.();
|
|
91
118
|
this.realtimeSyncing = null;
|
|
119
|
+
this.pendingP2pPullUpper = null;
|
|
120
|
+
this.pendingGroupPullUpper = null;
|
|
121
|
+
this.pendingPullNoProgressAcks = null;
|
|
122
|
+
this.onlineUnreadHintTargets = null;
|
|
123
|
+
this.onlineUnreadHintOwners = null;
|
|
92
124
|
this.realtimeTailResults = null;
|
|
93
125
|
this.realtimeAcking = null;
|
|
94
126
|
this.pendingP2PInlineAcks = null;
|
|
@@ -98,6 +130,20 @@ export class MessageDeliveryEngine {
|
|
|
98
130
|
isInlineGenerationCurrent(generation) {
|
|
99
131
|
return generation === this.inlineGeneration;
|
|
100
132
|
}
|
|
133
|
+
isPullOperationCurrent() {
|
|
134
|
+
const client = this.runtime.client;
|
|
135
|
+
const generation = client._pullOperationGeneration;
|
|
136
|
+
if (generation === undefined)
|
|
137
|
+
return true;
|
|
138
|
+
const pipeline = client._rpcPipeline;
|
|
139
|
+
return typeof pipeline?.isPullGenerationCurrent === 'function'
|
|
140
|
+
? pipeline.isPullGenerationCurrent(generation)
|
|
141
|
+
: true;
|
|
142
|
+
}
|
|
143
|
+
ensurePullOperationCurrent() {
|
|
144
|
+
if (!this.isPullOperationCurrent())
|
|
145
|
+
throw new Error('pull invalidated');
|
|
146
|
+
}
|
|
101
147
|
captureInlineGeneration() {
|
|
102
148
|
return this.inlineGeneration;
|
|
103
149
|
}
|
|
@@ -112,6 +158,74 @@ export class MessageDeliveryEngine {
|
|
|
112
158
|
endRealtimeSync(ns) {
|
|
113
159
|
this.realtimeSyncing?.delete(ns);
|
|
114
160
|
}
|
|
161
|
+
hasPendingPull(ns) {
|
|
162
|
+
const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
|
|
163
|
+
return (pending?.get(ns) ?? 0) > 0;
|
|
164
|
+
}
|
|
165
|
+
onPullGateIdle(ns) {
|
|
166
|
+
if (!ns)
|
|
167
|
+
return;
|
|
168
|
+
this.schedulePendingPullIfNeeded(ns, 'pull-gate-idle');
|
|
169
|
+
}
|
|
170
|
+
recordPendingPull(ns, seq) {
|
|
171
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
172
|
+
return;
|
|
173
|
+
const pending = ns.startsWith('p2p:')
|
|
174
|
+
? (this.pendingP2pPullUpper ?? new Map())
|
|
175
|
+
: (this.pendingGroupPullUpper ?? new Map());
|
|
176
|
+
if (ns.startsWith('p2p:'))
|
|
177
|
+
this.pendingP2pPullUpper = pending;
|
|
178
|
+
else
|
|
179
|
+
this.pendingGroupPullUpper = pending;
|
|
180
|
+
pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
|
|
181
|
+
}
|
|
182
|
+
consumePendingPull(ns, throughSeq) {
|
|
183
|
+
const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
|
|
184
|
+
if ((pending?.get(ns) ?? 0) <= throughSeq) {
|
|
185
|
+
pending?.delete(ns);
|
|
186
|
+
this.pendingPullNoProgressAcks?.delete(ns);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
markPendingPullNoProgress(ns, ack) {
|
|
190
|
+
const blocked = this.pendingPullNoProgressAcks ?? new Map();
|
|
191
|
+
this.pendingPullNoProgressAcks = blocked;
|
|
192
|
+
blocked.set(ns, ack);
|
|
193
|
+
}
|
|
194
|
+
schedulePendingPullIfNeeded(ns, reason) {
|
|
195
|
+
const pending = ns.startsWith('p2p:') ? this.pendingP2pPullUpper : this.pendingGroupPullUpper;
|
|
196
|
+
if (!pending)
|
|
197
|
+
return false;
|
|
198
|
+
const upper = pending.get(ns) ?? 0;
|
|
199
|
+
if (upper <= 0) {
|
|
200
|
+
pending.delete(ns);
|
|
201
|
+
this.pendingPullNoProgressAcks?.delete(ns);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const client = this.runtime.client;
|
|
205
|
+
const contiguous = client._seqTracker.getContiguousSeq(ns);
|
|
206
|
+
if (upper <= contiguous) {
|
|
207
|
+
pending.delete(ns);
|
|
208
|
+
this.pendingPullNoProgressAcks?.delete(ns);
|
|
209
|
+
client._clientLog?.debug(`pending pull upper already covered: ns=${ns}, upper_seq=${upper}, contiguous=${contiguous}, reason=${reason}`);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const blockedAck = this.pendingPullNoProgressAcks?.get(ns);
|
|
213
|
+
if (blockedAck !== undefined && contiguous <= blockedAck)
|
|
214
|
+
return false;
|
|
215
|
+
this.pendingPullNoProgressAcks?.delete(ns);
|
|
216
|
+
if (client.state !== ConnectionState.READY || client._closing || this.realtimeSyncing?.has(ns)
|
|
217
|
+
|| client._rpcPipeline?.hasPullActivity?.(ns, false))
|
|
218
|
+
return false;
|
|
219
|
+
pending.delete(ns);
|
|
220
|
+
client._clientLog?.debug(`pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upper}, reason=${reason}`);
|
|
221
|
+
if (ns.startsWith('p2p:')) {
|
|
222
|
+
client._safeAsync(this.onV2PushNotification({ seq: upper }));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
client._safeAsync(this.onRawGroupV2MessageCreated({ group_id: ns.slice('group:'.length), seq: upper }));
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
115
229
|
beginRealtimeAck(ns, seq) {
|
|
116
230
|
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
117
231
|
return false;
|
|
@@ -499,6 +613,230 @@ export class MessageDeliveryEngine {
|
|
|
499
613
|
}
|
|
500
614
|
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
501
615
|
}
|
|
616
|
+
resolveP2PPullAckSeq(result, requestedSeq) {
|
|
617
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
618
|
+
return 0;
|
|
619
|
+
if (isJsonObject(result)
|
|
620
|
+
&& Object.prototype.hasOwnProperty.call(result, 'server_ack_seq')) {
|
|
621
|
+
const value = result.server_ack_seq;
|
|
622
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
623
|
+
}
|
|
624
|
+
return 0;
|
|
625
|
+
}
|
|
626
|
+
resolveGroupPullAckSeq(result, requestedSeq) {
|
|
627
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
628
|
+
return 0;
|
|
629
|
+
if (!isJsonObject(result))
|
|
630
|
+
return 0;
|
|
631
|
+
const row = result;
|
|
632
|
+
if (!Object.prototype.hasOwnProperty.call(row, 'cursor'))
|
|
633
|
+
return 0;
|
|
634
|
+
const cursor = row.cursor;
|
|
635
|
+
if (!isJsonObject(cursor))
|
|
636
|
+
return 0;
|
|
637
|
+
const cursorRow = cursor;
|
|
638
|
+
if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
|
|
639
|
+
return 0;
|
|
640
|
+
const value = cursorRow.current_seq;
|
|
641
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
642
|
+
}
|
|
643
|
+
resolveForwardAckSeq(result, requestedSeq) {
|
|
644
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
645
|
+
return 0;
|
|
646
|
+
// 旧服务端成功响应可能不带累计水位字段;RPC 已成功,兼容按请求水位处理。
|
|
647
|
+
if (!isJsonObject(result))
|
|
648
|
+
return requestedSeq;
|
|
649
|
+
const row = result;
|
|
650
|
+
let value;
|
|
651
|
+
if (Object.prototype.hasOwnProperty.call(row, 'effective_ack_seq')) {
|
|
652
|
+
value = row.effective_ack_seq;
|
|
653
|
+
}
|
|
654
|
+
else if (Object.prototype.hasOwnProperty.call(row, 'ack_seq')) {
|
|
655
|
+
value = row.ack_seq;
|
|
656
|
+
}
|
|
657
|
+
else if (Object.prototype.hasOwnProperty.call(row, 'cursor')) {
|
|
658
|
+
const cursor = row.cursor;
|
|
659
|
+
const cursorRow = isJsonObject(cursor)
|
|
660
|
+
? cursor
|
|
661
|
+
: null;
|
|
662
|
+
if (cursorRow) {
|
|
663
|
+
if (!Object.prototype.hasOwnProperty.call(cursorRow, 'current_seq'))
|
|
664
|
+
return 0;
|
|
665
|
+
value = cursorRow.current_seq;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
value = cursor;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
return requestedSeq;
|
|
673
|
+
}
|
|
674
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
675
|
+
}
|
|
676
|
+
forwardCoordinator() {
|
|
677
|
+
const coordinator = this.runtime.client._v2E2EE;
|
|
678
|
+
if (!coordinator
|
|
679
|
+
|| typeof coordinator.pendingForwardCursor !== 'function'
|
|
680
|
+
|| typeof coordinator.recordForwardCursor !== 'function'
|
|
681
|
+
|| typeof coordinator.clearForwardCursor !== 'function'
|
|
682
|
+
|| typeof coordinator.pendingForwardAck !== 'function'
|
|
683
|
+
|| typeof coordinator.recordForwardAck !== 'function'
|
|
684
|
+
|| typeof coordinator.clearForwardAck !== 'function') {
|
|
685
|
+
throw new Error('Forward cursor coordinator is unavailable');
|
|
686
|
+
}
|
|
687
|
+
return coordinator;
|
|
688
|
+
}
|
|
689
|
+
async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
|
|
690
|
+
const client = this.runtime.client;
|
|
691
|
+
this.ensurePullOperationCurrent();
|
|
692
|
+
const coordinator = this.forwardCoordinator();
|
|
693
|
+
const generation = this.captureInlineGeneration();
|
|
694
|
+
coordinator.recordForwardAck(ns, ackSeq);
|
|
695
|
+
const params = method === 'message.ack'
|
|
696
|
+
? {
|
|
697
|
+
seq: ackSeq,
|
|
698
|
+
device_id: client._deviceId,
|
|
699
|
+
slot_id: client._slotId,
|
|
700
|
+
_rpc_background: true,
|
|
701
|
+
}
|
|
702
|
+
: {
|
|
703
|
+
group_id: groupId,
|
|
704
|
+
msg_seq: ackSeq,
|
|
705
|
+
device_id: client._deviceId,
|
|
706
|
+
slot_id: client._slotId,
|
|
707
|
+
_rpc_background: true,
|
|
708
|
+
};
|
|
709
|
+
const result = await client._rpcPipeline.rawCall(method, params, { background: true });
|
|
710
|
+
this.ensurePullOperationCurrent();
|
|
711
|
+
const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
|
|
712
|
+
if (actualAckSeq < ackSeq) {
|
|
713
|
+
throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
|
|
714
|
+
}
|
|
715
|
+
coordinator.clearForwardAck(ns, actualAckSeq);
|
|
716
|
+
if (method === 'message.ack') {
|
|
717
|
+
this.completePendingP2PInlineAckUpTo(ns, actualAckSeq, generation);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
this.completePendingGroupInlineAckUpTo(ns, actualAckSeq, generation);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
|
|
724
|
+
if (!isJsonObject(result)) {
|
|
725
|
+
throw new Error(`${method} response must be an object`);
|
|
726
|
+
}
|
|
727
|
+
const client = this.runtime.client;
|
|
728
|
+
this.ensurePullOperationCurrent();
|
|
729
|
+
const response = result;
|
|
730
|
+
const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
|
|
731
|
+
const coordinator = this.forwardCoordinator();
|
|
732
|
+
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
733
|
+
const pageInlineGeneration = this.captureInlineGeneration();
|
|
734
|
+
const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
|
|
735
|
+
const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
|
|
736
|
+
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
737
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
738
|
+
: null;
|
|
739
|
+
response._contig_before = pageContigBefore;
|
|
740
|
+
const cursor = isJsonObject(response.cursor)
|
|
741
|
+
? response.cursor
|
|
742
|
+
: null;
|
|
743
|
+
const serverAckRaw = method === 'message.pull' ? response.server_ack_seq : cursor?.current_seq;
|
|
744
|
+
const hasServerAck = method === 'message.pull'
|
|
745
|
+
? Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')
|
|
746
|
+
: Boolean(cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq'));
|
|
747
|
+
const parsedServerAck = hasServerAck ? nonNegativeSafeSequenceHint(serverAckRaw) : null;
|
|
748
|
+
const serverAckSeq = parsedServerAck ?? 0;
|
|
749
|
+
const retentionFloor = method === 'message.pull'
|
|
750
|
+
? positiveSafeSequenceHint(response.retention_floor_seq)
|
|
751
|
+
: Math.max(positiveSafeSequenceHint(response.retention_floor_message_seq), positiveSafeSequenceHint(response.retention_floor_seq), positiveSafeSequenceHint(cursor?.retention_floor_seq));
|
|
752
|
+
const visibilityFloor = method === 'group.pull'
|
|
753
|
+
? Math.max(positiveSafeSequenceHint(cursor?.join_seq), Math.max(0, positiveSafeSequenceHint(response.earliest_available_message_seq) - 1))
|
|
754
|
+
: 0;
|
|
755
|
+
let publishedCount = 0;
|
|
756
|
+
let committed = false;
|
|
757
|
+
try {
|
|
758
|
+
for (const rawMessage of messages) {
|
|
759
|
+
this.ensurePullOperationCurrent();
|
|
760
|
+
const seq = positiveSafeSequenceHint(rawMessage.seq);
|
|
761
|
+
if (method === 'message.pull') {
|
|
762
|
+
const appEvent = p2pAppEventFromPlainPullMessage(rawMessage);
|
|
763
|
+
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
764
|
+
publishedCount += 1;
|
|
765
|
+
}
|
|
766
|
+
this.ensurePullOperationCurrent();
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const message = normalizeGroupMentionMode(rawMessage);
|
|
770
|
+
if (this.recallEventFromGroupMessage(message)) {
|
|
771
|
+
if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
|
|
772
|
+
this.ensurePullOperationCurrent();
|
|
773
|
+
this.markPublishedSeq(ns, seq);
|
|
774
|
+
publishedCount += 1;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
|
|
778
|
+
this.ensurePullOperationCurrent();
|
|
779
|
+
publishedCount += 1;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
this.ensurePullOperationCurrent();
|
|
783
|
+
if (messages.length > 0)
|
|
784
|
+
client._seqTracker.onPullResult(ns, messages, afterSeq);
|
|
785
|
+
const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
|
|
786
|
+
if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
|
|
787
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
788
|
+
}
|
|
789
|
+
if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
|
|
790
|
+
this.ensurePullOperationCurrent();
|
|
791
|
+
await this.drainOrderedMessages(ns, undefined, false, false);
|
|
792
|
+
this.ensurePullOperationCurrent();
|
|
793
|
+
await client._commitSeqTrackerState(ns);
|
|
794
|
+
}
|
|
795
|
+
committed = true;
|
|
796
|
+
}
|
|
797
|
+
catch (exc) {
|
|
798
|
+
if (this.isPullOperationCurrent() && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
799
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
800
|
+
this.dropSeqTrackerPending(ns);
|
|
801
|
+
}
|
|
802
|
+
throw exc;
|
|
803
|
+
}
|
|
804
|
+
this.ensurePullOperationCurrent();
|
|
805
|
+
const committedAck = client._seqTracker.getContiguousSeq(ns);
|
|
806
|
+
if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
|
|
807
|
+
coordinator.clearForwardCursor(ns, committedAck);
|
|
808
|
+
}
|
|
809
|
+
if (hasServerAck && parsedServerAck !== null && serverAckSeq > committedAck) {
|
|
810
|
+
coordinator.recordForwardCursor(ns, serverAckSeq, committedAck);
|
|
811
|
+
}
|
|
812
|
+
if (hasServerAck && parsedServerAck !== null && serverAckSeq >= deferredForwardAck) {
|
|
813
|
+
coordinator.clearForwardAck(ns, serverAckSeq);
|
|
814
|
+
if (method === 'message.pull') {
|
|
815
|
+
this.completePendingP2PInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
this.completePendingGroupInlineAckUpTo(ns, serverAckSeq, pageInlineGeneration);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const contigAdvanced = committedAck > pageContigBefore;
|
|
822
|
+
const knownServerAck = hasServerAck ? serverAckSeq : deferredServerCursor;
|
|
823
|
+
if (committedAck > 0
|
|
824
|
+
&& ((hasServerAck && committedAck > serverAckSeq)
|
|
825
|
+
|| (contigAdvanced && committedAck > knownServerAck))) {
|
|
826
|
+
coordinator.recordForwardAck(ns, committedAck);
|
|
827
|
+
}
|
|
828
|
+
const pendingAckSeq = Number(coordinator.pendingForwardAck(ns, committedAck) ?? 0);
|
|
829
|
+
if (pendingAckSeq > 0) {
|
|
830
|
+
const ackMethod = method === 'message.pull' ? 'message.ack' : 'group.ack_messages';
|
|
831
|
+
const clampedAckSeq = this.clampAckSeq(ackMethod, ackMethod === 'message.ack' ? 'seq' : 'msg_seq', ns, pendingAckSeq);
|
|
832
|
+
if (clampedAckSeq < pendingAckSeq) {
|
|
833
|
+
throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
|
|
834
|
+
}
|
|
835
|
+
this.ensurePullOperationCurrent();
|
|
836
|
+
await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
|
|
837
|
+
}
|
|
838
|
+
return { rawCount: messages.length, publishedCount };
|
|
839
|
+
}
|
|
502
840
|
enqueueOrderedMessage(ns, event, seq, payload) {
|
|
503
841
|
const client = this.runtime.client;
|
|
504
842
|
let queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -1432,55 +1770,47 @@ export class MessageDeliveryEngine {
|
|
|
1432
1770
|
}
|
|
1433
1771
|
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
1434
1772
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1435
|
-
|
|
1436
|
-
|
|
1773
|
+
if (client._v2Session) {
|
|
1774
|
+
try {
|
|
1437
1775
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1438
1776
|
await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
|
|
1439
1777
|
return;
|
|
1440
1778
|
}
|
|
1441
|
-
|
|
1779
|
+
catch (exc) {
|
|
1780
|
+
client._clientLog.warn(`auto pull group message failed:${String(exc)}`);
|
|
1781
|
+
await client._publishAppEvent('group.message_created', notification);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
let rawCompleted = false;
|
|
1786
|
+
try {
|
|
1787
|
+
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', {
|
|
1442
1788
|
group_id: groupId,
|
|
1443
1789
|
after_message_seq: afterSeq,
|
|
1444
|
-
device_id: client._deviceId,
|
|
1445
1790
|
limit: 50,
|
|
1446
|
-
_rpc_background: true,
|
|
1447
1791
|
});
|
|
1448
|
-
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
if (s !== undefined && s !== null
|
|
1462
|
-
&& this.recallEventFromGroupMessage(msg)) {
|
|
1463
|
-
await this.publishGroupRecallTombstone(groupId, s, msg);
|
|
1464
|
-
this.markPublishedSeq(ns, Number(s));
|
|
1465
|
-
continue;
|
|
1466
|
-
}
|
|
1467
|
-
if (s !== undefined && s !== null) {
|
|
1468
|
-
await client._publishPulledMessage('group.message_created', ns, s, msg);
|
|
1469
|
-
}
|
|
1470
|
-
else {
|
|
1471
|
-
await client._publishAppEvent('group.message_created', msg);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
this.prunePushedSeqs(ns);
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1792
|
+
await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
1793
|
+
const result = await client._rpcPipeline.rawCall('group.pull', {
|
|
1794
|
+
group_id: groupId,
|
|
1795
|
+
after_message_seq: afterSeq,
|
|
1796
|
+
device_id: client._deviceId,
|
|
1797
|
+
slot_id: client._slotId,
|
|
1798
|
+
limit: 50,
|
|
1799
|
+
_rpc_background: true,
|
|
1800
|
+
}, { background: true });
|
|
1801
|
+
rawCompleted = true;
|
|
1802
|
+
await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
|
|
1803
|
+
this.prunePushedSeqs(ns);
|
|
1804
|
+
}, true);
|
|
1479
1805
|
}
|
|
1480
1806
|
catch (exc) {
|
|
1481
|
-
|
|
1807
|
+
// 页发布或本地提交失败不能 ACK;提交后 ACK 失败则保留 pending。
|
|
1808
|
+
// 两种情况都不能再把原始通知当作第二条业务消息发布。
|
|
1809
|
+
client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
|
|
1810
|
+
if (!rawCompleted) {
|
|
1811
|
+
await client._publishAppEvent('group.message_created', notification);
|
|
1812
|
+
}
|
|
1482
1813
|
}
|
|
1483
|
-
await client._publishAppEvent('group.message_created', notification);
|
|
1484
1814
|
}
|
|
1485
1815
|
scheduleP2pGapRetry(ns, retryAttempt) {
|
|
1486
1816
|
const client = this.runtime.client;
|
|
@@ -1541,6 +1871,7 @@ export class MessageDeliveryEngine {
|
|
|
1541
1871
|
finally {
|
|
1542
1872
|
client._gapFillDone.delete(dedupKey);
|
|
1543
1873
|
this.runtime.delivery.setGapFillActive(false);
|
|
1874
|
+
this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
|
|
1544
1875
|
}
|
|
1545
1876
|
}
|
|
1546
1877
|
async fillP2pGapWithPlainPull(ns, afterSeq) {
|
|
@@ -1552,25 +1883,9 @@ export class MessageDeliveryEngine {
|
|
|
1552
1883
|
slot_id: client._slotId,
|
|
1553
1884
|
_rpc_background: true,
|
|
1554
1885
|
};
|
|
1555
|
-
const raw = await client.
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1558
|
-
: raw;
|
|
1559
|
-
const messages = (isJsonObject(result) && Array.isArray(result.messages))
|
|
1560
|
-
? result.messages
|
|
1561
|
-
: [];
|
|
1562
|
-
let published = 0;
|
|
1563
|
-
for (const item of messages) {
|
|
1564
|
-
if (!isJsonObject(item))
|
|
1565
|
-
continue;
|
|
1566
|
-
const msg = item;
|
|
1567
|
-
const seq = Number(msg.seq ?? 0);
|
|
1568
|
-
const appEvent = p2pAppEventFromPlainPullMessage(msg);
|
|
1569
|
-
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload)) {
|
|
1570
|
-
published += 1;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
return published;
|
|
1886
|
+
const raw = await client._rpcPipeline.rawCall('message.pull', params, { background: true });
|
|
1887
|
+
const page = await this.commitPlainForwardPage(ns, afterSeq, 'message.pull', raw);
|
|
1888
|
+
return page.publishedCount;
|
|
1574
1889
|
}
|
|
1575
1890
|
async fillGroupGap(groupId) {
|
|
1576
1891
|
const client = this.runtime.client;
|
|
@@ -1604,9 +1919,10 @@ export class MessageDeliveryEngine {
|
|
|
1604
1919
|
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
1605
1920
|
client._safeAsync(this.fillGroupGap(groupId));
|
|
1606
1921
|
}
|
|
1922
|
+
this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
|
|
1607
1923
|
}
|
|
1608
1924
|
}
|
|
1609
|
-
async fillGroupEventGap(groupId) {
|
|
1925
|
+
async fillGroupEventGap(groupId, options) {
|
|
1610
1926
|
const client = this.runtime.client;
|
|
1611
1927
|
if (client._sessionOptions?.background_sync === false)
|
|
1612
1928
|
return;
|
|
@@ -1616,25 +1932,31 @@ export class MessageDeliveryEngine {
|
|
|
1616
1932
|
if (!groupId)
|
|
1617
1933
|
return;
|
|
1618
1934
|
const ns = `group_event:${groupId}`;
|
|
1619
|
-
const
|
|
1935
|
+
const requestedAfterSeq = Number(options?.afterSeq ?? 0);
|
|
1936
|
+
const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
|
|
1937
|
+
? requestedAfterSeq
|
|
1938
|
+
: client._seqTracker.getContiguousSeq(ns);
|
|
1939
|
+
const singlePage = options?.singlePage === true;
|
|
1620
1940
|
const dedupKey = `group_event_pull:${ns}`;
|
|
1621
1941
|
if (client._gapFillDone.has(dedupKey))
|
|
1622
1942
|
return;
|
|
1623
1943
|
client._gapFillDone.add(dedupKey);
|
|
1624
1944
|
this.runtime.delivery.setGapFillActive(true);
|
|
1945
|
+
let continuationAfterSeq = 0;
|
|
1625
1946
|
try {
|
|
1626
1947
|
let nextAfterSeq = afterSeq;
|
|
1627
|
-
const maxPages = 100;
|
|
1948
|
+
const maxPages = singlePage ? 1 : 100;
|
|
1628
1949
|
let pageCount = 0;
|
|
1629
1950
|
while (pageCount < maxPages) {
|
|
1630
1951
|
pageCount += 1;
|
|
1631
|
-
const
|
|
1952
|
+
const request = {
|
|
1632
1953
|
group_id: groupId,
|
|
1633
1954
|
after_event_seq: nextAfterSeq,
|
|
1634
1955
|
device_id: client._deviceId,
|
|
1635
1956
|
limit: 50,
|
|
1636
1957
|
_rpc_background: true,
|
|
1637
|
-
}
|
|
1958
|
+
};
|
|
1959
|
+
const result = await client.call('group.pull_events', request);
|
|
1638
1960
|
if (!isJsonObject(result))
|
|
1639
1961
|
return;
|
|
1640
1962
|
const events = result.events;
|
|
@@ -1690,16 +2012,28 @@ export class MessageDeliveryEngine {
|
|
|
1690
2012
|
this.persistSeq(ns);
|
|
1691
2013
|
}
|
|
1692
2014
|
if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2015
|
+
try {
|
|
2016
|
+
const ackPromise = client._transport.call('group.ack_events', {
|
|
2017
|
+
group_id: groupId,
|
|
2018
|
+
event_seq: ackContig,
|
|
2019
|
+
device_id: client._deviceId,
|
|
2020
|
+
slot_id: client._slotId,
|
|
2021
|
+
_rpc_background: true,
|
|
2022
|
+
});
|
|
2023
|
+
if (singlePage)
|
|
2024
|
+
await ackPromise;
|
|
2025
|
+
else
|
|
2026
|
+
ackPromise.catch((e) => { client._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
|
|
2027
|
+
}
|
|
2028
|
+
catch (e) {
|
|
2029
|
+
client._clientLog.warn('group event auto-ack failed: group=' + groupId, e);
|
|
2030
|
+
}
|
|
1700
2031
|
}
|
|
1701
2032
|
const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
|
|
1702
|
-
if (
|
|
2033
|
+
if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
|
|
2034
|
+
continuationAfterSeq = nextAfter;
|
|
2035
|
+
}
|
|
2036
|
+
if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more !== true)
|
|
1703
2037
|
break;
|
|
1704
2038
|
nextAfterSeq = nextAfter;
|
|
1705
2039
|
}
|
|
@@ -1713,6 +2047,12 @@ export class MessageDeliveryEngine {
|
|
|
1713
2047
|
finally {
|
|
1714
2048
|
client._gapFillDone.delete(dedupKey);
|
|
1715
2049
|
this.runtime.delivery.setGapFillActive(false);
|
|
2050
|
+
if (singlePage && continuationAfterSeq > afterSeq) {
|
|
2051
|
+
this.enqueueOnlineUnreadEventHint({
|
|
2052
|
+
group_id: groupId,
|
|
2053
|
+
_online_hint_after_event_seq: continuationAfterSeq,
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
1716
2056
|
}
|
|
1717
2057
|
}
|
|
1718
2058
|
async handleGroupChangedEventSeq(data, groupId) {
|
|
@@ -1731,8 +2071,8 @@ export class MessageDeliveryEngine {
|
|
|
1731
2071
|
client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
|
|
1732
2072
|
return;
|
|
1733
2073
|
}
|
|
1734
|
-
client._clientLog.debug(`group.changed online unread hint triggering
|
|
1735
|
-
|
|
2074
|
+
client._clientLog.debug(`group.changed online unread hint triggering event Forward: group=${groupId}, event_seq=${eventSeq}`);
|
|
2075
|
+
this.enqueueOnlineUnreadEventHint(data);
|
|
1736
2076
|
return;
|
|
1737
2077
|
}
|
|
1738
2078
|
if (this.isSelfJoinGroupChanged(data)) {
|
|
@@ -1794,50 +2134,88 @@ export class MessageDeliveryEngine {
|
|
|
1794
2134
|
const groupId = String(data.group_id ?? '').trim();
|
|
1795
2135
|
if (!groupId)
|
|
1796
2136
|
return;
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
this.
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2137
|
+
const ns = `group:${groupId}`;
|
|
2138
|
+
const targets = this.onlineUnreadHintTargets ?? new Map();
|
|
2139
|
+
const owners = this.onlineUnreadHintOwners ?? new Set();
|
|
2140
|
+
this.onlineUnreadHintTargets = targets;
|
|
2141
|
+
this.onlineUnreadHintOwners = owners;
|
|
2142
|
+
targets.set(ns, Math.max(targets.get(ns) ?? 0, positiveSafeSequenceHint(data.seq)));
|
|
2143
|
+
if (owners.has(ns))
|
|
2144
|
+
return;
|
|
2145
|
+
owners.add(ns);
|
|
2146
|
+
client._safeAsync((async () => {
|
|
2147
|
+
try {
|
|
2148
|
+
while (true) {
|
|
2149
|
+
const throughSeq = targets.get(ns) ?? 0;
|
|
2150
|
+
const ackBefore = this.syncState(ns).ack;
|
|
2151
|
+
await this.runGroupForwardRecovery(groupId, ns, 50, true, throughSeq);
|
|
2152
|
+
const ackAfter = this.syncState(ns).ack;
|
|
2153
|
+
const target = targets.get(ns) ?? 0;
|
|
2154
|
+
if (ackAfter >= target || ackAfter <= ackBefore)
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
catch (exc) {
|
|
2159
|
+
client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2160
|
+
}
|
|
2161
|
+
finally {
|
|
2162
|
+
if (this.onlineUnreadHintTargets === targets)
|
|
2163
|
+
targets.delete(ns);
|
|
2164
|
+
if (this.onlineUnreadHintOwners === owners)
|
|
2165
|
+
owners.delete(ns);
|
|
2166
|
+
}
|
|
2167
|
+
})());
|
|
1805
2168
|
}
|
|
1806
|
-
|
|
2169
|
+
enqueueOnlineUnreadEventHint(data) {
|
|
1807
2170
|
const client = this.runtime.client;
|
|
1808
|
-
|
|
2171
|
+
const groupId = String(data.group_id ?? '').trim();
|
|
2172
|
+
if (!groupId || client._sessionOptions?.background_sync === false)
|
|
1809
2173
|
return;
|
|
1810
|
-
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2174
|
+
const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
|
|
2175
|
+
const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
|
|
2176
|
+
client._safeAsync(this.fillGroupEventGap(groupId, {
|
|
2177
|
+
afterSeq,
|
|
2178
|
+
singlePage: true,
|
|
2179
|
+
}));
|
|
2180
|
+
}
|
|
2181
|
+
async runP2PForwardRecovery(ns, pageLimit, background = false) {
|
|
2182
|
+
const client = this.runtime.client;
|
|
2183
|
+
const request = {
|
|
2184
|
+
after_seq: this.syncState(ns).ack,
|
|
2185
|
+
limit: pageLimit,
|
|
2186
|
+
max_pages: 1,
|
|
2187
|
+
};
|
|
2188
|
+
const invoke = async () => {
|
|
2189
|
+
const after = this.syncState(ns).ack;
|
|
2190
|
+
const messages = await client._pullV2(after, pageLimit, { gateLocked: true, maxPages: 1 });
|
|
2191
|
+
return { messages, raw_count: messages.length };
|
|
2192
|
+
};
|
|
2193
|
+
const pipeline = client._rpcPipeline;
|
|
2194
|
+
if (!pipeline)
|
|
2195
|
+
return await invoke();
|
|
2196
|
+
const key = pipeline.pullGateKeyForCall('message.pull', request);
|
|
2197
|
+
return await pipeline.runPullSerialized(key, invoke, background);
|
|
2198
|
+
}
|
|
2199
|
+
async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
|
|
2200
|
+
const client = this.runtime.client;
|
|
2201
|
+
const state = this.syncState(ns);
|
|
2202
|
+
const maxPages = this.forwardMaxPages(state.ack, throughSeq, pageLimit);
|
|
2203
|
+
const request = {
|
|
2204
|
+
group_id: groupId,
|
|
2205
|
+
after_seq: state.ack,
|
|
2206
|
+
limit: pageLimit,
|
|
2207
|
+
max_pages: maxPages,
|
|
2208
|
+
};
|
|
2209
|
+
const invoke = async () => {
|
|
2210
|
+
const after = this.syncState(ns).ack;
|
|
2211
|
+
const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages });
|
|
2212
|
+
return { messages, raw_count: messages.length };
|
|
2213
|
+
};
|
|
2214
|
+
const pipeline = client._rpcPipeline;
|
|
2215
|
+
if (!pipeline)
|
|
2216
|
+
return await invoke();
|
|
2217
|
+
const key = pipeline.pullGateKeyForCall('group.pull', request);
|
|
2218
|
+
return await pipeline.runPullSerialized(key, invoke, background);
|
|
1841
2219
|
}
|
|
1842
2220
|
syncState(ns) {
|
|
1843
2221
|
const tracker = this.runtime.client._seqTracker;
|
|
@@ -1846,6 +2224,10 @@ export class MessageDeliveryEngine {
|
|
|
1846
2224
|
const ack = Number(tracker.getContiguousSeq(ns) || 0);
|
|
1847
2225
|
return { ack, tail: ack, head: Math.max(ack, Number(tracker.getMaxSeenSeq?.(ns) || 0)) };
|
|
1848
2226
|
}
|
|
2227
|
+
forwardMaxPages(ack, throughSeq, pageLimit) {
|
|
2228
|
+
const limit = Number.isSafeInteger(pageLimit) && pageLimit > 0 ? pageLimit : 1;
|
|
2229
|
+
return Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / limit));
|
|
2230
|
+
}
|
|
1849
2231
|
inlineMessage(data) {
|
|
1850
2232
|
if (!isJsonObject(data))
|
|
1851
2233
|
return null;
|
|
@@ -2253,8 +2635,10 @@ export class MessageDeliveryEngine {
|
|
|
2253
2635
|
}
|
|
2254
2636
|
}
|
|
2255
2637
|
hasOutstandingRealtimeGap(ns) {
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2638
|
+
const state = this.syncState(ns);
|
|
2639
|
+
// Tail 已建立可靠后缀窗口;后台续拉只针对尚未接上窗口的真实缺口。
|
|
2640
|
+
// maxSeen 可能只是 Tail 期间到达的通知,不能单独据此启动全量补洞。
|
|
2641
|
+
return state.ack < state.tail - 1;
|
|
2258
2642
|
}
|
|
2259
2643
|
scheduleP2PRealtimeFullGapFill(ns) {
|
|
2260
2644
|
const client = this.runtime.client;
|
|
@@ -2307,7 +2691,17 @@ export class MessageDeliveryEngine {
|
|
|
2307
2691
|
}
|
|
2308
2692
|
async runP2PRealtimeSync(ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
2309
2693
|
const client = this.runtime.client;
|
|
2310
|
-
const
|
|
2694
|
+
const before = this.syncState(ns);
|
|
2695
|
+
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
2696
|
+
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
2697
|
+
: (() => {
|
|
2698
|
+
const gap = pushSeq - before.ack;
|
|
2699
|
+
if (gap <= 0)
|
|
2700
|
+
return before.ack < before.tail - 1 ? ['forward'] : [];
|
|
2701
|
+
return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
|
|
2702
|
+
})();
|
|
2703
|
+
// Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
|
|
2704
|
+
const headForward = order[0] === 'forward' && pushSeq > before.ack;
|
|
2311
2705
|
let result = { messages: [], raw_count: 0 };
|
|
2312
2706
|
let tailCompleted = false;
|
|
2313
2707
|
const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
|
|
@@ -2322,7 +2716,7 @@ export class MessageDeliveryEngine {
|
|
|
2322
2716
|
if (tail) {
|
|
2323
2717
|
const tailResult = await client._v2E2EE.pullV2TailInternal({
|
|
2324
2718
|
window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
|
|
2325
|
-
});
|
|
2719
|
+
}, true);
|
|
2326
2720
|
tailCompleted = true;
|
|
2327
2721
|
return tailResult;
|
|
2328
2722
|
}
|
|
@@ -2341,22 +2735,46 @@ export class MessageDeliveryEngine {
|
|
|
2341
2735
|
// 落盘的 ack 为起点,至多补一页普通 Forward 即可恢复该小窗口。
|
|
2342
2736
|
const tailMissedDuringPull = tailCompleted
|
|
2343
2737
|
&& Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2738
|
+
const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
|
|
2739
|
+
const pendingThroughSeq = tailMissedDuringPull
|
|
2740
|
+
? maxSeen
|
|
2741
|
+
: 0;
|
|
2742
|
+
const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
|
|
2743
|
+
const backgroundWindowForward = order[0] === 'forward' && !headForward;
|
|
2744
|
+
if (headForward || tailForward
|
|
2745
|
+
|| (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
|
|
2746
|
+
const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
|
|
2747
|
+
const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
|
|
2748
|
+
result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
|
|
2749
|
+
if (pendingThroughSeq > 0) {
|
|
2750
|
+
const committedAck = this.syncState(ns).ack;
|
|
2751
|
+
if (committedAck >= pendingThroughSeq)
|
|
2752
|
+
this.consumePendingPull(ns, pendingThroughSeq);
|
|
2753
|
+
else if (committedAck <= state.ack)
|
|
2754
|
+
this.markPendingPullNoProgress(ns, committedAck);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
const finalState = this.syncState(ns);
|
|
2758
|
+
if (tailCompleted && finalState.ack < finalState.tail - 1) {
|
|
2352
2759
|
this.scheduleP2PRealtimeFullGapFill(ns);
|
|
2760
|
+
}
|
|
2353
2761
|
if (tailCompleted)
|
|
2354
2762
|
this.markRealtimeTailCompleted(result);
|
|
2355
2763
|
return result;
|
|
2356
2764
|
}
|
|
2357
2765
|
async runGroupRealtimeSync(groupId, ns, pushSeq, pageLimit = 50, forceTail = false) {
|
|
2358
2766
|
const client = this.runtime.client;
|
|
2359
|
-
const
|
|
2767
|
+
const before = this.syncState(ns);
|
|
2768
|
+
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
2769
|
+
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
2770
|
+
: (() => {
|
|
2771
|
+
const gap = pushSeq - before.ack;
|
|
2772
|
+
if (gap <= 0)
|
|
2773
|
+
return before.ack < before.tail - 1 ? ['forward'] : [];
|
|
2774
|
+
return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
|
|
2775
|
+
})();
|
|
2776
|
+
// Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
|
|
2777
|
+
const headForward = order[0] === 'forward' && pushSeq > before.ack;
|
|
2360
2778
|
let result = { messages: [], raw_count: 0 };
|
|
2361
2779
|
let tailCompleted = false;
|
|
2362
2780
|
const run = async (tail, background, maxPages, forceForward = false, bypassCoveredHead = false) => {
|
|
@@ -2371,7 +2789,7 @@ export class MessageDeliveryEngine {
|
|
|
2371
2789
|
if (tail) {
|
|
2372
2790
|
const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
|
|
2373
2791
|
group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
|
|
2374
|
-
});
|
|
2792
|
+
}, true);
|
|
2375
2793
|
tailCompleted = true;
|
|
2376
2794
|
return tailResult;
|
|
2377
2795
|
}
|
|
@@ -2389,31 +2807,33 @@ export class MessageDeliveryEngine {
|
|
|
2389
2807
|
// 期间新到达的序号;绝不重新规划或再执行 Tail。
|
|
2390
2808
|
const tailMissedDuringPull = tailCompleted
|
|
2391
2809
|
&& Number(client._seqTracker.getMaxSeenSeq(ns) || 0) > state.head;
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2810
|
+
const maxSeen = Number(client._seqTracker.getMaxSeenSeq(ns) || 0);
|
|
2811
|
+
const pendingThroughSeq = tailMissedDuringPull
|
|
2812
|
+
? maxSeen
|
|
2813
|
+
: 0;
|
|
2814
|
+
const tailForward = tailCompleted && (state.ack < state.tail - 1 || tailMissedDuringPull);
|
|
2815
|
+
const backgroundWindowForward = order[0] === 'forward' && !headForward;
|
|
2816
|
+
if (headForward || tailForward
|
|
2817
|
+
|| (client._sessionOptions?.background_sync !== false && backgroundWindowForward)) {
|
|
2818
|
+
const forwardTarget = Math.max(headForward ? Math.max(pushSeq, maxSeen) : 0, (tailForward || backgroundWindowForward) ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
|
|
2819
|
+
const forwardMaxPages = this.forwardMaxPages(state.ack, forwardTarget, pageLimit);
|
|
2820
|
+
result = await run(false, !(headForward || tailForward), forwardMaxPages, headForward || tailMissedDuringPull);
|
|
2821
|
+
if (pendingThroughSeq > 0) {
|
|
2822
|
+
const committedAck = this.syncState(ns).ack;
|
|
2823
|
+
if (committedAck >= pendingThroughSeq)
|
|
2824
|
+
this.consumePendingPull(ns, pendingThroughSeq);
|
|
2825
|
+
else if (committedAck <= state.ack)
|
|
2826
|
+
this.markPendingPullNoProgress(ns, committedAck);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
const finalState = this.syncState(ns);
|
|
2830
|
+
if (tailCompleted && finalState.ack < finalState.tail - 1) {
|
|
2400
2831
|
this.scheduleGroupRealtimeFullGapFill(groupId, ns);
|
|
2832
|
+
}
|
|
2401
2833
|
if (tailCompleted)
|
|
2402
2834
|
this.markRealtimeTailCompleted(result);
|
|
2403
2835
|
return result;
|
|
2404
2836
|
}
|
|
2405
|
-
async runGroupOnlineUnreadHintForward(groupId, ns, pageLimit = 50) {
|
|
2406
|
-
const client = this.runtime.client;
|
|
2407
|
-
const request = { group_id: groupId, after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
|
|
2408
|
-
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
2409
|
-
return await client._rpcPipeline.runPullSerialized(key, async () => {
|
|
2410
|
-
const live = this.syncState(ns);
|
|
2411
|
-
const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
|
|
2412
|
-
gateLocked: true, maxPages: 1,
|
|
2413
|
-
});
|
|
2414
|
-
return { messages, raw_count: messages.length };
|
|
2415
|
-
}, true);
|
|
2416
|
-
}
|
|
2417
2837
|
async recoverInvalidP2PRealtimeHead(ns, rawSeq) {
|
|
2418
2838
|
const client = this.runtime.client;
|
|
2419
2839
|
client._clientLog.warn(`V2 P2P inline Head has invalid seq; forcing authoritative Tail: seq=${String(rawSeq)}`);
|
|
@@ -2483,21 +2903,9 @@ export class MessageDeliveryEngine {
|
|
|
2483
2903
|
const eventKind = String(data.kind ?? '').trim();
|
|
2484
2904
|
// Hint 是恢复信号而不是消息 Head:不得消费 inline、推进水位或直接 ACK。
|
|
2485
2905
|
if (eventKind === 'group.online_unread_hint') {
|
|
2486
|
-
if (!data._online_hint_drained) {
|
|
2487
|
-
if (client._sessionOptions?.background_sync === false)
|
|
2488
|
-
return;
|
|
2489
|
-
this.enqueueOnlineUnreadHint(data);
|
|
2490
|
-
return;
|
|
2491
|
-
}
|
|
2492
2906
|
if (client._sessionOptions?.background_sync === false)
|
|
2493
2907
|
return;
|
|
2494
|
-
|
|
2495
|
-
// 只从已提交 Ack 做一页普通 Forward,不能由 planner 把 hint 当 Tail 证据。
|
|
2496
|
-
await this.runGroupOnlineUnreadHintForward(groupId, `group:${groupId}`);
|
|
2497
|
-
}
|
|
2498
|
-
catch (exc) {
|
|
2499
|
-
client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2500
|
-
}
|
|
2908
|
+
this.enqueueOnlineUnreadHint(data);
|
|
2501
2909
|
return;
|
|
2502
2910
|
}
|
|
2503
2911
|
const ns = this.resolvePendingGroupInlineAckNamespace(`group:${groupId}`);
|
|
@@ -2527,30 +2935,33 @@ export class MessageDeliveryEngine {
|
|
|
2527
2935
|
// inline 绑定仍要求原始 seq 是严格 number,避免类型归一化后消费正文。
|
|
2528
2936
|
if (strictSeq || !inlinePresent)
|
|
2529
2937
|
client._seqTracker.updateMaxSeen(ns, seq);
|
|
2530
|
-
if (
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
|
|
2539
|
-
}
|
|
2540
|
-
return;
|
|
2938
|
+
if (seq <= before.ack && before.ack >= before.tail - 1) {
|
|
2939
|
+
const canonicalNs = this.resolvePendingGroupInlineAckNamespace(ns);
|
|
2940
|
+
const pending = this.pendingGroupInlineAcks?.get(canonicalNs);
|
|
2941
|
+
if (pending && seq >= pending.seq) {
|
|
2942
|
+
this.retryPendingGroupInlineAck(canonicalNs, pending.seq, 'covered inline Group ack', seq);
|
|
2943
|
+
}
|
|
2944
|
+
else if (!inlinePresent || inlineMatches) {
|
|
2945
|
+
this.fireGroupV2Ack(groupId, ns, seq, 'group v2 covered push ack', inlineMatches ? inlineGeneration : null);
|
|
2541
2946
|
}
|
|
2947
|
+
return;
|
|
2542
2948
|
}
|
|
2543
|
-
|
|
2949
|
+
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
2950
|
+
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
2951
|
+
this.recordPendingPull(ns, seq);
|
|
2952
|
+
client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
|
|
2544
2953
|
return;
|
|
2954
|
+
}
|
|
2545
2955
|
try {
|
|
2546
2956
|
// namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
|
|
2547
|
-
await this.runGroupRealtimeSync(groupId, ns, seq, 50,
|
|
2957
|
+
await this.runGroupRealtimeSync(groupId, ns, seq, 50, false);
|
|
2548
2958
|
}
|
|
2549
2959
|
catch (exc) {
|
|
2550
2960
|
client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2551
2961
|
}
|
|
2552
2962
|
finally {
|
|
2553
2963
|
this.endRealtimeSync(ns);
|
|
2964
|
+
this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
2554
2965
|
}
|
|
2555
2966
|
}
|
|
2556
2967
|
async onV2PushNotification(data) {
|
|
@@ -2558,6 +2969,12 @@ export class MessageDeliveryEngine {
|
|
|
2558
2969
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
2559
2970
|
if (!ns)
|
|
2560
2971
|
return;
|
|
2972
|
+
if (isJsonObject(data) && String(data.kind ?? '').trim() === 'message.online_unread_hint') {
|
|
2973
|
+
if (client._sessionOptions?.background_sync === false)
|
|
2974
|
+
return;
|
|
2975
|
+
this.enqueueOnlineUnreadP2pHint(data);
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2561
2978
|
const pushSeq = isJsonObject(data) && typeof data.seq === 'number' ? data.seq : 0;
|
|
2562
2979
|
if (client._v2Session && Number.isSafeInteger(pushSeq) && pushSeq > 0) {
|
|
2563
2980
|
// 与 Group 相同,在排队前暴露更高 Head 给当前在途 Tail。
|
|
@@ -2606,30 +3023,42 @@ export class MessageDeliveryEngine {
|
|
|
2606
3023
|
const before = this.syncState(ns);
|
|
2607
3024
|
if (strictSeq || !inlinePresent)
|
|
2608
3025
|
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
2609
|
-
if (
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
|
|
2614
|
-
}
|
|
2615
|
-
else if (inlineMatches) {
|
|
2616
|
-
this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
|
|
2617
|
-
}
|
|
2618
|
-
return;
|
|
3026
|
+
if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
|
|
3027
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
3028
|
+
if (pending && pushSeq >= pending.seq) {
|
|
3029
|
+
this.retryPendingP2PInlineAck(ns, pending.seq, 'covered inline P2P ack', pushSeq);
|
|
2619
3030
|
}
|
|
3031
|
+
else if (inlineMatches) {
|
|
3032
|
+
this.fireP2PV2Ack(pushSeq, 'P2P inline covered push ack', inlineGeneration);
|
|
3033
|
+
}
|
|
3034
|
+
return;
|
|
2620
3035
|
}
|
|
2621
|
-
|
|
3036
|
+
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
3037
|
+
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
3038
|
+
this.recordPendingPull(ns, pushSeq);
|
|
3039
|
+
client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
|
|
2622
3040
|
return;
|
|
3041
|
+
}
|
|
2623
3042
|
try {
|
|
2624
|
-
await this.runP2PRealtimeSync(ns, pushSeq, 50,
|
|
3043
|
+
await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
|
|
2625
3044
|
}
|
|
2626
3045
|
catch (exc) {
|
|
2627
3046
|
client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
|
|
2628
3047
|
}
|
|
2629
3048
|
finally {
|
|
2630
3049
|
this.endRealtimeSync(ns);
|
|
3050
|
+
this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
2631
3051
|
}
|
|
2632
3052
|
}
|
|
3053
|
+
enqueueOnlineUnreadP2pHint(data) {
|
|
3054
|
+
const client = this.runtime.client;
|
|
3055
|
+
if (!client._aid || !client._v2Session)
|
|
3056
|
+
return;
|
|
3057
|
+
const ns = `p2p:${client._aid}`;
|
|
3058
|
+
client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
|
|
3059
|
+
client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
3060
|
+
}));
|
|
3061
|
+
}
|
|
2633
3062
|
async restoreSeqTrackerState() {
|
|
2634
3063
|
const client = this.runtime.client;
|
|
2635
3064
|
if (!client._aid)
|
|
@@ -2745,8 +3174,14 @@ export class MessageDeliveryEngine {
|
|
|
2745
3174
|
if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
|
|
2746
3175
|
return;
|
|
2747
3176
|
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
2748
|
-
|
|
2749
|
-
|
|
3177
|
+
const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
|
|
3178
|
+
try {
|
|
3179
|
+
await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
|
|
3180
|
+
}
|
|
3181
|
+
catch (exc) {
|
|
3182
|
+
this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
|
|
3183
|
+
throw exc;
|
|
3184
|
+
}
|
|
2750
3185
|
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
2751
3186
|
}
|
|
2752
3187
|
async flushSeqTrackerPending() {
|
|
@@ -2773,6 +3208,34 @@ export class MessageDeliveryEngine {
|
|
|
2773
3208
|
this.cancelSeqTrackerFlushTimer();
|
|
2774
3209
|
}
|
|
2775
3210
|
}
|
|
3211
|
+
takeSeqTrackerPending(context, ns) {
|
|
3212
|
+
const client = this.runtime.client;
|
|
3213
|
+
if (client._seqTrackerPendingPersistContext !== context)
|
|
3214
|
+
return null;
|
|
3215
|
+
const pending = this.seqTrackerPending();
|
|
3216
|
+
const state = pending.get(ns);
|
|
3217
|
+
if (!state)
|
|
3218
|
+
return null;
|
|
3219
|
+
pending.delete(ns);
|
|
3220
|
+
if (pending.size === 0) {
|
|
3221
|
+
client._seqTrackerPendingPersistContext = null;
|
|
3222
|
+
this.cancelSeqTrackerFlushTimer();
|
|
3223
|
+
}
|
|
3224
|
+
return { ...state };
|
|
3225
|
+
}
|
|
3226
|
+
restoreSeqTrackerPending(context, ns, state) {
|
|
3227
|
+
if (!state || this.currentSeqTrackerContext() !== context)
|
|
3228
|
+
return;
|
|
3229
|
+
const client = this.runtime.client;
|
|
3230
|
+
const pendingContext = client._seqTrackerPendingPersistContext;
|
|
3231
|
+
if (pendingContext && pendingContext !== context)
|
|
3232
|
+
return;
|
|
3233
|
+
const pending = this.seqTrackerPending();
|
|
3234
|
+
if (!pending.has(ns))
|
|
3235
|
+
pending.set(ns, state);
|
|
3236
|
+
client._seqTrackerPendingPersistContext = context;
|
|
3237
|
+
this.scheduleSeqTrackerFlush();
|
|
3238
|
+
}
|
|
2776
3239
|
currentSeqTrackerContext() {
|
|
2777
3240
|
const client = this.runtime.client;
|
|
2778
3241
|
if (!client._aid)
|
|
@@ -2823,6 +3286,14 @@ export class MessageDeliveryEngine {
|
|
|
2823
3286
|
}
|
|
2824
3287
|
}
|
|
2825
3288
|
async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
3289
|
+
await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
|
|
3290
|
+
}
|
|
3291
|
+
enqueueSeqTrackerWrite(operation) {
|
|
3292
|
+
const run = this.seqTrackerWriteChain.then(operation, operation);
|
|
3293
|
+
this.seqTrackerWriteChain = run.catch(() => { });
|
|
3294
|
+
return run;
|
|
3295
|
+
}
|
|
3296
|
+
async writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError = false) {
|
|
2826
3297
|
const client = this.runtime.client;
|
|
2827
3298
|
if (!aid || Object.keys(state).length === 0)
|
|
2828
3299
|
return;
|
|
@@ -2865,41 +3336,56 @@ export class MessageDeliveryEngine {
|
|
|
2865
3336
|
const client = this.runtime.client;
|
|
2866
3337
|
if (!client._aid || !ns)
|
|
2867
3338
|
return;
|
|
3339
|
+
const aid = client._aid;
|
|
3340
|
+
const deviceId = client._deviceId;
|
|
3341
|
+
const slotId = client._slotId;
|
|
2868
3342
|
const seq = client._seqTracker.getContiguousSeq(ns);
|
|
2869
3343
|
this.dropSeqTrackerPending(ns);
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3344
|
+
const saveSeq = client._tokenStore.saveSeq;
|
|
3345
|
+
if (seq > 0 && typeof saveSeq === 'function') {
|
|
3346
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3347
|
+
try {
|
|
3348
|
+
await saveSeq.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
|
|
3349
|
+
}
|
|
3350
|
+
catch (exc) {
|
|
2873
3351
|
client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2874
|
-
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3352
|
+
}
|
|
3353
|
+
});
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
const deleteSeq = client._tokenStore.deleteSeq;
|
|
3357
|
+
if (seq <= 0 && typeof deleteSeq === 'function') {
|
|
3358
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3359
|
+
try {
|
|
3360
|
+
await deleteSeq.call(client._tokenStore, aid, deviceId, slotId, ns);
|
|
3361
|
+
}
|
|
3362
|
+
catch (exc) {
|
|
2880
3363
|
client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
const updater = client._tokenStore.updateInstanceState;
|
|
3369
|
+
if (seq <= 0 && typeof updater === 'function') {
|
|
3370
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3371
|
+
try {
|
|
3372
|
+
await updater.call(client._tokenStore, aid, deviceId, slotId, (current) => {
|
|
3373
|
+
if (isJsonObject(current.seq_tracker_state)) {
|
|
3374
|
+
const next = { ...current.seq_tracker_state };
|
|
3375
|
+
delete next[ns];
|
|
3376
|
+
current.seq_tracker_state = next;
|
|
3377
|
+
}
|
|
3378
|
+
return current;
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
catch (exc) {
|
|
2893
3382
|
client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
if (seq > 0) {
|
|
2898
|
-
void this.saveSeqTrackerState();
|
|
2899
|
-
}
|
|
3383
|
+
}
|
|
3384
|
+
});
|
|
3385
|
+
return;
|
|
2900
3386
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
3387
|
+
if (seq > 0) {
|
|
3388
|
+
void this.saveSeqTrackerState();
|
|
2903
3389
|
}
|
|
2904
3390
|
}
|
|
2905
3391
|
clampAckSeq(method, field, ns, seq) {
|
|
@@ -2960,8 +3446,9 @@ export class MessageDeliveryEngine {
|
|
|
2960
3446
|
}
|
|
2961
3447
|
return params;
|
|
2962
3448
|
}
|
|
2963
|
-
async drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
|
|
3449
|
+
async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
|
|
2964
3450
|
const client = this.runtime.client;
|
|
3451
|
+
this.ensurePullOperationCurrent();
|
|
2965
3452
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
2966
3453
|
if (!queue || queue.size === 0)
|
|
2967
3454
|
return;
|
|
@@ -2971,18 +3458,22 @@ export class MessageDeliveryEngine {
|
|
|
2971
3458
|
.sort((a, b) => a - b);
|
|
2972
3459
|
let delivered = false;
|
|
2973
3460
|
for (const seq of ready) {
|
|
3461
|
+
this.ensurePullOperationCurrent();
|
|
2974
3462
|
const item = queue.get(seq);
|
|
2975
3463
|
queue.delete(seq);
|
|
2976
3464
|
if (!item || client._pushedSeqs.get(ns)?.has(seq))
|
|
2977
3465
|
continue;
|
|
2978
3466
|
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse);
|
|
3467
|
+
this.ensurePullOperationCurrent();
|
|
2979
3468
|
this.markPublishedSeq(ns, seq);
|
|
2980
3469
|
delivered = true;
|
|
2981
3470
|
}
|
|
2982
3471
|
if (queue.size === 0) {
|
|
2983
3472
|
client._pendingOrderedMsgs.delete(ns);
|
|
2984
|
-
if (delivered)
|
|
3473
|
+
if (delivered && persist) {
|
|
3474
|
+
this.ensurePullOperationCurrent();
|
|
2985
3475
|
await this.saveSeqTrackerState();
|
|
3476
|
+
}
|
|
2986
3477
|
}
|
|
2987
3478
|
}
|
|
2988
3479
|
async publishOrderedMessage(event, ns, seq, payload) {
|
|
@@ -3018,14 +3509,18 @@ export class MessageDeliveryEngine {
|
|
|
3018
3509
|
await this.saveSeqTrackerState();
|
|
3019
3510
|
return true;
|
|
3020
3511
|
}
|
|
3021
|
-
async publishPulledMessage(event, ns, seq, payload) {
|
|
3512
|
+
async publishPulledMessage(event, ns, seq, payload, persist = true) {
|
|
3022
3513
|
const client = this.runtime.client;
|
|
3514
|
+
this.ensurePullOperationCurrent();
|
|
3023
3515
|
const seqNum = Number(seq);
|
|
3024
3516
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
|
|
3025
3517
|
if (event === 'message.recalled') {
|
|
3026
|
-
|
|
3518
|
+
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload));
|
|
3519
|
+
this.ensurePullOperationCurrent();
|
|
3520
|
+
return published;
|
|
3027
3521
|
}
|
|
3028
3522
|
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
3523
|
+
this.ensurePullOperationCurrent();
|
|
3029
3524
|
return true;
|
|
3030
3525
|
}
|
|
3031
3526
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -3035,16 +3530,19 @@ export class MessageDeliveryEngine {
|
|
|
3035
3530
|
client._pendingOrderedMsgs.delete(ns);
|
|
3036
3531
|
return false;
|
|
3037
3532
|
}
|
|
3038
|
-
await this.drainOrderedMessages(ns, seqNum);
|
|
3533
|
+
await this.drainOrderedMessages(ns, seqNum, false, persist);
|
|
3534
|
+
this.ensurePullOperationCurrent();
|
|
3039
3535
|
queue?.delete(seqNum);
|
|
3040
3536
|
if (queue && queue.size === 0)
|
|
3041
3537
|
client._pendingOrderedMsgs.delete(ns);
|
|
3042
3538
|
if (event === 'message.recalled') {
|
|
3043
3539
|
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload));
|
|
3540
|
+
this.ensurePullOperationCurrent();
|
|
3044
3541
|
this.markPublishedSeq(ns, seqNum);
|
|
3045
3542
|
return published;
|
|
3046
3543
|
}
|
|
3047
3544
|
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload));
|
|
3545
|
+
this.ensurePullOperationCurrent();
|
|
3048
3546
|
this.markPublishedSeq(ns, seqNum);
|
|
3049
3547
|
return true;
|
|
3050
3548
|
}
|