@agentunion/fastaun 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 +106 -0
- package/_packed_docs/CHANGELOG.md +106 -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 +16 -0
- package/dist/agent-md-schema.js +109 -0
- package/dist/agent-md-schema.js.map +1 -0
- package/dist/agent-md.d.ts +7 -0
- package/dist/agent-md.js +91 -9
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +5 -1
- package/dist/aid-store.js +25 -6
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -0
- package/dist/aid.js +32 -12
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +65 -13
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +2 -1
- package/dist/cert-utils.js +32 -16
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +36 -7
- package/dist/client/delivery.js +701 -216
- 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.js +475 -152
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +24 -6
- package/dist/client/rpc-pipeline.js +372 -131
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/runtime.d.ts +1 -3
- package/dist/client/runtime.js +4 -7
- package/dist/client/runtime.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -3
- package/dist/client/v2-e2ee.js +376 -93
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -10
- package/dist/client.js +515 -150
- package/dist/client.js.map +1 -1
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +3 -0
- package/dist/discovery.js.map +1 -1
- package/dist/facades.d.ts +23 -1
- package/dist/facades.js +59 -7
- package/dist/facades.js.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.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/net.d.ts +3 -0
- package/dist/net.js +13 -0
- package/dist/net.js.map +1 -1
- package/dist/seq-tracker.js +6 -1
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +29 -3
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.js +8 -1
- package/dist/transport.js.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 +4 -3
package/dist/client/delivery.js
CHANGED
|
@@ -37,23 +37,235 @@ function isPromiseLike(value) {
|
|
|
37
37
|
function formatDeliveryError(error) {
|
|
38
38
|
return error instanceof Error ? error : String(error);
|
|
39
39
|
}
|
|
40
|
+
function positiveSafeSequenceHint(value) {
|
|
41
|
+
if (typeof value === 'number')
|
|
42
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
43
|
+
if (typeof value !== 'string' || !value || value.trim() !== value)
|
|
44
|
+
return 0;
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
47
|
+
}
|
|
48
|
+
function nonNegativeSafeSequenceHint(value) {
|
|
49
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
50
|
+
}
|
|
51
|
+
function deduplicatePlainForwardPageMessages(value, afterSeq) {
|
|
52
|
+
if (!Array.isArray(value))
|
|
53
|
+
return [];
|
|
54
|
+
const bySeq = new Map();
|
|
55
|
+
for (const raw of value) {
|
|
56
|
+
if (!isJsonObject(raw))
|
|
57
|
+
continue;
|
|
58
|
+
const seq = positiveSafeSequenceHint(raw.seq);
|
|
59
|
+
if (seq <= afterSeq || bySeq.has(seq))
|
|
60
|
+
continue;
|
|
61
|
+
bySeq.set(seq, { ...raw, seq });
|
|
62
|
+
}
|
|
63
|
+
return [...bySeq.entries()].sort(([left], [right]) => left - right).map(([, message]) => message);
|
|
64
|
+
}
|
|
40
65
|
export class MessageDeliveryEngine {
|
|
41
66
|
runtime;
|
|
42
67
|
realtimeTailHeads = null;
|
|
43
68
|
realtimeSyncing = null;
|
|
69
|
+
pendingGroupPullUpper = null;
|
|
70
|
+
onlineUnreadHintTargets = null;
|
|
71
|
+
onlineUnreadHintTasks = null;
|
|
44
72
|
pendingP2PInlineAcks = null;
|
|
45
73
|
pendingGroupInlineAcks = null;
|
|
46
74
|
pendingGroupInlineAckNamespaces = null;
|
|
47
75
|
inlineAckGeneration = 0;
|
|
76
|
+
// 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
|
|
77
|
+
seqTrackerWriteChain = Promise.resolve();
|
|
48
78
|
constructor(runtime) {
|
|
49
79
|
this.runtime = runtime;
|
|
50
80
|
}
|
|
81
|
+
isPullOperationCurrent() {
|
|
82
|
+
const client = this.runtime.client;
|
|
83
|
+
const generation = client._pullOperationGeneration;
|
|
84
|
+
if (generation === undefined)
|
|
85
|
+
return true;
|
|
86
|
+
const pipeline = client._rpcPipeline;
|
|
87
|
+
return typeof pipeline?.isPullGenerationCurrent === 'function'
|
|
88
|
+
? pipeline.isPullGenerationCurrent(generation)
|
|
89
|
+
: true;
|
|
90
|
+
}
|
|
91
|
+
ensurePullOperationCurrent() {
|
|
92
|
+
if (!this.isPullOperationCurrent())
|
|
93
|
+
throw new Error('pull invalidated');
|
|
94
|
+
}
|
|
95
|
+
forwardCoordinator() {
|
|
96
|
+
const coordinator = this.runtime.client._v2E2EE;
|
|
97
|
+
if (!coordinator
|
|
98
|
+
|| typeof coordinator.pendingForwardCursor !== 'function'
|
|
99
|
+
|| typeof coordinator.recordForwardCursor !== 'function'
|
|
100
|
+
|| typeof coordinator.clearForwardCursor !== 'function'
|
|
101
|
+
|| typeof coordinator.pendingForwardAck !== 'function'
|
|
102
|
+
|| typeof coordinator.recordForwardAck !== 'function'
|
|
103
|
+
|| typeof coordinator.clearForwardAck !== 'function') {
|
|
104
|
+
throw new Error('Forward cursor coordinator is unavailable');
|
|
105
|
+
}
|
|
106
|
+
return coordinator;
|
|
107
|
+
}
|
|
108
|
+
async confirmPlainForwardAck(ns, method, ackSeq, groupId = '') {
|
|
109
|
+
const client = this.runtime.client;
|
|
110
|
+
this.ensurePullOperationCurrent();
|
|
111
|
+
const coordinator = this.forwardCoordinator();
|
|
112
|
+
const generation = this.captureInlineAckGeneration();
|
|
113
|
+
coordinator.recordForwardAck(ns, ackSeq);
|
|
114
|
+
const params = method === 'message.ack'
|
|
115
|
+
? {
|
|
116
|
+
seq: ackSeq,
|
|
117
|
+
device_id: client._deviceId,
|
|
118
|
+
slot_id: client._slotId,
|
|
119
|
+
_rpc_background: true,
|
|
120
|
+
}
|
|
121
|
+
: {
|
|
122
|
+
group_id: groupId,
|
|
123
|
+
msg_seq: ackSeq,
|
|
124
|
+
device_id: client._deviceId,
|
|
125
|
+
slot_id: client._slotId,
|
|
126
|
+
_rpc_background: true,
|
|
127
|
+
};
|
|
128
|
+
const result = await client._rpcPipeline.rawCall(method, params, { background: true });
|
|
129
|
+
this.ensurePullOperationCurrent();
|
|
130
|
+
const actualAckSeq = this.resolveForwardAckSeq(result, ackSeq);
|
|
131
|
+
if (actualAckSeq < ackSeq) {
|
|
132
|
+
throw new Error(`${method} server ACK watermark ${actualAckSeq} is below requested ${ackSeq}`);
|
|
133
|
+
}
|
|
134
|
+
coordinator.clearForwardAck(ns, actualAckSeq);
|
|
135
|
+
if (method === 'message.ack') {
|
|
136
|
+
this.onP2PV2AckSucceeded(actualAckSeq, generation);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async commitPlainForwardPage(ns, afterSeq, method, result, groupId = '') {
|
|
143
|
+
if (!isJsonObject(result)) {
|
|
144
|
+
throw new Error(`${method} response must be an object`);
|
|
145
|
+
}
|
|
146
|
+
const client = this.runtime.client;
|
|
147
|
+
this.ensurePullOperationCurrent();
|
|
148
|
+
const response = result;
|
|
149
|
+
const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
|
|
150
|
+
const coordinator = this.forwardCoordinator();
|
|
151
|
+
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
152
|
+
const pageInlineGeneration = this.captureInlineAckGeneration();
|
|
153
|
+
const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
|
|
154
|
+
const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
|
|
155
|
+
const pageTrackerSnapshot = typeof client._seqTracker.snapshotNamespace === 'function'
|
|
156
|
+
? client._seqTracker.snapshotNamespace(ns)
|
|
157
|
+
: null;
|
|
158
|
+
response._contig_before = pageContigBefore;
|
|
159
|
+
const cursor = isJsonObject(response.cursor)
|
|
160
|
+
? response.cursor
|
|
161
|
+
: null;
|
|
162
|
+
const serverAckRaw = method === 'message.pull' ? response.server_ack_seq : cursor?.current_seq;
|
|
163
|
+
const hasServerAck = method === 'message.pull'
|
|
164
|
+
? Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')
|
|
165
|
+
: Boolean(cursor && Object.prototype.hasOwnProperty.call(cursor, 'current_seq'));
|
|
166
|
+
const parsedServerAck = hasServerAck ? nonNegativeSafeSequenceHint(serverAckRaw) : null;
|
|
167
|
+
const serverAckSeq = parsedServerAck ?? 0;
|
|
168
|
+
const retentionFloor = method === 'message.pull'
|
|
169
|
+
? positiveSafeSequenceHint(response.retention_floor_seq)
|
|
170
|
+
: Math.max(positiveSafeSequenceHint(response.retention_floor_message_seq), positiveSafeSequenceHint(response.retention_floor_seq), positiveSafeSequenceHint(cursor?.retention_floor_seq));
|
|
171
|
+
const visibilityFloor = method === 'group.pull'
|
|
172
|
+
? Math.max(positiveSafeSequenceHint(cursor?.join_seq), Math.max(0, positiveSafeSequenceHint(response.earliest_available_message_seq) - 1))
|
|
173
|
+
: 0;
|
|
174
|
+
let publishedCount = 0;
|
|
175
|
+
let committed = false;
|
|
176
|
+
try {
|
|
177
|
+
for (const rawMessage of messages) {
|
|
178
|
+
this.ensurePullOperationCurrent();
|
|
179
|
+
const seq = positiveSafeSequenceHint(rawMessage.seq);
|
|
180
|
+
if (method === 'message.pull') {
|
|
181
|
+
const appEvent = this.p2pAppEventForMessage(rawMessage);
|
|
182
|
+
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
183
|
+
publishedCount += 1;
|
|
184
|
+
}
|
|
185
|
+
this.ensurePullOperationCurrent();
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const message = normalizeGroupMentionMode(rawMessage);
|
|
189
|
+
if (this.recallEventFromGroupMessage(message)) {
|
|
190
|
+
if (await this.publishGroupRecallTombstone(groupId, seq, message)) {
|
|
191
|
+
this.ensurePullOperationCurrent();
|
|
192
|
+
this.markPublishedSeq(ns, seq);
|
|
193
|
+
publishedCount += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
|
|
197
|
+
this.ensurePullOperationCurrent();
|
|
198
|
+
publishedCount += 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.ensurePullOperationCurrent();
|
|
202
|
+
if (messages.length > 0)
|
|
203
|
+
client._seqTracker.onPullResult(ns, messages, afterSeq);
|
|
204
|
+
const commitTarget = Math.max(client._seqTracker.getContiguousSeq(ns), retentionFloor, visibilityFloor, deferredServerCursor);
|
|
205
|
+
if (commitTarget > client._seqTracker.getContiguousSeq(ns)) {
|
|
206
|
+
client._seqTracker.forceContiguousSeq(ns, commitTarget);
|
|
207
|
+
}
|
|
208
|
+
if (client._seqTracker.getContiguousSeq(ns) !== pageContigBefore) {
|
|
209
|
+
this.ensurePullOperationCurrent();
|
|
210
|
+
await this.drainOrderedMessages(ns, undefined, false, false);
|
|
211
|
+
this.ensurePullOperationCurrent();
|
|
212
|
+
await client._commitSeqTrackerState(ns);
|
|
213
|
+
}
|
|
214
|
+
committed = true;
|
|
215
|
+
}
|
|
216
|
+
catch (exc) {
|
|
217
|
+
if (this.isPullOperationCurrent() && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
218
|
+
client._seqTracker.restoreNamespaceSnapshot(ns, pageTrackerSnapshot);
|
|
219
|
+
this.dropSeqTrackerPending(ns);
|
|
220
|
+
}
|
|
221
|
+
throw exc;
|
|
222
|
+
}
|
|
223
|
+
this.ensurePullOperationCurrent();
|
|
224
|
+
const committedAck = client._seqTracker.getContiguousSeq(ns);
|
|
225
|
+
if (deferredServerCursor > 0 && committedAck >= deferredServerCursor) {
|
|
226
|
+
coordinator.clearForwardCursor(ns, committedAck);
|
|
227
|
+
}
|
|
228
|
+
if (hasServerAck && parsedServerAck !== null && serverAckSeq > committedAck) {
|
|
229
|
+
coordinator.recordForwardCursor(ns, serverAckSeq, committedAck);
|
|
230
|
+
}
|
|
231
|
+
if (hasServerAck && parsedServerAck !== null && serverAckSeq >= deferredForwardAck) {
|
|
232
|
+
coordinator.clearForwardAck(ns, serverAckSeq);
|
|
233
|
+
if (method === 'message.pull') {
|
|
234
|
+
this.onP2PV2AckSucceeded(serverAckSeq, pageInlineGeneration);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this.onGroupV2AckSucceeded(groupId, serverAckSeq, pageInlineGeneration);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const contigAdvanced = committedAck > pageContigBefore;
|
|
241
|
+
const knownServerAck = hasServerAck ? serverAckSeq : deferredServerCursor;
|
|
242
|
+
if (committedAck > 0
|
|
243
|
+
&& ((hasServerAck && committedAck > serverAckSeq)
|
|
244
|
+
|| (contigAdvanced && committedAck > knownServerAck))) {
|
|
245
|
+
coordinator.recordForwardAck(ns, committedAck);
|
|
246
|
+
}
|
|
247
|
+
const pendingAckSeq = Number(coordinator.pendingForwardAck(ns, committedAck) ?? 0);
|
|
248
|
+
if (pendingAckSeq > 0) {
|
|
249
|
+
const ackMethod = method === 'message.pull' ? 'message.ack' : 'group.ack_messages';
|
|
250
|
+
const clampedAckSeq = this.clampAckSeq(ackMethod, ackMethod === 'message.ack' ? 'seq' : 'msg_seq', ns, pendingAckSeq);
|
|
251
|
+
if (clampedAckSeq < pendingAckSeq) {
|
|
252
|
+
throw new Error(`${ackMethod} cannot confirm pending Forward watermark ${pendingAckSeq}`);
|
|
253
|
+
}
|
|
254
|
+
this.ensurePullOperationCurrent();
|
|
255
|
+
await this.confirmPlainForwardAck(ns, ackMethod, pendingAckSeq, groupId);
|
|
256
|
+
}
|
|
257
|
+
return { rawCount: messages.length, publishedCount };
|
|
258
|
+
}
|
|
51
259
|
resetInlineAckState() {
|
|
52
260
|
this.inlineAckGeneration += 1;
|
|
261
|
+
void this.runtime.client._rpcPipeline?.invalidatePulls?.();
|
|
53
262
|
this.pendingP2PInlineAcks = null;
|
|
54
263
|
this.pendingGroupInlineAcks = null;
|
|
55
264
|
this.pendingGroupInlineAckNamespaces = null;
|
|
56
265
|
this.realtimeSyncing = null;
|
|
266
|
+
this.pendingGroupPullUpper = null;
|
|
267
|
+
this.onlineUnreadHintTargets = null;
|
|
268
|
+
this.onlineUnreadHintTasks = null;
|
|
57
269
|
this.realtimeTailHeads = null;
|
|
58
270
|
}
|
|
59
271
|
inlineGenerationIsCurrent(generation) {
|
|
@@ -68,6 +280,15 @@ export class MessageDeliveryEngine {
|
|
|
68
280
|
captureGroupInlineAckGeneration() {
|
|
69
281
|
return this.pendingGroupInlineAcks?.size ? this.inlineAckGeneration : null;
|
|
70
282
|
}
|
|
283
|
+
async waitForPendingInlineCommit(ns, seq) {
|
|
284
|
+
const pending = this.pendingP2PInlineAcks?.get(ns);
|
|
285
|
+
if (pending && (pending.committed || pending.seq > seq))
|
|
286
|
+
return;
|
|
287
|
+
const queues = this.runtime.client._pushProcessQueues;
|
|
288
|
+
const processing = queues?.get(ns);
|
|
289
|
+
if (processing)
|
|
290
|
+
await processing;
|
|
291
|
+
}
|
|
71
292
|
pendingInlineAckNamespaceCount() {
|
|
72
293
|
return (this.pendingP2PInlineAcks?.size ?? 0) + (this.pendingGroupInlineAcks?.size ?? 0);
|
|
73
294
|
}
|
|
@@ -90,6 +311,50 @@ export class MessageDeliveryEngine {
|
|
|
90
311
|
endRealtimeSync(ns) {
|
|
91
312
|
this.realtimeSyncing?.delete(ns);
|
|
92
313
|
}
|
|
314
|
+
hasPendingPull(ns) {
|
|
315
|
+
return (this.pendingGroupPullUpper?.get(ns) ?? 0) > 0;
|
|
316
|
+
}
|
|
317
|
+
onPullGateIdle(ns) {
|
|
318
|
+
if (!ns)
|
|
319
|
+
return;
|
|
320
|
+
if (ns.startsWith('p2p:')) {
|
|
321
|
+
this.runtime.client._schedulePendingP2pPullIfNeeded?.(ns, 'pull-gate-idle');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.schedulePendingGroupPullIfNeeded(ns, 'pull-gate-idle');
|
|
325
|
+
}
|
|
326
|
+
recordPendingGroupPull(ns, seq) {
|
|
327
|
+
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
328
|
+
return;
|
|
329
|
+
const pending = this.pendingGroupPullUpper ?? new Map();
|
|
330
|
+
this.pendingGroupPullUpper = pending;
|
|
331
|
+
pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
|
|
332
|
+
}
|
|
333
|
+
schedulePendingGroupPullIfNeeded(ns, reason) {
|
|
334
|
+
const pending = this.pendingGroupPullUpper;
|
|
335
|
+
if (!pending)
|
|
336
|
+
return false;
|
|
337
|
+
const upper = pending.get(ns) ?? 0;
|
|
338
|
+
if (upper <= 0) {
|
|
339
|
+
pending.delete(ns);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
const client = this.runtime.client;
|
|
343
|
+
const contiguous = client._seqTracker.getContiguousSeq(ns);
|
|
344
|
+
if (upper <= contiguous) {
|
|
345
|
+
pending.delete(ns);
|
|
346
|
+
client._clientLog.debug(`Group pending pull upper already covered: ns=${ns}, upper_seq=${upper}, contiguous=${contiguous}, reason=${reason}`);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (client.state !== ConnectionState.READY || client._closing
|
|
350
|
+
|| client._rpcPipeline?.hasPullActivity?.(ns, false))
|
|
351
|
+
return false;
|
|
352
|
+
pending.delete(ns);
|
|
353
|
+
const groupId = ns.slice('group:'.length);
|
|
354
|
+
client._clientLog.debug(`Group pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upper}, reason=${reason}`);
|
|
355
|
+
client._safeAsync(this.onRawGroupV2MessageCreated({ group_id: groupId, seq: upper }));
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
93
358
|
prunePushedSeqs(ns) {
|
|
94
359
|
const client = this.runtime.client;
|
|
95
360
|
const pushed = client._pushedSeqs.get(ns);
|
|
@@ -132,12 +397,15 @@ export class MessageDeliveryEngine {
|
|
|
132
397
|
}
|
|
133
398
|
async publishOrderedQueueItem(ns, event, seq, payload, source, pullResponse = false) {
|
|
134
399
|
const client = this.runtime.client;
|
|
400
|
+
this.ensurePullOperationCurrent();
|
|
135
401
|
if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
|
|
136
402
|
await this.publishOrderedGroupChanged(payload, source);
|
|
403
|
+
this.ensurePullOperationCurrent();
|
|
137
404
|
return;
|
|
138
405
|
}
|
|
139
406
|
if (event === 'message.recalled') {
|
|
140
407
|
await this.publishMessageRecallTombstone(seq, payload);
|
|
408
|
+
this.ensurePullOperationCurrent();
|
|
141
409
|
return;
|
|
142
410
|
}
|
|
143
411
|
if (pullResponse) {
|
|
@@ -150,6 +418,7 @@ export class MessageDeliveryEngine {
|
|
|
150
418
|
if (isPromiseLike(published))
|
|
151
419
|
await published;
|
|
152
420
|
}
|
|
421
|
+
this.ensurePullOperationCurrent();
|
|
153
422
|
}
|
|
154
423
|
async publishOrderedGroupChanged(payload, source = 'ordered') {
|
|
155
424
|
const client = this.runtime.client;
|
|
@@ -925,7 +1194,7 @@ export class MessageDeliveryEngine {
|
|
|
925
1194
|
// 在旧 seq tracking / ordered publish 之前转入前台 Tail 协调器。
|
|
926
1195
|
const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
927
1196
|
const legacyPushSeq = this.realtimeHeadSeq(msg.seq, inlinePresent);
|
|
928
|
-
if (client._v2Session && client._aid && msg.seq !== undefined && msg.seq !== null) {
|
|
1197
|
+
if (client._v2Session && client._aid && (msg.seq !== undefined && msg.seq !== null || inlinePresent)) {
|
|
929
1198
|
if (legacyPushSeq === null) {
|
|
930
1199
|
if (inlinePresent)
|
|
931
1200
|
await this.recoverInvalidP2PRealtimeHead(`p2p:${client._aid}`, msg.seq);
|
|
@@ -1025,7 +1294,7 @@ export class MessageDeliveryEngine {
|
|
|
1025
1294
|
// 不能让 payload 路径直接推进旧 ordered publish 状态。
|
|
1026
1295
|
const inlinePresent = Object.prototype.hasOwnProperty.call(msg, 'inline_message');
|
|
1027
1296
|
const legacyPushSeq = this.realtimeHeadSeq(seq, inlinePresent);
|
|
1028
|
-
if (client._v2Session && groupId && seq !== undefined && seq !== null) {
|
|
1297
|
+
if (client._v2Session && groupId && (seq !== undefined && seq !== null || inlinePresent)) {
|
|
1029
1298
|
if (legacyPushSeq === null) {
|
|
1030
1299
|
if (inlinePresent)
|
|
1031
1300
|
await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, seq);
|
|
@@ -1139,20 +1408,14 @@ export class MessageDeliveryEngine {
|
|
|
1139
1408
|
client._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
|
|
1140
1409
|
let filled = 0;
|
|
1141
1410
|
try {
|
|
1142
|
-
|
|
1411
|
+
// Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
|
|
1412
|
+
// 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
|
|
1413
|
+
const operation = () => client._pullV2(afterSeq, 50, { gateLocked: true });
|
|
1143
1414
|
const messages = client._rpcPipeline
|
|
1144
1415
|
? await client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), operation, true)
|
|
1145
1416
|
: await client._withBackgroundRpc(operation);
|
|
1146
1417
|
filled = messages.length;
|
|
1147
1418
|
this.prunePushedSeqs(ns);
|
|
1148
|
-
if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
|
|
1149
|
-
await this.drainOrderedMessages(ns, undefined, true);
|
|
1150
|
-
this.persistSeq(ns);
|
|
1151
|
-
}
|
|
1152
|
-
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
1153
|
-
if (contig > 0 && contig !== afterSeq) {
|
|
1154
|
-
await client._withBackgroundRpc(() => client._ackV2(contig));
|
|
1155
|
-
}
|
|
1156
1419
|
client._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
|
|
1157
1420
|
}
|
|
1158
1421
|
catch (exc) {
|
|
@@ -1160,6 +1423,7 @@ export class MessageDeliveryEngine {
|
|
|
1160
1423
|
}
|
|
1161
1424
|
finally {
|
|
1162
1425
|
client._gapFillDone.delete(dedupKey);
|
|
1426
|
+
client._schedulePendingP2pPullIfNeeded?.(ns, 'gap-fill-complete');
|
|
1163
1427
|
}
|
|
1164
1428
|
}
|
|
1165
1429
|
async fillGroupGap(groupId) {
|
|
@@ -1184,10 +1448,6 @@ export class MessageDeliveryEngine {
|
|
|
1184
1448
|
: await client._withBackgroundRpc(operation);
|
|
1185
1449
|
filled = messages.length;
|
|
1186
1450
|
this.prunePushedSeqs(ns);
|
|
1187
|
-
if (client._seqTracker.getContiguousSeq(ns) !== afterSeq) {
|
|
1188
|
-
await this.drainOrderedMessages(ns, undefined, true);
|
|
1189
|
-
this.persistSeq(ns);
|
|
1190
|
-
}
|
|
1191
1451
|
client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
|
|
1192
1452
|
}
|
|
1193
1453
|
catch (exc) {
|
|
@@ -1198,9 +1458,41 @@ export class MessageDeliveryEngine {
|
|
|
1198
1458
|
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
1199
1459
|
void this.fillGroupGap(groupId);
|
|
1200
1460
|
}
|
|
1461
|
+
this.schedulePendingGroupPullIfNeeded(ns, 'gap-fill-complete');
|
|
1201
1462
|
}
|
|
1202
1463
|
}
|
|
1203
|
-
|
|
1464
|
+
scheduleRealtimeWindowForward(ns, groupId = '') {
|
|
1465
|
+
const client = this.runtime.client;
|
|
1466
|
+
const state = this.syncState(ns);
|
|
1467
|
+
if (client._sessionOptions?.background_sync === false
|
|
1468
|
+
|| state.ack >= state.tail - 1)
|
|
1469
|
+
return;
|
|
1470
|
+
groupId = String(groupId ?? '').trim();
|
|
1471
|
+
if (!groupId && (!client._aid || ns !== `p2p:${client._aid}`))
|
|
1472
|
+
return;
|
|
1473
|
+
const key = `${groupId ? 'group' : 'p2p'}_realtime_window_forward:${ns}`;
|
|
1474
|
+
if (client._gapFillDone.has(key))
|
|
1475
|
+
return;
|
|
1476
|
+
client._gapFillDone.set(key, Date.now());
|
|
1477
|
+
client._safeAsync(Promise.resolve().then(async () => {
|
|
1478
|
+
try {
|
|
1479
|
+
if (client._sessionOptions?.background_sync === false
|
|
1480
|
+
|| client.state !== ConnectionState.READY || client._closing)
|
|
1481
|
+
return;
|
|
1482
|
+
const live = this.syncState(ns);
|
|
1483
|
+
if (live.ack >= live.tail - 1)
|
|
1484
|
+
return;
|
|
1485
|
+
if (groupId)
|
|
1486
|
+
await this.fillGroupGap(groupId);
|
|
1487
|
+
else
|
|
1488
|
+
await this.fillP2pGap();
|
|
1489
|
+
}
|
|
1490
|
+
finally {
|
|
1491
|
+
client._gapFillDone.delete(key);
|
|
1492
|
+
}
|
|
1493
|
+
}));
|
|
1494
|
+
}
|
|
1495
|
+
async fillGroupEventGap(groupId, options = {}) {
|
|
1204
1496
|
const client = this.runtime.client;
|
|
1205
1497
|
if (client._sessionOptions?.background_sync === false)
|
|
1206
1498
|
return;
|
|
@@ -1208,32 +1500,49 @@ export class MessageDeliveryEngine {
|
|
|
1208
1500
|
if (!groupId)
|
|
1209
1501
|
return;
|
|
1210
1502
|
const ns = `group_event:${groupId}`;
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1503
|
+
const requestedAfterSeq = Number(options.afterSeq ?? 0);
|
|
1504
|
+
const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
|
|
1505
|
+
? requestedAfterSeq
|
|
1506
|
+
: client._seqTracker.getContiguousSeq(ns);
|
|
1507
|
+
const singlePage = options.singlePage === true;
|
|
1508
|
+
const dedupKey = `group_event_pull:${ns}`;
|
|
1213
1509
|
if (client._gapFillDone.has(dedupKey))
|
|
1214
1510
|
return;
|
|
1215
|
-
|
|
1216
|
-
if (
|
|
1217
|
-
|
|
1218
|
-
|
|
1511
|
+
let legacyGateToken = null;
|
|
1512
|
+
if (!client._rpcPipeline) {
|
|
1513
|
+
const acquire = client._tryAcquirePullGate;
|
|
1514
|
+
if (typeof acquire === 'function') {
|
|
1515
|
+
legacyGateToken = acquire.call(client, ns);
|
|
1516
|
+
if (legacyGateToken === null)
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1219
1519
|
}
|
|
1220
1520
|
client._gapFillDone.set(dedupKey, Date.now());
|
|
1221
1521
|
let filled = 0;
|
|
1522
|
+
let continuationAfterSeq = 0;
|
|
1222
1523
|
try {
|
|
1223
1524
|
let nextAfterSeq = afterSeq;
|
|
1224
|
-
const maxPages = 100;
|
|
1525
|
+
const maxPages = singlePage ? 1 : 100;
|
|
1225
1526
|
let pageCount = 0;
|
|
1226
1527
|
while (pageCount < maxPages) {
|
|
1227
1528
|
pageCount += 1;
|
|
1228
1529
|
client._clientLog.debug(`group event gap fill start: group=${groupId}, after_seq=${nextAfterSeq}`);
|
|
1229
|
-
const
|
|
1530
|
+
const request = {
|
|
1230
1531
|
group_id: groupId,
|
|
1231
1532
|
after_event_seq: nextAfterSeq,
|
|
1232
1533
|
device_id: client._deviceId,
|
|
1233
1534
|
limit: 50,
|
|
1234
1535
|
_pull_gate_locked: true,
|
|
1235
1536
|
_rpc_background: true,
|
|
1236
|
-
}
|
|
1537
|
+
};
|
|
1538
|
+
let result;
|
|
1539
|
+
if (client._rpcPipeline) {
|
|
1540
|
+
const pullKey = client._rpcPipeline.pullGateKeyForCall('group.pull_events', request);
|
|
1541
|
+
result = await client._rpcPipeline.runPullSerialized(pullKey, () => client.call('group.pull_events', request), true);
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
result = await client.call('group.pull_events', request);
|
|
1545
|
+
}
|
|
1237
1546
|
if (!isJsonObject(result))
|
|
1238
1547
|
return;
|
|
1239
1548
|
const events = result.events;
|
|
@@ -1293,16 +1602,28 @@ export class MessageDeliveryEngine {
|
|
|
1293
1602
|
}
|
|
1294
1603
|
if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
|
|
1295
1604
|
const ackSeq = this.clampAckSeq('group.ack_events', 'event_seq', ns, ackContig);
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1605
|
+
try {
|
|
1606
|
+
const ackPromise = client._transport.call('group.ack_events', {
|
|
1607
|
+
group_id: groupId,
|
|
1608
|
+
event_seq: ackSeq,
|
|
1609
|
+
device_id: client._deviceId,
|
|
1610
|
+
slot_id: client._slotId,
|
|
1611
|
+
}, undefined, undefined, true);
|
|
1612
|
+
if (singlePage)
|
|
1613
|
+
await ackPromise;
|
|
1614
|
+
else
|
|
1615
|
+
ackPromise.catch((e) => {
|
|
1616
|
+
client._clientLog.debug(`group event auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
catch (e) {
|
|
1302
1620
|
client._clientLog.debug(`group event auto-ack failed: group=${groupId} ${formatDeliveryError(e)}`);
|
|
1303
|
-
}
|
|
1621
|
+
}
|
|
1304
1622
|
}
|
|
1305
1623
|
const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
|
|
1624
|
+
if (singlePage && result.has_more === true && nextAfter > nextAfterSeq) {
|
|
1625
|
+
continuationAfterSeq = nextAfter;
|
|
1626
|
+
}
|
|
1306
1627
|
if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
1307
1628
|
break;
|
|
1308
1629
|
nextAfterSeq = nextAfter;
|
|
@@ -1317,12 +1638,32 @@ export class MessageDeliveryEngine {
|
|
|
1317
1638
|
}
|
|
1318
1639
|
finally {
|
|
1319
1640
|
client._gapFillDone.delete(dedupKey);
|
|
1320
|
-
client._releasePullGate
|
|
1321
|
-
|
|
1641
|
+
if (legacyGateToken !== null && typeof client._releasePullGate === 'function') {
|
|
1642
|
+
client._releasePullGate(ns, legacyGateToken);
|
|
1643
|
+
}
|
|
1644
|
+
if (singlePage && continuationAfterSeq > afterSeq) {
|
|
1645
|
+
this.enqueueOnlineUnreadEventHint({
|
|
1646
|
+
group_id: groupId,
|
|
1647
|
+
_online_hint_after_event_seq: continuationAfterSeq,
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
else if (!singlePage && filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
1322
1651
|
void this.fillGroupEventGap(groupId);
|
|
1323
1652
|
}
|
|
1324
1653
|
}
|
|
1325
1654
|
}
|
|
1655
|
+
enqueueOnlineUnreadEventHint(data) {
|
|
1656
|
+
const client = this.runtime.client;
|
|
1657
|
+
const groupId = String(data.group_id ?? '').trim();
|
|
1658
|
+
if (!groupId || client._sessionOptions?.background_sync === false)
|
|
1659
|
+
return;
|
|
1660
|
+
const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
|
|
1661
|
+
const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
|
|
1662
|
+
client._safeAsync(this.fillGroupEventGap(groupId, {
|
|
1663
|
+
afterSeq,
|
|
1664
|
+
singlePage: true,
|
|
1665
|
+
}));
|
|
1666
|
+
}
|
|
1326
1667
|
async handleGroupChangedEventSeq(data, groupId) {
|
|
1327
1668
|
const client = this.runtime.client;
|
|
1328
1669
|
groupId = String(groupId ?? '').trim();
|
|
@@ -1339,10 +1680,8 @@ export class MessageDeliveryEngine {
|
|
|
1339
1680
|
client._clientLog.debug(`group.changed online unread hint skipped: group=${groupId} background_sync=false`);
|
|
1340
1681
|
return;
|
|
1341
1682
|
}
|
|
1342
|
-
client._clientLog.debug(`group.changed online unread hint triggering
|
|
1343
|
-
this.
|
|
1344
|
-
client._clientLog.warn(`background gap fill trigger failed: ${formatDeliveryError(exc)}`);
|
|
1345
|
-
});
|
|
1683
|
+
client._clientLog.debug(`group.changed online unread hint triggering event Forward: group=${groupId}, event_seq=${eventSeq}`);
|
|
1684
|
+
this.enqueueOnlineUnreadEventHint(data);
|
|
1346
1685
|
return;
|
|
1347
1686
|
}
|
|
1348
1687
|
if (this.isSelfJoinGroupChanged(data)) {
|
|
@@ -1606,16 +1945,49 @@ export class MessageDeliveryEngine {
|
|
|
1606
1945
|
if (!this.pendingGroupInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
|
|
1607
1946
|
|| !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
1608
1947
|
return;
|
|
1609
|
-
|
|
1948
|
+
const actualAckSeq = this.resolveGroupPullAckSeq(result, requestedSeq);
|
|
1949
|
+
this.onGroupV2AckSucceeded(groupId, actualAckSeq, generation);
|
|
1950
|
+
}
|
|
1951
|
+
resolveGroupPullAckSeq(result, requestedSeq) {
|
|
1952
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
1953
|
+
return 0;
|
|
1610
1954
|
const response = isJsonObject(result) ? result : null;
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1955
|
+
if (!response || !Object.prototype.hasOwnProperty.call(response, 'cursor'))
|
|
1956
|
+
return 0;
|
|
1957
|
+
const cursor = response.cursor;
|
|
1958
|
+
const cursorObject = isJsonObject(cursor) ? cursor : null;
|
|
1959
|
+
if (!cursorObject || !Object.prototype.hasOwnProperty.call(cursorObject, 'current_seq'))
|
|
1960
|
+
return 0;
|
|
1961
|
+
const raw = cursorObject.current_seq;
|
|
1962
|
+
return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
|
|
1963
|
+
}
|
|
1964
|
+
resolveForwardAckSeq(result, requestedSeq) {
|
|
1965
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
1966
|
+
return 0;
|
|
1967
|
+
const response = isJsonObject(result) ? result : null;
|
|
1968
|
+
// 旧服务端成功响应可能不带累计水位字段;RPC 已成功,兼容按请求水位处理。
|
|
1969
|
+
if (!response)
|
|
1970
|
+
return requestedSeq;
|
|
1971
|
+
let raw;
|
|
1972
|
+
if (Object.prototype.hasOwnProperty.call(response, 'effective_ack_seq'))
|
|
1973
|
+
raw = response.effective_ack_seq;
|
|
1974
|
+
else if (Object.prototype.hasOwnProperty.call(response, 'ack_seq'))
|
|
1975
|
+
raw = response.ack_seq;
|
|
1976
|
+
else if (Object.prototype.hasOwnProperty.call(response, 'cursor')) {
|
|
1977
|
+
const cursor = response.cursor;
|
|
1978
|
+
const cursorObject = isJsonObject(cursor) ? cursor : null;
|
|
1979
|
+
if (cursorObject) {
|
|
1980
|
+
if (!Object.prototype.hasOwnProperty.call(cursorObject, 'current_seq'))
|
|
1981
|
+
return 0;
|
|
1982
|
+
raw = cursorObject.current_seq;
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
raw = cursor;
|
|
1986
|
+
}
|
|
1617
1987
|
}
|
|
1618
|
-
|
|
1988
|
+
else
|
|
1989
|
+
return requestedSeq;
|
|
1990
|
+
return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
|
|
1619
1991
|
}
|
|
1620
1992
|
resolveActualAckSeq(result, requestedSeq) {
|
|
1621
1993
|
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
@@ -1649,22 +2021,31 @@ export class MessageDeliveryEngine {
|
|
|
1649
2021
|
const before = this.syncState(ns);
|
|
1650
2022
|
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
1651
2023
|
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
1652
|
-
: (
|
|
2024
|
+
: (() => {
|
|
2025
|
+
const gap = pushSeq - before.ack;
|
|
2026
|
+
if (gap <= 0)
|
|
2027
|
+
return before.ack < before.tail - 1 ? ['forward'] : [];
|
|
2028
|
+
return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
|
|
2029
|
+
})();
|
|
2030
|
+
// Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
|
|
2031
|
+
const headForward = order.length === 1 && order[0] === 'forward' && pushSeq > before.ack;
|
|
1653
2032
|
let result = { messages: [], raw_count: 0 };
|
|
1654
2033
|
let tailHead = null;
|
|
1655
|
-
const run = async (request, background) => {
|
|
2034
|
+
const run = async (request, background, forceForward = false) => {
|
|
1656
2035
|
const invoke = async () => {
|
|
1657
2036
|
const live = this.syncState(ns);
|
|
1658
2037
|
if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
|
|
1659
2038
|
return { messages: [], raw_count: 0 };
|
|
1660
|
-
if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
|
|
2039
|
+
if (request.window_mode !== 'tail' && !forceForward && !headForward && live.ack >= live.tail - 1)
|
|
2040
|
+
return { messages: [], raw_count: 0 };
|
|
2041
|
+
if (request.window_mode !== 'tail' && headForward && live.ack >= pushSeq)
|
|
1661
2042
|
return { messages: [], raw_count: 0 };
|
|
1662
2043
|
if (request.window_mode === 'tail') {
|
|
1663
2044
|
const tailResult = await client._v2E2EE.pullV2TailInternal({
|
|
1664
2045
|
window_mode: 'tail',
|
|
1665
2046
|
after_seq: live.ack,
|
|
1666
2047
|
limit: pageLimit,
|
|
1667
|
-
});
|
|
2048
|
+
}, true);
|
|
1668
2049
|
tailHead = this.syncState(ns).head;
|
|
1669
2050
|
return tailResult;
|
|
1670
2051
|
}
|
|
@@ -1686,8 +2067,24 @@ export class MessageDeliveryEngine {
|
|
|
1686
2067
|
const state = this.syncState(ns);
|
|
1687
2068
|
const needsForward = state.ack < state.tail - 1;
|
|
1688
2069
|
const backgroundEnabled = client._sessionOptions?.background_sync !== false;
|
|
1689
|
-
|
|
1690
|
-
|
|
2070
|
+
// Tail 期间到达的新 Push 可能只更新 maxSeen 而没有扩展 Tail 窗口。
|
|
2071
|
+
const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
|
|
2072
|
+
const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
|
|
2073
|
+
const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
|
|
2074
|
+
const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
|
|
2075
|
+
|| (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
|
|
2076
|
+
const foregroundForward = headForward || tailForward;
|
|
2077
|
+
if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
|
|
2078
|
+
const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
|
|
2079
|
+
const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
|
|
2080
|
+
result = await run({
|
|
2081
|
+
after_seq: state.ack,
|
|
2082
|
+
limit: pageLimit,
|
|
2083
|
+
...(headForward ? { max_pages: Math.max(2, requiredPages) } : order[0] === 'tail' ? { max_pages: requiredPages } : {}),
|
|
2084
|
+
}, !foregroundForward, headForward || tailMissedDuringPull);
|
|
2085
|
+
}
|
|
2086
|
+
if (tailHead !== null && this.syncState(ns).ack < this.syncState(ns).tail - 1) {
|
|
2087
|
+
this.scheduleRealtimeWindowForward(ns);
|
|
1691
2088
|
}
|
|
1692
2089
|
if (tailHead !== null) {
|
|
1693
2090
|
this.markRealtimeTailCompleted(result, tailHead);
|
|
@@ -1699,15 +2096,24 @@ export class MessageDeliveryEngine {
|
|
|
1699
2096
|
const before = this.syncState(ns);
|
|
1700
2097
|
const order = forceTail ? ['tail'] : typeof client._seqTracker.planRealtimeSync === 'function'
|
|
1701
2098
|
? client._seqTracker.planRealtimeSync(ns, pushSeq, pageLimit)
|
|
1702
|
-
: (
|
|
2099
|
+
: (() => {
|
|
2100
|
+
const gap = pushSeq - before.ack;
|
|
2101
|
+
if (gap <= 0)
|
|
2102
|
+
return before.ack < before.tail - 1 ? ['forward'] : [];
|
|
2103
|
+
return gap <= pageLimit * 2 ? ['forward'] : ['tail'];
|
|
2104
|
+
})();
|
|
2105
|
+
// Planner 依据 S-A 选路;H 已覆盖 S 也不代表 A 已连续提交,仍需前台 Forward。
|
|
2106
|
+
const headForward = order.length === 1 && order[0] === 'forward' && pushSeq > before.ack;
|
|
1703
2107
|
let result = { messages: [], raw_count: 0 };
|
|
1704
2108
|
let tailHead = null;
|
|
1705
|
-
const run = async (request, background) => {
|
|
2109
|
+
const run = async (request, background, forceForward = false) => {
|
|
1706
2110
|
const invoke = async () => {
|
|
1707
2111
|
const live = this.syncState(ns);
|
|
1708
2112
|
if (request.window_mode === 'tail' && !forceTail && typeof client._seqTracker.getSyncState === 'function' && pushSeq <= live.head)
|
|
1709
2113
|
return { messages: [], raw_count: 0 };
|
|
1710
|
-
if (request.window_mode !== 'tail' && live.ack >= live.tail - 1)
|
|
2114
|
+
if (request.window_mode !== 'tail' && !forceForward && !headForward && live.ack >= live.tail - 1)
|
|
2115
|
+
return { messages: [], raw_count: 0 };
|
|
2116
|
+
if (request.window_mode !== 'tail' && headForward && live.ack >= pushSeq)
|
|
1711
2117
|
return { messages: [], raw_count: 0 };
|
|
1712
2118
|
if (request.window_mode === 'tail') {
|
|
1713
2119
|
const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
|
|
@@ -1715,7 +2121,7 @@ export class MessageDeliveryEngine {
|
|
|
1715
2121
|
window_mode: 'tail',
|
|
1716
2122
|
after_seq: live.ack,
|
|
1717
2123
|
limit: pageLimit,
|
|
1718
|
-
});
|
|
2124
|
+
}, true);
|
|
1719
2125
|
tailHead = this.syncState(ns).head;
|
|
1720
2126
|
return tailResult;
|
|
1721
2127
|
}
|
|
@@ -1736,15 +2142,30 @@ export class MessageDeliveryEngine {
|
|
|
1736
2142
|
const state = this.syncState(ns);
|
|
1737
2143
|
const needsForward = state.ack < state.tail - 1;
|
|
1738
2144
|
const backgroundEnabled = client._sessionOptions?.background_sync !== false;
|
|
1739
|
-
|
|
1740
|
-
|
|
2145
|
+
const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
|
|
2146
|
+
const tailMissedDuringPull = tailHead !== null && maxSeen > state.head;
|
|
2147
|
+
const tailForward = tailHead !== null && (needsForward || tailMissedDuringPull);
|
|
2148
|
+
const backgroundWindowForward = (order.length === 1 && order[0] === 'forward' && !headForward)
|
|
2149
|
+
|| (order.length === 1 && order[0] === 'tail' && tailHead === null && needsForward);
|
|
2150
|
+
const foregroundForward = headForward || tailForward;
|
|
2151
|
+
if (foregroundForward || backgroundEnabled && backgroundWindowForward) {
|
|
2152
|
+
const forwardTarget = Math.max(headForward ? pushSeq : 0, tailForward || backgroundWindowForward ? state.tail - 1 : 0, tailMissedDuringPull ? maxSeen : 0);
|
|
2153
|
+
const requiredPages = Math.max(1, Math.ceil(Math.max(0, forwardTarget - state.ack) / pageLimit));
|
|
2154
|
+
result = await run({
|
|
2155
|
+
after_seq: state.ack,
|
|
2156
|
+
limit: pageLimit,
|
|
2157
|
+
...(headForward ? { max_pages: Math.max(2, requiredPages) } : order[0] === 'tail' ? { max_pages: requiredPages } : {}),
|
|
2158
|
+
}, !foregroundForward, headForward || tailMissedDuringPull);
|
|
2159
|
+
}
|
|
2160
|
+
if (tailHead !== null && this.syncState(ns).ack < this.syncState(ns).tail - 1) {
|
|
2161
|
+
this.scheduleRealtimeWindowForward(ns, groupId);
|
|
1741
2162
|
}
|
|
1742
2163
|
if (tailHead !== null) {
|
|
1743
2164
|
this.markRealtimeTailCompleted(result, tailHead);
|
|
1744
2165
|
}
|
|
1745
2166
|
return result;
|
|
1746
2167
|
}
|
|
1747
|
-
async runP2PForwardRecovery(ns, pageLimit) {
|
|
2168
|
+
async runP2PForwardRecovery(ns, pageLimit, background = false) {
|
|
1748
2169
|
const client = this.runtime.client;
|
|
1749
2170
|
const request = { after_seq: this.syncState(ns).ack, limit: pageLimit, max_pages: 1 };
|
|
1750
2171
|
const invoke = async () => {
|
|
@@ -1755,20 +2176,22 @@ export class MessageDeliveryEngine {
|
|
|
1755
2176
|
if (!client._rpcPipeline)
|
|
1756
2177
|
return await invoke();
|
|
1757
2178
|
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
|
|
1758
|
-
return await client._rpcPipeline.runPullSerialized(key, invoke,
|
|
2179
|
+
return await client._rpcPipeline.runPullSerialized(key, invoke, background);
|
|
1759
2180
|
}
|
|
1760
|
-
async runGroupForwardRecovery(groupId, ns, pageLimit) {
|
|
2181
|
+
async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
|
|
1761
2182
|
const client = this.runtime.client;
|
|
1762
|
-
const
|
|
2183
|
+
const ack = this.syncState(ns).ack;
|
|
2184
|
+
const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
|
|
2185
|
+
const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
|
|
1763
2186
|
const invoke = async () => {
|
|
1764
2187
|
const after = this.syncState(ns).ack;
|
|
1765
|
-
const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages
|
|
2188
|
+
const messages = await client._pullGroupV2(groupId, after, pageLimit, { gateLocked: true, maxPages });
|
|
1766
2189
|
return { messages, raw_count: messages.length };
|
|
1767
2190
|
};
|
|
1768
2191
|
if (!client._rpcPipeline)
|
|
1769
2192
|
return await invoke();
|
|
1770
2193
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
1771
|
-
return await client._rpcPipeline.runPullSerialized(key, invoke,
|
|
2194
|
+
return await client._rpcPipeline.runPullSerialized(key, invoke, background);
|
|
1772
2195
|
}
|
|
1773
2196
|
syncState(ns) {
|
|
1774
2197
|
const tracker = this.runtime.client._seqTracker;
|
|
@@ -2131,7 +2554,7 @@ export class MessageDeliveryEngine {
|
|
|
2131
2554
|
}
|
|
2132
2555
|
if (!this.inlineGenerationIsCurrent(generation))
|
|
2133
2556
|
return false;
|
|
2134
|
-
client._commitSeqTrackerState(ns);
|
|
2557
|
+
await client._commitSeqTrackerState(ns);
|
|
2135
2558
|
if (!this.inlineGenerationIsCurrent(generation))
|
|
2136
2559
|
return false;
|
|
2137
2560
|
this.markPublishedSeq(ns, seq);
|
|
@@ -2306,13 +2729,18 @@ export class MessageDeliveryEngine {
|
|
|
2306
2729
|
if (!this.pendingP2PInlineAcks?.size || !this.inlineGenerationIsCurrent(generation)
|
|
2307
2730
|
|| !Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
2308
2731
|
return;
|
|
2309
|
-
|
|
2732
|
+
const actualAckSeq = this.resolveP2PPullAckSeq(result, requestedSeq);
|
|
2733
|
+
this.onP2PV2AckSucceeded(actualAckSeq, generation);
|
|
2734
|
+
}
|
|
2735
|
+
resolveP2PPullAckSeq(result, requestedSeq) {
|
|
2736
|
+
if (!Number.isSafeInteger(requestedSeq) || requestedSeq <= 0)
|
|
2737
|
+
return 0;
|
|
2310
2738
|
const response = isJsonObject(result) ? result : null;
|
|
2311
2739
|
if (response && Object.prototype.hasOwnProperty.call(response, 'server_ack_seq')) {
|
|
2312
2740
|
const raw = response.server_ack_seq;
|
|
2313
|
-
|
|
2741
|
+
return typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : 0;
|
|
2314
2742
|
}
|
|
2315
|
-
|
|
2743
|
+
return 0;
|
|
2316
2744
|
}
|
|
2317
2745
|
async onV2PushNotification(data, expectedVersion, inlineGeneration) {
|
|
2318
2746
|
const client = this.runtime.client;
|
|
@@ -2325,10 +2753,16 @@ export class MessageDeliveryEngine {
|
|
|
2325
2753
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
2326
2754
|
if (!pushData || !ns)
|
|
2327
2755
|
return;
|
|
2756
|
+
if (String(pushData.kind ?? '').trim() === 'message.online_unread_hint') {
|
|
2757
|
+
if (client._sessionOptions?.background_sync === false)
|
|
2758
|
+
return;
|
|
2759
|
+
this.enqueueOnlineUnreadP2pHint(pushData);
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2328
2762
|
const inlinePresent = Object.prototype.hasOwnProperty.call(pushData, 'inline_message');
|
|
2329
2763
|
const pushSeq = this.realtimeHeadSeq(rawPushSeq, inlinePresent);
|
|
2330
2764
|
if (pushSeq === null) {
|
|
2331
|
-
if (inlinePresent
|
|
2765
|
+
if (inlinePresent) {
|
|
2332
2766
|
await this.recoverInvalidP2PRealtimeHead(ns, rawPushSeq);
|
|
2333
2767
|
}
|
|
2334
2768
|
return;
|
|
@@ -2350,34 +2784,26 @@ export class MessageDeliveryEngine {
|
|
|
2350
2784
|
catch (exc) {
|
|
2351
2785
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2352
2786
|
return;
|
|
2353
|
-
client._clientLog.warn(`V2 P2P inline push rejected, falling back to
|
|
2787
|
+
client._clientLog.warn(`V2 P2P inline push rejected, falling back to sync planner: seq=${pushSeq} err=${formatDeliveryError(exc)}`);
|
|
2354
2788
|
}
|
|
2355
2789
|
const before = this.syncState(ns);
|
|
2356
2790
|
client._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
2357
|
-
|
|
2358
|
-
if (pushSeq <= before.head && !hasGap && !inlinePresent) {
|
|
2791
|
+
if (pushSeq <= before.ack && before.ack >= before.tail - 1) {
|
|
2359
2792
|
client._clientLog.debug(`V2 P2P duplicate push ignored: seq=${pushSeq} A/T/H=${JSON.stringify(before)}`);
|
|
2360
2793
|
this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
|
|
2361
2794
|
return;
|
|
2362
2795
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2796
|
+
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
2797
|
+
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
2798
|
+
client._recordPendingP2pPull(ns, pushSeq);
|
|
2799
|
+
client._clientLog.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
|
|
2365
2800
|
return;
|
|
2366
2801
|
}
|
|
2367
2802
|
try {
|
|
2368
|
-
|
|
2803
|
+
await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
|
|
2369
2804
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2370
2805
|
return;
|
|
2371
|
-
|
|
2372
|
-
const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
|
|
2373
|
-
if (client._sessionOptions?.background_sync !== false
|
|
2374
|
-
&& tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
|
|
2375
|
-
await this.runP2PForwardRecovery(ns, 50).catch((exc) => {
|
|
2376
|
-
client._clientLog.warn(`V2 P2P stale Tail Forward recovery failed: ${formatDeliveryError(exc)}`);
|
|
2377
|
-
});
|
|
2378
|
-
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2806
|
+
// Tail 期间新到达的 Push 已由 realtime sync 在同一周期内补至多一页 Forward。
|
|
2381
2807
|
}
|
|
2382
2808
|
catch (exc) {
|
|
2383
2809
|
client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
|
|
@@ -2385,6 +2811,7 @@ export class MessageDeliveryEngine {
|
|
|
2385
2811
|
finally {
|
|
2386
2812
|
if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
|
|
2387
2813
|
this.endRealtimeSync(ns);
|
|
2814
|
+
client._schedulePendingP2pPullIfNeeded?.(ns, 'push-auto-pull-complete');
|
|
2388
2815
|
}
|
|
2389
2816
|
}
|
|
2390
2817
|
}
|
|
@@ -2393,50 +2820,35 @@ export class MessageDeliveryEngine {
|
|
|
2393
2820
|
const groupId = String(data.group_id ?? '').trim();
|
|
2394
2821
|
if (!groupId)
|
|
2395
2822
|
return;
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
this.
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
while (client._onlineUnreadHintQueue.size > 0) {
|
|
2413
|
-
if (client.state !== ConnectionState.READY)
|
|
2414
|
-
return;
|
|
2415
|
-
const groupId = client._onlineUnreadHintQueue.keys().next().value;
|
|
2416
|
-
if (!groupId)
|
|
2417
|
-
return;
|
|
2418
|
-
const payload = client._onlineUnreadHintQueue.get(groupId);
|
|
2419
|
-
client._onlineUnreadHintQueue.delete(groupId);
|
|
2420
|
-
if (!payload)
|
|
2421
|
-
continue;
|
|
2422
|
-
const currentAck = this.syncState(`group:${groupId}`).ack;
|
|
2423
|
-
const attemptedAck = attemptedAckByGroup.get(groupId);
|
|
2424
|
-
if (attemptedAck !== undefined && currentAck <= attemptedAck)
|
|
2425
|
-
continue;
|
|
2426
|
-
attemptedAckByGroup.set(groupId, currentAck);
|
|
2427
|
-
await this.onRawGroupV2MessageCreated({ ...payload, _online_hint_drained: true });
|
|
2428
|
-
const intervalMs = Math.max(0, Number(client._onlineUnreadHintIntervalMs ?? 50) || 0);
|
|
2429
|
-
if (intervalMs > 0 && client._onlineUnreadHintQueue.size > 0) {
|
|
2430
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
2823
|
+
const ns = `group:${groupId}`;
|
|
2824
|
+
const targets = this.onlineUnreadHintTargets ?? new Map();
|
|
2825
|
+
const tasks = this.onlineUnreadHintTasks ?? new Map();
|
|
2826
|
+
this.onlineUnreadHintTargets = targets;
|
|
2827
|
+
this.onlineUnreadHintTasks = tasks;
|
|
2828
|
+
targets.set(ns, Math.max(targets.get(ns) ?? 0, positiveSafeSequenceHint(data.seq)));
|
|
2829
|
+
if (tasks.has(ns))
|
|
2830
|
+
return;
|
|
2831
|
+
const task = Promise.resolve().then(async () => {
|
|
2832
|
+
try {
|
|
2833
|
+
while (true) {
|
|
2834
|
+
const ackBefore = this.syncState(ns).ack;
|
|
2835
|
+
await this.runGroupForwardRecovery(groupId, ns, 50, true, targets.get(ns) ?? 0);
|
|
2836
|
+
const ackAfter = this.syncState(ns).ack;
|
|
2837
|
+
if (ackAfter >= (targets.get(ns) ?? 0) || ackAfter <= ackBefore)
|
|
2838
|
+
return;
|
|
2431
2839
|
}
|
|
2432
2840
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2841
|
+
catch (exc) {
|
|
2842
|
+
client._clientLog.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2843
|
+
}
|
|
2844
|
+
finally {
|
|
2845
|
+
targets.delete(ns);
|
|
2846
|
+
if (tasks.get(ns) === task)
|
|
2847
|
+
tasks.delete(ns);
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
tasks.set(ns, task);
|
|
2851
|
+
client._safeAsync(task);
|
|
2440
2852
|
}
|
|
2441
2853
|
async onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
|
|
2442
2854
|
const client = this.runtime.client;
|
|
@@ -2457,29 +2869,16 @@ export class MessageDeliveryEngine {
|
|
|
2457
2869
|
return;
|
|
2458
2870
|
}
|
|
2459
2871
|
if (seq === null) {
|
|
2460
|
-
if (inlinePresent
|
|
2872
|
+
if (inlinePresent) {
|
|
2461
2873
|
await this.recoverInvalidGroupRealtimeHead(groupId, `group:${groupId}`, rawSeq);
|
|
2462
2874
|
}
|
|
2463
2875
|
return;
|
|
2464
2876
|
}
|
|
2465
2877
|
const eventKind = String(d.kind ?? '').trim();
|
|
2466
2878
|
if (eventKind === 'group.online_unread_hint') {
|
|
2467
|
-
if (!d._online_hint_drained) {
|
|
2468
|
-
if (client._sessionOptions.background_sync === false)
|
|
2469
|
-
return;
|
|
2470
|
-
this.enqueueOnlineUnreadHint(d);
|
|
2471
|
-
return;
|
|
2472
|
-
}
|
|
2473
|
-
// Hint 不是真实消息 Head:不消费 inline、不推进水位、也不直接 ACK。
|
|
2474
|
-
// 只在后台同步开启时,从已提交 Ack 强制做一次普通 Forward,绝不进入 Tail planner。
|
|
2475
2879
|
if (client._sessionOptions.background_sync === false)
|
|
2476
2880
|
return;
|
|
2477
|
-
|
|
2478
|
-
await this.runGroupForwardRecovery(groupId, `group:${groupId}`, 50);
|
|
2479
|
-
}
|
|
2480
|
-
catch (exc) {
|
|
2481
|
-
client._clientLog.warn(`group online unread hint recovery pull failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2482
|
-
}
|
|
2881
|
+
this.enqueueOnlineUnreadHint(d);
|
|
2483
2882
|
return;
|
|
2484
2883
|
}
|
|
2485
2884
|
const ns = `group:${groupId}`;
|
|
@@ -2500,40 +2899,32 @@ export class MessageDeliveryEngine {
|
|
|
2500
2899
|
catch (exc) {
|
|
2501
2900
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2502
2901
|
return;
|
|
2503
|
-
client._clientLog.warn(`V2 group inline push rejected, falling back to
|
|
2902
|
+
client._clientLog.warn(`V2 group inline push rejected, falling back to sync planner: group=${groupId} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
2504
2903
|
}
|
|
2505
2904
|
}
|
|
2506
2905
|
const before = this.syncState(ns);
|
|
2507
2906
|
client._seqTracker.updateMaxSeen(ns, seq);
|
|
2508
|
-
|
|
2509
|
-
if (seq <= before.head && !hasGap && !inlinePresent) {
|
|
2907
|
+
if (seq <= before.ack && before.ack >= before.tail - 1) {
|
|
2510
2908
|
client._clientLog.debug(`V2 group duplicate push ignored: group=${groupId} seq=${seq} A/T/H=${JSON.stringify(before)}`);
|
|
2511
2909
|
const pending = this.pendingGroupInlineAcks?.get(ns);
|
|
2512
2910
|
if (pending?.seq === seq) {
|
|
2513
2911
|
this.retryPendingGroupInlineAck(ns, seq, 'covered inline push');
|
|
2514
2912
|
}
|
|
2515
|
-
else {
|
|
2913
|
+
else if (!inlinePresent) {
|
|
2516
2914
|
this.fireGroupV2Ack(groupId, seq, 'covered push');
|
|
2517
2915
|
}
|
|
2518
2916
|
return;
|
|
2519
2917
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2918
|
+
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
2919
|
+
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
2920
|
+
this.recordPendingGroupPull(ns, seq);
|
|
2921
|
+
client._clientLog.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
|
|
2522
2922
|
return;
|
|
2523
2923
|
}
|
|
2524
|
-
|
|
2924
|
+
void this.runGroupRealtimeSync(groupId, ns, seq, 50, false).then(async () => {
|
|
2525
2925
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2526
2926
|
return;
|
|
2527
|
-
|
|
2528
|
-
const maxSeen = Number(client._seqTracker.getMaxSeenSeq?.(ns) ?? 0);
|
|
2529
|
-
if (client._sessionOptions?.background_sync !== false
|
|
2530
|
-
&& tailHead !== null && Number.isSafeInteger(maxSeen) && maxSeen > tailHead) {
|
|
2531
|
-
await this.runGroupForwardRecovery(groupId, ns, 50).catch((exc) => {
|
|
2532
|
-
client._clientLog.warn(`V2 group stale Tail Forward recovery failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
2533
|
-
});
|
|
2534
|
-
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2535
|
-
return;
|
|
2536
|
-
}
|
|
2927
|
+
// Tail 期间新到达的 Push 已由 realtime sync 在同一周期内补至多一页 Forward。
|
|
2537
2928
|
}).catch((exc) => {
|
|
2538
2929
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
2539
2930
|
return;
|
|
@@ -2541,11 +2932,9 @@ export class MessageDeliveryEngine {
|
|
|
2541
2932
|
}).finally(() => {
|
|
2542
2933
|
if (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration)) {
|
|
2543
2934
|
this.endRealtimeSync(ns);
|
|
2935
|
+
this.schedulePendingGroupPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
2544
2936
|
}
|
|
2545
2937
|
});
|
|
2546
|
-
if (d._online_hint_drained) {
|
|
2547
|
-
await pullTask;
|
|
2548
|
-
}
|
|
2549
2938
|
}
|
|
2550
2939
|
restoreSeqTrackerState() {
|
|
2551
2940
|
const client = this.runtime.client;
|
|
@@ -2598,11 +2987,20 @@ export class MessageDeliveryEngine {
|
|
|
2598
2987
|
migrateSeqStateGroupIds(state) {
|
|
2599
2988
|
return state;
|
|
2600
2989
|
}
|
|
2601
|
-
|
|
2990
|
+
enqueueOnlineUnreadP2pHint(data) {
|
|
2991
|
+
const client = this.runtime.client;
|
|
2992
|
+
if (!client._aid || !client._v2Session)
|
|
2993
|
+
return;
|
|
2994
|
+
const ns = `p2p:${client._aid}`;
|
|
2995
|
+
client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
|
|
2996
|
+
client._clientLog.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2997
|
+
}));
|
|
2998
|
+
}
|
|
2999
|
+
async saveSeqTrackerState() {
|
|
2602
3000
|
const client = this.runtime.client;
|
|
2603
3001
|
const context = this.currentSeqTrackerContext();
|
|
2604
3002
|
if (!context) {
|
|
2605
|
-
this.flushSeqTrackerPending();
|
|
3003
|
+
await this.flushSeqTrackerPending();
|
|
2606
3004
|
return;
|
|
2607
3005
|
}
|
|
2608
3006
|
const state = typeof client._seqTracker.exportSyncState === 'function'
|
|
@@ -2620,13 +3018,13 @@ export class MessageDeliveryEngine {
|
|
|
2620
3018
|
}
|
|
2621
3019
|
if (Object.keys(state).length > 0)
|
|
2622
3020
|
this.mergeSeqTrackerPending(context, state);
|
|
2623
|
-
this.flushSeqTrackerPending();
|
|
3021
|
+
await this.flushSeqTrackerPending();
|
|
2624
3022
|
}
|
|
2625
3023
|
persistSeq(ns, forceSeq) {
|
|
2626
3024
|
const client = this.runtime.client;
|
|
2627
3025
|
const context = this.currentSeqTrackerContext();
|
|
2628
3026
|
if (!context) {
|
|
2629
|
-
this.flushSeqTrackerPending();
|
|
3027
|
+
void this.flushSeqTrackerPending();
|
|
2630
3028
|
return;
|
|
2631
3029
|
}
|
|
2632
3030
|
const seq = forceSeq ?? client._seqTracker.getContiguousSeq(ns);
|
|
@@ -2641,7 +3039,7 @@ export class MessageDeliveryEngine {
|
|
|
2641
3039
|
this.mergeSeqTrackerPending(context, { [ns]: state });
|
|
2642
3040
|
this.scheduleSeqTrackerFlush();
|
|
2643
3041
|
}
|
|
2644
|
-
commitSeqTrackerState(ns) {
|
|
3042
|
+
async commitSeqTrackerState(ns) {
|
|
2645
3043
|
const client = this.runtime.client;
|
|
2646
3044
|
const context = this.currentSeqTrackerContext();
|
|
2647
3045
|
if (!context || !ns)
|
|
@@ -2652,11 +3050,17 @@ export class MessageDeliveryEngine {
|
|
|
2652
3050
|
if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
|
|
2653
3051
|
return;
|
|
2654
3052
|
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
2655
|
-
this.
|
|
2656
|
-
|
|
3053
|
+
const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
|
|
3054
|
+
try {
|
|
3055
|
+
await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
|
|
3056
|
+
}
|
|
3057
|
+
catch (exc) {
|
|
3058
|
+
this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
|
|
3059
|
+
throw exc;
|
|
3060
|
+
}
|
|
2657
3061
|
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
2658
3062
|
}
|
|
2659
|
-
flushSeqTrackerPending() {
|
|
3063
|
+
async flushSeqTrackerPending() {
|
|
2660
3064
|
const client = this.runtime.client;
|
|
2661
3065
|
this.cancelSeqTrackerFlushTimer();
|
|
2662
3066
|
const context = client._seqTrackerPendingPersistContext;
|
|
@@ -2669,7 +3073,7 @@ export class MessageDeliveryEngine {
|
|
|
2669
3073
|
const state = Object.fromEntries(pending.entries());
|
|
2670
3074
|
pending.clear();
|
|
2671
3075
|
client._seqTrackerPendingPersistContext = null;
|
|
2672
|
-
this.writeSeqTrackerState(aid, deviceId, slotId, state);
|
|
3076
|
+
await this.writeSeqTrackerState(aid, deviceId, slotId, state);
|
|
2673
3077
|
}
|
|
2674
3078
|
dropSeqTrackerPending(ns) {
|
|
2675
3079
|
const client = this.runtime.client;
|
|
@@ -2680,6 +3084,34 @@ export class MessageDeliveryEngine {
|
|
|
2680
3084
|
this.cancelSeqTrackerFlushTimer();
|
|
2681
3085
|
}
|
|
2682
3086
|
}
|
|
3087
|
+
takeSeqTrackerPending(context, ns) {
|
|
3088
|
+
const client = this.runtime.client;
|
|
3089
|
+
if (client._seqTrackerPendingPersistContext !== context)
|
|
3090
|
+
return null;
|
|
3091
|
+
const pending = this.seqTrackerPending();
|
|
3092
|
+
const state = pending.get(ns);
|
|
3093
|
+
if (!state)
|
|
3094
|
+
return null;
|
|
3095
|
+
pending.delete(ns);
|
|
3096
|
+
if (pending.size === 0) {
|
|
3097
|
+
client._seqTrackerPendingPersistContext = null;
|
|
3098
|
+
this.cancelSeqTrackerFlushTimer();
|
|
3099
|
+
}
|
|
3100
|
+
return { ...state };
|
|
3101
|
+
}
|
|
3102
|
+
restoreSeqTrackerPending(context, ns, state) {
|
|
3103
|
+
if (!state || this.currentSeqTrackerContext() !== context)
|
|
3104
|
+
return;
|
|
3105
|
+
const client = this.runtime.client;
|
|
3106
|
+
const pendingContext = client._seqTrackerPendingPersistContext;
|
|
3107
|
+
if (pendingContext && pendingContext !== context)
|
|
3108
|
+
return;
|
|
3109
|
+
const pending = this.seqTrackerPending();
|
|
3110
|
+
if (!pending.has(ns))
|
|
3111
|
+
pending.set(ns, state);
|
|
3112
|
+
client._seqTrackerPendingPersistContext = context;
|
|
3113
|
+
this.scheduleSeqTrackerFlush();
|
|
3114
|
+
}
|
|
2683
3115
|
currentSeqTrackerContext() {
|
|
2684
3116
|
const client = this.runtime.client;
|
|
2685
3117
|
if (!client._aid)
|
|
@@ -2698,7 +3130,7 @@ export class MessageDeliveryEngine {
|
|
|
2698
3130
|
const pending = this.seqTrackerPending();
|
|
2699
3131
|
const pendingContext = client._seqTrackerPendingPersistContext;
|
|
2700
3132
|
if (pendingContext && pendingContext !== context && pending.size > 0) {
|
|
2701
|
-
this.flushSeqTrackerPending();
|
|
3133
|
+
void this.flushSeqTrackerPending();
|
|
2702
3134
|
}
|
|
2703
3135
|
client._seqTrackerPendingPersistContext = context;
|
|
2704
3136
|
for (const [ns, raw] of Object.entries(state)) {
|
|
@@ -2719,7 +3151,7 @@ export class MessageDeliveryEngine {
|
|
|
2719
3151
|
return;
|
|
2720
3152
|
client._seqTrackerFlushTimer = setTimeout(() => {
|
|
2721
3153
|
client._seqTrackerFlushTimer = null;
|
|
2722
|
-
this.flushSeqTrackerPending();
|
|
3154
|
+
void this.flushSeqTrackerPending();
|
|
2723
3155
|
}, SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS);
|
|
2724
3156
|
}
|
|
2725
3157
|
cancelSeqTrackerFlushTimer() {
|
|
@@ -2729,26 +3161,34 @@ export class MessageDeliveryEngine {
|
|
|
2729
3161
|
client._seqTrackerFlushTimer = null;
|
|
2730
3162
|
}
|
|
2731
3163
|
}
|
|
2732
|
-
writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
3164
|
+
async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
3165
|
+
await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
|
|
3166
|
+
}
|
|
3167
|
+
enqueueSeqTrackerWrite(operation) {
|
|
3168
|
+
const run = this.seqTrackerWriteChain.then(operation, operation);
|
|
3169
|
+
this.seqTrackerWriteChain = run.catch(() => { });
|
|
3170
|
+
return run;
|
|
3171
|
+
}
|
|
3172
|
+
async writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError = false) {
|
|
2733
3173
|
const client = this.runtime.client;
|
|
2734
3174
|
if (!aid || Object.keys(state).length === 0)
|
|
2735
3175
|
return;
|
|
2736
3176
|
try {
|
|
2737
3177
|
const saveSync = client._tokenStore.saveSyncStates;
|
|
2738
3178
|
if (typeof saveSync === 'function') {
|
|
2739
|
-
saveSync.call(client._tokenStore, aid, deviceId, slotId, state);
|
|
3179
|
+
await saveSync.call(client._tokenStore, aid, deviceId, slotId, state);
|
|
2740
3180
|
return;
|
|
2741
3181
|
}
|
|
2742
3182
|
const saveFn = client._tokenStore.saveSeq;
|
|
2743
3183
|
if (typeof saveFn === 'function') {
|
|
2744
3184
|
for (const [ns, triplet] of Object.entries(state)) {
|
|
2745
|
-
saveFn.call(client._tokenStore, aid, deviceId, slotId, ns, triplet.ack);
|
|
3185
|
+
await saveFn.call(client._tokenStore, aid, deviceId, slotId, ns, triplet.ack);
|
|
2746
3186
|
}
|
|
2747
3187
|
return;
|
|
2748
3188
|
}
|
|
2749
3189
|
const updater = client._tokenStore.updateInstanceState;
|
|
2750
3190
|
if (typeof updater === 'function') {
|
|
2751
|
-
updater.call(client._tokenStore, aid, deviceId, slotId, (metadata) => {
|
|
3191
|
+
await updater.call(client._tokenStore, aid, deviceId, slotId, (metadata) => {
|
|
2752
3192
|
const existing = isJsonObject(metadata.seq_tracker_state)
|
|
2753
3193
|
? { ...metadata.seq_tracker_state }
|
|
2754
3194
|
: {};
|
|
@@ -2775,36 +3215,56 @@ export class MessageDeliveryEngine {
|
|
|
2775
3215
|
const client = this.runtime.client;
|
|
2776
3216
|
if (!client._aid || !ns)
|
|
2777
3217
|
return;
|
|
3218
|
+
const aid = client._aid;
|
|
3219
|
+
const deviceId = client._deviceId;
|
|
3220
|
+
const slotId = client._slotId;
|
|
2778
3221
|
const seq = client._seqTracker.getContiguousSeq(ns);
|
|
2779
3222
|
this.dropSeqTrackerPending(ns);
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
if (seq <= 0 && typeof updater === 'function') {
|
|
2792
|
-
updater.call(client._tokenStore, client._aid, client._deviceId, client._slotId, (metadata) => {
|
|
2793
|
-
if (isJsonObject(metadata.seq_tracker_state)) {
|
|
2794
|
-
const next = { ...metadata.seq_tracker_state };
|
|
2795
|
-
delete next[ns];
|
|
2796
|
-
metadata.seq_tracker_state = next;
|
|
2797
|
-
}
|
|
2798
|
-
return metadata;
|
|
2799
|
-
});
|
|
2800
|
-
return;
|
|
2801
|
-
}
|
|
2802
|
-
if (seq > 0) {
|
|
2803
|
-
this.saveSeqTrackerState();
|
|
2804
|
-
}
|
|
3223
|
+
const saveSeq = client._tokenStore.saveSeq;
|
|
3224
|
+
if (seq > 0 && typeof saveSeq === 'function') {
|
|
3225
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3226
|
+
try {
|
|
3227
|
+
await saveSeq.call(client._tokenStore, aid, deviceId, slotId, ns, seq);
|
|
3228
|
+
}
|
|
3229
|
+
catch (exc) {
|
|
3230
|
+
client._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
3231
|
+
}
|
|
3232
|
+
});
|
|
3233
|
+
return;
|
|
2805
3234
|
}
|
|
2806
|
-
|
|
2807
|
-
|
|
3235
|
+
const deleteSeq = client._tokenStore.deleteSeq;
|
|
3236
|
+
if (seq <= 0 && typeof deleteSeq === 'function') {
|
|
3237
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3238
|
+
try {
|
|
3239
|
+
await deleteSeq.call(client._tokenStore, aid, deviceId, slotId, ns);
|
|
3240
|
+
}
|
|
3241
|
+
catch (exc) {
|
|
3242
|
+
client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
const updater = client._tokenStore.updateInstanceState;
|
|
3248
|
+
if (seq <= 0 && typeof updater === 'function') {
|
|
3249
|
+
void this.enqueueSeqTrackerWrite(async () => {
|
|
3250
|
+
try {
|
|
3251
|
+
await updater.call(client._tokenStore, aid, deviceId, slotId, (metadata) => {
|
|
3252
|
+
if (isJsonObject(metadata.seq_tracker_state)) {
|
|
3253
|
+
const next = { ...metadata.seq_tracker_state };
|
|
3254
|
+
delete next[ns];
|
|
3255
|
+
metadata.seq_tracker_state = next;
|
|
3256
|
+
}
|
|
3257
|
+
return metadata;
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
catch (exc) {
|
|
3261
|
+
client._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
3262
|
+
}
|
|
3263
|
+
});
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
if (seq > 0) {
|
|
3267
|
+
void this.saveSeqTrackerState();
|
|
2808
3268
|
}
|
|
2809
3269
|
}
|
|
2810
3270
|
clampAckSeq(method, field, ns, seq) {
|
|
@@ -2864,13 +3324,15 @@ export class MessageDeliveryEngine {
|
|
|
2864
3324
|
}
|
|
2865
3325
|
return params;
|
|
2866
3326
|
}
|
|
2867
|
-
async drainOrderedMessages(ns, beforeSeq, pullResponse = false) {
|
|
3327
|
+
async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
|
|
2868
3328
|
const client = this.runtime.client;
|
|
3329
|
+
this.ensurePullOperationCurrent();
|
|
2869
3330
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
2870
3331
|
if (!queue || queue.size === 0)
|
|
2871
3332
|
return;
|
|
2872
3333
|
let delivered = false;
|
|
2873
3334
|
while (true) {
|
|
3335
|
+
this.ensurePullOperationCurrent();
|
|
2874
3336
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
2875
3337
|
const ready = [...queue.keys()]
|
|
2876
3338
|
.filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
|
|
@@ -2896,6 +3358,7 @@ export class MessageDeliveryEngine {
|
|
|
2896
3358
|
continue;
|
|
2897
3359
|
}
|
|
2898
3360
|
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, 'ordered-drain', pullResponse);
|
|
3361
|
+
this.ensurePullOperationCurrent();
|
|
2899
3362
|
this.markPublishedSeq(ns, seq);
|
|
2900
3363
|
client._markOrderedSeqDelivered(ns, seq);
|
|
2901
3364
|
delivered = true;
|
|
@@ -2903,16 +3366,20 @@ export class MessageDeliveryEngine {
|
|
|
2903
3366
|
}
|
|
2904
3367
|
if (queue.size === 0) {
|
|
2905
3368
|
client._pendingOrderedMsgs.delete(ns);
|
|
2906
|
-
if (delivered)
|
|
2907
|
-
this.
|
|
3369
|
+
if (delivered && persist) {
|
|
3370
|
+
this.ensurePullOperationCurrent();
|
|
3371
|
+
await this.saveSeqTrackerState();
|
|
3372
|
+
}
|
|
2908
3373
|
}
|
|
2909
3374
|
}
|
|
2910
3375
|
async publishOrderedMessage(event, ns, seq, payload) {
|
|
2911
3376
|
const client = this.runtime.client;
|
|
3377
|
+
this.ensurePullOperationCurrent();
|
|
2912
3378
|
const seqNum = Number(seq);
|
|
2913
3379
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
|
|
2914
3380
|
client._clientLog.debug(`publish ordered direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
|
|
2915
3381
|
await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
|
|
3382
|
+
this.ensurePullOperationCurrent();
|
|
2916
3383
|
return true;
|
|
2917
3384
|
}
|
|
2918
3385
|
if (client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
@@ -2930,6 +3397,7 @@ export class MessageDeliveryEngine {
|
|
|
2930
3397
|
return false;
|
|
2931
3398
|
}
|
|
2932
3399
|
await this.drainOrderedMessages(ns, seqNum);
|
|
3400
|
+
this.ensurePullOperationCurrent();
|
|
2933
3401
|
if (client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
2934
3402
|
client._clientLog.debug(`publish ordered skipped after-drain duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
2935
3403
|
return false;
|
|
@@ -2939,19 +3407,24 @@ export class MessageDeliveryEngine {
|
|
|
2939
3407
|
if (queue && queue.size === 0)
|
|
2940
3408
|
client._pendingOrderedMsgs.delete(ns);
|
|
2941
3409
|
await this.publishOrderedQueueItem(ns, event, seqNum, payload, 'ordered');
|
|
3410
|
+
this.ensurePullOperationCurrent();
|
|
2942
3411
|
this.markPublishedSeq(ns, seqNum);
|
|
2943
3412
|
client._markOrderedSeqDelivered(ns, seqNum);
|
|
2944
3413
|
client._clientLog.debug(`publish ordered delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
2945
3414
|
await this.drainOrderedMessages(ns);
|
|
2946
|
-
if (!client._pendingOrderedMsgs.get(ns))
|
|
2947
|
-
this.
|
|
3415
|
+
if (!client._pendingOrderedMsgs.get(ns)) {
|
|
3416
|
+
this.ensurePullOperationCurrent();
|
|
3417
|
+
await this.saveSeqTrackerState();
|
|
3418
|
+
}
|
|
2948
3419
|
return true;
|
|
2949
3420
|
}
|
|
2950
3421
|
async publishOrderedGroupRecall(ns, seq, message) {
|
|
2951
3422
|
const client = this.runtime.client;
|
|
3423
|
+
this.ensurePullOperationCurrent();
|
|
2952
3424
|
const seqNum = Number(seq);
|
|
2953
3425
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
|
|
2954
3426
|
await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
|
|
3427
|
+
this.ensurePullOperationCurrent();
|
|
2955
3428
|
return true;
|
|
2956
3429
|
}
|
|
2957
3430
|
// 撤回 tombstone 也占 seq:推进 contiguous 并标记已发布,确保 ack 正常推进、不留空洞。
|
|
@@ -2961,24 +3434,31 @@ export class MessageDeliveryEngine {
|
|
|
2961
3434
|
}
|
|
2962
3435
|
client._seqTracker.onMessageSeq(ns, seqNum);
|
|
2963
3436
|
await this.publishGroupRecallTombstone(ns.replace(/^group:/, ''), seq, message);
|
|
3437
|
+
this.ensurePullOperationCurrent();
|
|
2964
3438
|
this.markPublishedSeq(ns, seqNum);
|
|
2965
3439
|
client._markOrderedSeqDelivered?.(ns, seqNum);
|
|
2966
3440
|
await this.drainOrderedMessages(ns);
|
|
2967
|
-
if (!client._pendingOrderedMsgs.get(ns))
|
|
2968
|
-
this.
|
|
3441
|
+
if (!client._pendingOrderedMsgs.get(ns)) {
|
|
3442
|
+
this.ensurePullOperationCurrent();
|
|
3443
|
+
await this.saveSeqTrackerState();
|
|
3444
|
+
}
|
|
2969
3445
|
return true;
|
|
2970
3446
|
}
|
|
2971
|
-
async publishPulledMessage(event, ns, seq, payload) {
|
|
3447
|
+
async publishPulledMessage(event, ns, seq, payload, persist = true) {
|
|
2972
3448
|
const client = this.runtime.client;
|
|
3449
|
+
this.ensurePullOperationCurrent();
|
|
2973
3450
|
const seqNum = Number(seq);
|
|
2974
3451
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
|
|
2975
3452
|
client._clientLog.debug(`publish pulled direct(no-seq): event=${event}, ns=${ns || '<none>'}, seq=${String(seq)}`);
|
|
2976
3453
|
if (event === 'message.recalled') {
|
|
2977
|
-
|
|
3454
|
+
const published = await this.publishMessageRecallTombstone(seq, payload);
|
|
3455
|
+
this.ensurePullOperationCurrent();
|
|
3456
|
+
return published;
|
|
2978
3457
|
}
|
|
2979
3458
|
const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
|
|
2980
3459
|
if (isPromiseLike(published))
|
|
2981
3460
|
await published;
|
|
3461
|
+
this.ensurePullOperationCurrent();
|
|
2982
3462
|
return true;
|
|
2983
3463
|
}
|
|
2984
3464
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -2989,23 +3469,28 @@ export class MessageDeliveryEngine {
|
|
|
2989
3469
|
client._pendingOrderedMsgs.delete(ns);
|
|
2990
3470
|
return false;
|
|
2991
3471
|
}
|
|
2992
|
-
await this.drainOrderedMessages(ns, seqNum, true);
|
|
3472
|
+
await this.drainOrderedMessages(ns, seqNum, true, persist);
|
|
3473
|
+
this.ensurePullOperationCurrent();
|
|
2993
3474
|
queue?.delete(seqNum);
|
|
2994
3475
|
if (queue && queue.size === 0)
|
|
2995
3476
|
client._pendingOrderedMsgs.delete(ns);
|
|
2996
3477
|
if (event === 'message.recalled') {
|
|
2997
3478
|
const recallPublished = await this.publishMessageRecallTombstone(seqNum, payload);
|
|
3479
|
+
this.ensurePullOperationCurrent();
|
|
2998
3480
|
this.markPublishedSeq(ns, seqNum);
|
|
2999
3481
|
client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
3000
|
-
await this.drainOrderedMessages(ns, undefined, true);
|
|
3482
|
+
await this.drainOrderedMessages(ns, undefined, true, persist);
|
|
3483
|
+
this.ensurePullOperationCurrent();
|
|
3001
3484
|
return recallPublished;
|
|
3002
3485
|
}
|
|
3003
3486
|
const published = client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, 'pull'));
|
|
3004
3487
|
if (isPromiseLike(published))
|
|
3005
3488
|
await published;
|
|
3489
|
+
this.ensurePullOperationCurrent();
|
|
3006
3490
|
this.markPublishedSeq(ns, seqNum);
|
|
3007
3491
|
client._clientLog.debug(`publish pulled delivered: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
3008
|
-
await this.drainOrderedMessages(ns, undefined, true);
|
|
3492
|
+
await this.drainOrderedMessages(ns, undefined, true, persist);
|
|
3493
|
+
this.ensurePullOperationCurrent();
|
|
3009
3494
|
return true;
|
|
3010
3495
|
}
|
|
3011
3496
|
}
|