@agentunion/fastaun-browser 0.5.19 → 0.5.21
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 +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +16 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +65 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +35 -3
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +384 -150
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +59 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +202 -16
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +4 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +24 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +89 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +6410 -1848
- package/dist/cert-utils.d.ts +50 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +123 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +108 -16
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1411 -184
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +203 -117
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +22 -3
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +1312 -684
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +419 -101
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +24 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +171 -29
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +42 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +14 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +30 -13
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +19 -2
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +193 -6
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +15 -2
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +308 -27
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +827 -21
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +43 -9
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +183 -81
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +36 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +610 -89
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/seq-tracker.d.ts +2 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +3 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +61 -3
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +524 -45
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +64 -30
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/client/delivery.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { slotIsolationKey } from '../config.js';
|
|
2
|
+
import { StateError, ValidationError } from '../errors.js';
|
|
2
3
|
import { normalizeGroupAid } from '../group-id.js';
|
|
3
4
|
import { SeqTracker } from '../seq-tracker.js';
|
|
4
5
|
import { ConnectionState, isJsonObject } from '../types.js';
|
|
@@ -16,7 +17,7 @@ const APP_MESSAGE_ENVELOPE_KEYS = [
|
|
|
16
17
|
'module_id', 'message_type', 'type', 'kind', 'version',
|
|
17
18
|
'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
|
|
18
19
|
'timestamp', 'created_at', 'encrypted',
|
|
19
|
-
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
|
|
20
|
+
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode', 'need_render',
|
|
20
21
|
];
|
|
21
22
|
const RECALL_PAYLOAD_KEYS = [
|
|
22
23
|
'message_ids', 'target_message_seqs',
|
|
@@ -36,6 +37,11 @@ const APP_GROUP_EVENT_ENVELOPE_KEYS = [
|
|
|
36
37
|
'actor_aid', 'sender_aid', 'member_aid', 'target_aid', 'operator_aid',
|
|
37
38
|
'created_at', 'timestamp', 't_server', 'status', 'device_id',
|
|
38
39
|
];
|
|
40
|
+
const APPLICATION_EVENT_FAMILIES = {
|
|
41
|
+
'message.received': 'message.batch', 'message.recalled': 'message.batch',
|
|
42
|
+
'group.message_created': 'group.message_batch', 'group.message_recalled': 'group.message_batch',
|
|
43
|
+
'group.changed': 'group.event_batch',
|
|
44
|
+
};
|
|
39
45
|
const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
|
|
40
46
|
function formatDeliveryError(error) {
|
|
41
47
|
return error instanceof Error ? error : String(error);
|
|
@@ -120,6 +126,440 @@ function p2pAppEventFromPlainPullMessage(message) {
|
|
|
120
126
|
return { event: 'message.received', payload: message };
|
|
121
127
|
}
|
|
122
128
|
export class MessageDeliveryEngine {
|
|
129
|
+
peerBootstrapContext = '';
|
|
130
|
+
peerTracker = null;
|
|
131
|
+
peerTrackerGeneration = 0;
|
|
132
|
+
peerBootstrapFloor = null;
|
|
133
|
+
peerInitializations = new Map();
|
|
134
|
+
peerInitialized = new Set();
|
|
135
|
+
committedSeqTrackerStates = new Map();
|
|
136
|
+
seqTrackerNamespaceGenerations = new Map();
|
|
137
|
+
peerDirectoryRequests = new Map();
|
|
138
|
+
peerRecoveries = new Map();
|
|
139
|
+
peerPendingSeqHints = new Map();
|
|
140
|
+
peerPendingConfirmedThrough = new Map();
|
|
141
|
+
peerRecoveryTimer = null;
|
|
142
|
+
peerVolatileCursors = new Map();
|
|
143
|
+
peerDirectoryDirty = false;
|
|
144
|
+
peerDirectoryRefreshing = false;
|
|
145
|
+
p2pDeliveryInflight = new Map();
|
|
146
|
+
p2pDeliveryCompleted = new Map();
|
|
147
|
+
peerNamespace(sender) {
|
|
148
|
+
return `p2p.peer.v1/${btoa(String.fromCharCode(...new TextEncoder().encode(sender))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}`;
|
|
149
|
+
}
|
|
150
|
+
peerSender(ns) {
|
|
151
|
+
if (!ns.startsWith('p2p.peer.v1/'))
|
|
152
|
+
return '';
|
|
153
|
+
try {
|
|
154
|
+
return new TextDecoder().decode(Uint8Array.from(atob(ns.slice(12).replace(/-/g, '+').replace(/_/g, '/')), (value) => value.charCodeAt(0)));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
peerContext() {
|
|
161
|
+
const client = this.runtime.client;
|
|
162
|
+
const context = JSON.stringify([client._aid, client._deviceId, slotIsolationKey(client._slotId)]);
|
|
163
|
+
if (context !== this.peerBootstrapContext || this.peerTracker !== client._seqTracker) {
|
|
164
|
+
this.peerBootstrapContext = context;
|
|
165
|
+
this.peerTracker = client._seqTracker;
|
|
166
|
+
this.peerTrackerGeneration += 1;
|
|
167
|
+
this.peerBootstrapFloor = null;
|
|
168
|
+
this.peerInitializations.clear();
|
|
169
|
+
this.peerInitialized.clear();
|
|
170
|
+
this.committedSeqTrackerStates.clear();
|
|
171
|
+
this.seqTrackerNamespaceGenerations.clear();
|
|
172
|
+
this.peerRecoveries.clear();
|
|
173
|
+
this.peerPendingSeqHints.clear();
|
|
174
|
+
this.peerPendingConfirmedThrough.clear();
|
|
175
|
+
this.peerVolatileCursors.clear();
|
|
176
|
+
}
|
|
177
|
+
return JSON.stringify([context, this.peerTrackerGeneration]);
|
|
178
|
+
}
|
|
179
|
+
isPeerInitialized(ns) {
|
|
180
|
+
if (!this.peerInitialized.has(ns))
|
|
181
|
+
return false;
|
|
182
|
+
if (this.runtime.client._seqTracker.snapshotNamespace(ns) !== null)
|
|
183
|
+
return true;
|
|
184
|
+
this.peerInitialized.delete(ns);
|
|
185
|
+
this.committedSeqTrackerStates.delete(ns);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
async initializePeer(sender, initial) {
|
|
189
|
+
const client = this.runtime.client;
|
|
190
|
+
const context = this.peerContext();
|
|
191
|
+
const scope = [client._aid, client._deviceId, client._slotId];
|
|
192
|
+
if (!sender)
|
|
193
|
+
throw new ValidationError('sender_aid is required');
|
|
194
|
+
const ns = this.peerNamespace(sender);
|
|
195
|
+
if (this.isPeerInitialized(ns))
|
|
196
|
+
return;
|
|
197
|
+
const restored = client._seqTracker.exportSyncState()[ns];
|
|
198
|
+
if (restored && (restored.ack > 0 || restored.tail > 0 || restored.head > 0)) {
|
|
199
|
+
this.committedSeqTrackerStates.set(ns, { ...restored });
|
|
200
|
+
this.peerInitialized.add(ns);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (initial === undefined && this.peerBootstrapFloor !== null) {
|
|
204
|
+
let pageToken = '';
|
|
205
|
+
const visited = new Set();
|
|
206
|
+
do {
|
|
207
|
+
const directory = await this.peers({ filter: 'all', ...(pageToken ? { page_token: pageToken } : {}) }, true);
|
|
208
|
+
if (this.peerContext() !== context)
|
|
209
|
+
throw new StateError('peer initialization invalidated');
|
|
210
|
+
if (this.isPeerInitialized(ns))
|
|
211
|
+
return;
|
|
212
|
+
if (directory.has_more !== true)
|
|
213
|
+
break;
|
|
214
|
+
const next = directory.next_page_token;
|
|
215
|
+
if (typeof next !== 'string' || !next || visited.has(next))
|
|
216
|
+
throw new ValidationError('invalid peer directory pagination');
|
|
217
|
+
visited.add(next);
|
|
218
|
+
pageToken = next;
|
|
219
|
+
} while (true);
|
|
220
|
+
}
|
|
221
|
+
const existing = this.peerInitializations.get(sender);
|
|
222
|
+
if (existing)
|
|
223
|
+
return await existing;
|
|
224
|
+
const task = (async () => {
|
|
225
|
+
const ack = initial ?? this.peerBootstrapFloor;
|
|
226
|
+
if (!Number.isSafeInteger(ack) || Number(ack) < 0)
|
|
227
|
+
throw new StateError('peer bootstrap is not ready');
|
|
228
|
+
const state = { ack: Number(ack), tail: Number(ack), head: Number(ack) };
|
|
229
|
+
if (typeof client._tokenStore.saveSyncStates !== 'function')
|
|
230
|
+
throw new StateError('peer cursor persistence unavailable');
|
|
231
|
+
await this.enqueueSeqTrackerWrite(async () => {
|
|
232
|
+
if (this.peerContext() !== context)
|
|
233
|
+
throw new StateError('peer initialization invalidated');
|
|
234
|
+
await client._tokenStore.saveSyncStates(...scope, { [ns]: state });
|
|
235
|
+
if (this.peerContext() === context)
|
|
236
|
+
this.committedSeqTrackerStates.set(ns, { ...state });
|
|
237
|
+
});
|
|
238
|
+
if (this.peerContext() !== context)
|
|
239
|
+
throw new StateError('peer initialization invalidated');
|
|
240
|
+
client._seqTracker.forceContiguousSeq(ns, Number(ack));
|
|
241
|
+
client._seqTracker.getSyncState(ns);
|
|
242
|
+
this.peerInitialized.add(ns);
|
|
243
|
+
})();
|
|
244
|
+
this.peerInitializations.set(sender, task);
|
|
245
|
+
try {
|
|
246
|
+
await task;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
if (this.peerInitializations.get(sender) === task)
|
|
250
|
+
this.peerInitializations.delete(sender);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
peerDurableAck(ns) {
|
|
254
|
+
return this.committedSeqTrackerAck(ns);
|
|
255
|
+
}
|
|
256
|
+
rememberCommittedSeqState(ns) {
|
|
257
|
+
this.peerContext();
|
|
258
|
+
if (!this.isReliableSeqNamespace(ns) || this.committedSeqTrackerStates.has(ns))
|
|
259
|
+
return;
|
|
260
|
+
this.committedSeqTrackerStates.set(ns, { ...this.syncState(ns) });
|
|
261
|
+
}
|
|
262
|
+
committedSeqTrackerAck(ns) {
|
|
263
|
+
this.peerContext();
|
|
264
|
+
return this.committedSeqTrackerStates.get(ns)?.ack ?? 0;
|
|
265
|
+
}
|
|
266
|
+
captureSeqNamespaceGeneration(ns) {
|
|
267
|
+
return JSON.stringify([this.peerContext(), this.seqTrackerNamespaceGenerations.get(ns) ?? 0]);
|
|
268
|
+
}
|
|
269
|
+
isSeqNamespaceGenerationCurrent(ns, generation) {
|
|
270
|
+
return this.captureSeqNamespaceGeneration(ns) === generation;
|
|
271
|
+
}
|
|
272
|
+
isReliableSeqNamespace(ns) {
|
|
273
|
+
return ns.startsWith('p2p.peer.v1/') || ns.startsWith('group:') || ns.startsWith('group_event:');
|
|
274
|
+
}
|
|
275
|
+
async peers(params = {}, background = false) {
|
|
276
|
+
const client = this.runtime.client;
|
|
277
|
+
const context = this.peerContext();
|
|
278
|
+
const generation = this.captureInlineGeneration();
|
|
279
|
+
const filter = String(params.filter ?? 'pending');
|
|
280
|
+
const key = JSON.stringify([context, filter, params.page_token ?? '', params.limit ?? null]);
|
|
281
|
+
const existing = this.peerDirectoryRequests.get(key);
|
|
282
|
+
if (existing) {
|
|
283
|
+
if (!background)
|
|
284
|
+
existing.application = true;
|
|
285
|
+
return await existing.promise;
|
|
286
|
+
}
|
|
287
|
+
const request = { application: !background, promise: Promise.resolve({}) };
|
|
288
|
+
request.promise = (async () => {
|
|
289
|
+
const result = await client._callRawV2Rpc('message.peers', { ...params, cursor_scope: 'peer', filter, ...(background ? { _rpc_background: true } : { _rpc_foreground: true }) });
|
|
290
|
+
if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
|
|
291
|
+
throw new StateError('peer directory invalidated');
|
|
292
|
+
if (result.catalog_ready !== true || !Array.isArray(result.peers) || (filter === 'pending' && result.has_more === true))
|
|
293
|
+
throw new StateError('peer catalog is not ready', { retryable: true });
|
|
294
|
+
if (result.cursor_scope !== 'peer' || (result.filter !== undefined && result.filter !== filter))
|
|
295
|
+
throw new ValidationError('peer directory scope mismatch');
|
|
296
|
+
for (const peer of result.peers) {
|
|
297
|
+
if (!peer || typeof peer !== 'object' || typeof peer.sender_aid !== 'string' || !peer.sender_aid.trim())
|
|
298
|
+
throw new ValidationError('invalid peer sender_aid');
|
|
299
|
+
const initial = peer.initial_after_seq ?? peer.server_ack_seq ?? result.bootstrap_floor_seq;
|
|
300
|
+
if (!Number.isSafeInteger(initial) || Number(initial) < 0)
|
|
301
|
+
throw new ValidationError('invalid peer initial_after_seq');
|
|
302
|
+
}
|
|
303
|
+
const floor = result.bootstrap_floor_seq;
|
|
304
|
+
if (!Number.isSafeInteger(floor) || Number(floor) < 0)
|
|
305
|
+
throw new ValidationError('invalid bootstrap_floor_seq');
|
|
306
|
+
if (this.peerBootstrapFloor === null) {
|
|
307
|
+
if (typeof client._tokenStore.updateInstanceState !== 'function')
|
|
308
|
+
throw new StateError('peer bootstrap persistence unavailable');
|
|
309
|
+
await client._tokenStore.updateInstanceState(client._aid, client._deviceId, client._slotId, (state) => {
|
|
310
|
+
const saved = state.p2p_peer_bootstrap_floor_seq;
|
|
311
|
+
if (Number.isSafeInteger(saved) && saved !== floor)
|
|
312
|
+
throw new StateError('peer bootstrap changed');
|
|
313
|
+
state.p2p_peer_bootstrap_floor_seq = floor;
|
|
314
|
+
});
|
|
315
|
+
if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
|
|
316
|
+
throw new StateError('peer bootstrap invalidated');
|
|
317
|
+
this.peerBootstrapFloor = Number(floor);
|
|
318
|
+
}
|
|
319
|
+
else if (this.peerBootstrapFloor !== floor)
|
|
320
|
+
throw new StateError('peer bootstrap changed');
|
|
321
|
+
const rows = result.peers;
|
|
322
|
+
for (const peer of rows)
|
|
323
|
+
await this.initializePeer(String(peer.sender_aid ?? ''), Number(peer.initial_after_seq ?? peer.server_ack_seq ?? floor));
|
|
324
|
+
if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
|
|
325
|
+
throw new StateError('peer directory invalidated');
|
|
326
|
+
for (const peer of rows) {
|
|
327
|
+
const ns = this.peerNamespace(String(peer.sender_aid));
|
|
328
|
+
const next = peer.next_pending_seq;
|
|
329
|
+
const confirmed = Math.max(this.peerPendingConfirmedThrough.get(ns) ?? 0, Number(peer.server_ack_seq ?? peer.initial_after_seq ?? floor));
|
|
330
|
+
this.peerPendingConfirmedThrough.set(ns, confirmed);
|
|
331
|
+
if (typeof next === 'number' && Number.isSafeInteger(next) && next >= confirmed)
|
|
332
|
+
this.peerPendingSeqHints.set(ns, Math.max(this.peerPendingSeqHints.get(ns) ?? next, next));
|
|
333
|
+
else if ((this.peerPendingSeqHints.get(ns) ?? Infinity) <= confirmed)
|
|
334
|
+
this.peerPendingSeqHints.delete(ns);
|
|
335
|
+
}
|
|
336
|
+
if (request.application || rows.length > 0) {
|
|
337
|
+
client._dispatcher.enqueue('message.peers_snapshot', {
|
|
338
|
+
aid: client._aid, device_id: client._deviceId, slot_id: client._slotId, cursor_scope: 'peer', filter,
|
|
339
|
+
trigger: request.application ? 'application' : 'background', catalog_revision: result.catalog_revision,
|
|
340
|
+
peers: rows.map((peer) => ({ ...peer })), peer_count: rows.length, bootstrap_floor_seq: floor,
|
|
341
|
+
...(result.pending_count_estimate === undefined ? {} : { pending_count_estimate: result.pending_count_estimate }),
|
|
342
|
+
...(filter === 'all' ? { has_more: result.has_more === true, next_page_token: result.next_page_token } : {}),
|
|
343
|
+
}, () => this.peerContext() === context && this.isInlineGenerationCurrent(generation));
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
})();
|
|
347
|
+
this.peerDirectoryRequests.set(key, request);
|
|
348
|
+
try {
|
|
349
|
+
return await request.promise;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
if (this.peerDirectoryRequests.get(key) === request)
|
|
353
|
+
this.peerDirectoryRequests.delete(key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async pullPeers(params = {}, background = false) {
|
|
357
|
+
const context = this.peerContext();
|
|
358
|
+
const generation = this.captureInlineGeneration();
|
|
359
|
+
const sender = String(params.sender_aid ?? params.peer_aid ?? '').trim();
|
|
360
|
+
if (!sender && ('after_seq' in params || 'after_volatile_cursor' in params || params.force === true || 'window_mode' in params))
|
|
361
|
+
throw new ValidationError('sender_aid is required for an explicit cursor or window');
|
|
362
|
+
const pending = sender && this.peerBootstrapFloor !== null && this.peerInitialized.has(this.peerNamespace(sender))
|
|
363
|
+
? [] : (await this.peers({}, background)).peers;
|
|
364
|
+
if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
|
|
365
|
+
throw new StateError('pull invalidated');
|
|
366
|
+
const peers = sender ? [{ sender_aid: sender }] : background ? this.orderBackgroundPeers(pending) : pending;
|
|
367
|
+
const budget = { remaining: Math.max(1, Math.trunc(Number(params.max_pages ?? 100) || 100)) };
|
|
368
|
+
const results = await Promise.all(peers.map(async (peer) => {
|
|
369
|
+
const aid = String(peer.sender_aid ?? '');
|
|
370
|
+
await this.initializePeer(aid);
|
|
371
|
+
try {
|
|
372
|
+
return await this.runtime.client._v2E2EE.pullPeerV2(aid, params, background, budget);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
|
|
376
|
+
throw new StateError('pull invalidated');
|
|
377
|
+
this.schedulePeerRecovery(aid, Number(peer.head_seq ?? 0), peer.volatile_has_more === true);
|
|
378
|
+
return { sender_aid: aid, messages: [], complete: false, error: String(error) };
|
|
379
|
+
}
|
|
380
|
+
}));
|
|
381
|
+
if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
|
|
382
|
+
throw new StateError('pull invalidated');
|
|
383
|
+
return { cursor_scope: 'peer', peers: results, messages: results.flatMap((peer) => peer.messages ?? []), partial: results.some((peer) => !peer.complete) };
|
|
384
|
+
}
|
|
385
|
+
schedulePeerRecovery(sender, head = 0, volatile = false) {
|
|
386
|
+
const client = this.runtime.client;
|
|
387
|
+
if (!sender || client._sessionOptions?.background_sync === false || client._closing)
|
|
388
|
+
return;
|
|
389
|
+
this.peerContext();
|
|
390
|
+
const work = this.peerRecoveries.get(sender) ?? { head: 0, volatile: false, running: false, retryAt: 0, pending: false };
|
|
391
|
+
work.head = Math.max(work.head, head);
|
|
392
|
+
work.volatile ||= volatile;
|
|
393
|
+
work.pending = true;
|
|
394
|
+
this.peerRecoveries.set(sender, work);
|
|
395
|
+
this.requestPeerRecovery();
|
|
396
|
+
}
|
|
397
|
+
requestPeerRecovery() {
|
|
398
|
+
const client = this.runtime.client;
|
|
399
|
+
if (!client._p2pPeerPullEnabled || client._sessionOptions?.background_sync === false || client._closing || client.state !== ConnectionState.READY)
|
|
400
|
+
return;
|
|
401
|
+
for (const [sender, work] of this.peerRecoveries) {
|
|
402
|
+
if (work.running)
|
|
403
|
+
continue;
|
|
404
|
+
if (work.retryAt > Date.now()) {
|
|
405
|
+
if (this.peerRecoveryTimer === null)
|
|
406
|
+
this.peerRecoveryTimer = setTimeout(() => { this.peerRecoveryTimer = null; this.requestBackgroundPullRecovery(); }, Math.max(1, work.retryAt - Date.now()));
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
work.running = true;
|
|
410
|
+
work.pending = false;
|
|
411
|
+
const generation = this.captureInlineGeneration();
|
|
412
|
+
client._safeAsync((async () => {
|
|
413
|
+
try {
|
|
414
|
+
if (this.peerBootstrapFloor === null)
|
|
415
|
+
await this.peers({}, true);
|
|
416
|
+
await this.initializePeer(sender);
|
|
417
|
+
const result = await client._v2E2EE.pullPeerV2(sender, {}, true, { remaining: 100 }, { retainMessages: false });
|
|
418
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
419
|
+
return;
|
|
420
|
+
if (result.complete && Number(result.ack_seq ?? 0) >= work.head && !work.pending)
|
|
421
|
+
this.peerRecoveries.delete(sender);
|
|
422
|
+
else
|
|
423
|
+
work.retryAt = result.complete && !work.pending ? Date.now() + 1000 : 0;
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
work.retryAt = Date.now() + 1000;
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
work.running = false;
|
|
430
|
+
if (this.isInlineGenerationCurrent(generation))
|
|
431
|
+
queueMicrotask(() => this.requestPeerRecovery());
|
|
432
|
+
}
|
|
433
|
+
})());
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async refreshPeerDirectory() {
|
|
437
|
+
const result = await this.peers({}, true);
|
|
438
|
+
for (const peer of this.orderBackgroundPeers(result.peers))
|
|
439
|
+
this.schedulePeerRecovery(String(peer.sender_aid ?? ''), Number(peer.head_seq ?? 0), peer.volatile_has_more === true);
|
|
440
|
+
}
|
|
441
|
+
orderBackgroundPeers(peers) {
|
|
442
|
+
const hint = (peer) => this.peerBackgroundPullPriority(this.peerNamespace(String(peer.sender_aid)));
|
|
443
|
+
return [...peers].sort((left, right) => hint(left) - hint(right));
|
|
444
|
+
}
|
|
445
|
+
peerBackgroundPullPriority(ns) {
|
|
446
|
+
this.peerContext();
|
|
447
|
+
if (!ns.startsWith('p2p.peer.v1/'))
|
|
448
|
+
return undefined;
|
|
449
|
+
return this.peerPendingSeqHints.get(ns) ?? Math.max(this.peerPendingConfirmedThrough.get(ns) ?? 0, this.runtime.client._seqTracker.getContiguousSeq(ns));
|
|
450
|
+
}
|
|
451
|
+
consumePeerBackgroundPullPriority(ns, covered, next) {
|
|
452
|
+
this.peerContext();
|
|
453
|
+
if (covered < (this.peerPendingConfirmedThrough.get(ns) ?? 0))
|
|
454
|
+
return;
|
|
455
|
+
this.peerPendingConfirmedThrough.set(ns, Math.max(this.peerPendingConfirmedThrough.get(ns) ?? 0, covered));
|
|
456
|
+
if ((this.peerPendingSeqHints.get(ns) ?? Infinity) <= covered)
|
|
457
|
+
this.peerPendingSeqHints.delete(ns);
|
|
458
|
+
if (next !== undefined)
|
|
459
|
+
this.peerPendingSeqHints.set(ns, Math.max(this.peerPendingSeqHints.get(ns) ?? next, next));
|
|
460
|
+
}
|
|
461
|
+
peerVolatileCursor(sender) { this.peerContext(); return this.peerVolatileCursors.get(sender) ?? ''; }
|
|
462
|
+
setPeerVolatileCursor(sender, cursor, page) { this.deferApplicationAfterDelivery(page, () => { this.peerContext(); this.peerVolatileCursors.set(sender, cursor); }); }
|
|
463
|
+
async ownPeerType() {
|
|
464
|
+
return String(await this.runtime.client._agentMdManager?.ownType?.() ?? '');
|
|
465
|
+
}
|
|
466
|
+
requestPeerDirectoryRefresh() {
|
|
467
|
+
this.peerDirectoryDirty = true;
|
|
468
|
+
const client = this.runtime.client;
|
|
469
|
+
if (this.peerDirectoryRefreshing || client._sessionOptions?.background_sync === false || client._closing || client.state !== ConnectionState.READY)
|
|
470
|
+
return;
|
|
471
|
+
this.peerDirectoryRefreshing = true;
|
|
472
|
+
const generation = this.captureInlineGeneration();
|
|
473
|
+
client._safeAsync((async () => {
|
|
474
|
+
try {
|
|
475
|
+
do {
|
|
476
|
+
this.peerDirectoryDirty = false;
|
|
477
|
+
await this.refreshPeerDirectory();
|
|
478
|
+
} while (this.peerDirectoryDirty && this.isInlineGenerationCurrent(generation));
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
this.peerDirectoryDirty = true;
|
|
482
|
+
if (this.peerRecoveryTimer === null)
|
|
483
|
+
this.peerRecoveryTimer = setTimeout(() => {
|
|
484
|
+
this.peerRecoveryTimer = null;
|
|
485
|
+
if (this.isInlineGenerationCurrent(generation))
|
|
486
|
+
this.requestBackgroundPullRecovery();
|
|
487
|
+
}, 1000);
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
this.peerDirectoryRefreshing = false;
|
|
491
|
+
}
|
|
492
|
+
})());
|
|
493
|
+
}
|
|
494
|
+
handlePeerNotification(data) {
|
|
495
|
+
const client = this.runtime.client;
|
|
496
|
+
if (!isJsonObject(data) || !this.messageTargetsCurrentInstance(data))
|
|
497
|
+
return;
|
|
498
|
+
const sender = String(data.sender_aid ?? data.from_aid ?? data.from ?? '').trim();
|
|
499
|
+
if (String(data.kind ?? '') === 'message.online_unread_hint' || !sender || data.cursor_scope !== 'peer') {
|
|
500
|
+
this.requestPeerDirectoryRefresh();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const seq = positiveSafeSequenceHint(data.seq);
|
|
504
|
+
const ns = this.peerNamespace(sender);
|
|
505
|
+
this.isPeerInitialized(ns);
|
|
506
|
+
const volatile = this.isVolatileMessage(data) || data.volatile_has_more === true;
|
|
507
|
+
if (seq > 0 && seq <= client._seqTracker.getContiguousSeq(ns) && !volatile)
|
|
508
|
+
return;
|
|
509
|
+
if (seq > 0)
|
|
510
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
511
|
+
const inline = this.inlineMessage(data);
|
|
512
|
+
if (inline && seq > 0 && !volatile) {
|
|
513
|
+
const generation = this.captureInlineGeneration();
|
|
514
|
+
client._safeAsync((async () => {
|
|
515
|
+
let consumed = false;
|
|
516
|
+
try {
|
|
517
|
+
if (this.peerBootstrapFloor === null)
|
|
518
|
+
await this.peers({}, true);
|
|
519
|
+
await this.initializePeer(sender);
|
|
520
|
+
consumed = await client._v2E2EE.serializePeerPage(ns, async () => {
|
|
521
|
+
const state = this.syncState(ns);
|
|
522
|
+
const previous = data.prev_peer_seq;
|
|
523
|
+
const expectedVersion = inline.version === 'v1' ? 'v1' : 'v2';
|
|
524
|
+
if (!this.isInlineGenerationCurrent(generation) || !Number.isSafeInteger(previous) || Number(previous) < 0
|
|
525
|
+
|| Number(previous) >= seq || Number(previous) > state.ack || state.tail !== state.ack || state.head !== state.ack
|
|
526
|
+
|| !this.inlineP2PBindingMatches(data, inline, seq, expectedVersion)
|
|
527
|
+
|| (inline.prev_peer_seq !== undefined && inline.prev_peer_seq !== previous))
|
|
528
|
+
return false;
|
|
529
|
+
const decoded = await client._v2E2EE.decodeInlineP2PMessage(inline);
|
|
530
|
+
if (!decoded || !this.isInlineGenerationCurrent(generation) || !this.decodedInlineP2PMatches(data, decoded, seq))
|
|
531
|
+
return false;
|
|
532
|
+
decoded.need_render = inline.need_render !== false;
|
|
533
|
+
const event = this.p2pAppEventForMessage(decoded);
|
|
534
|
+
await client._publishAppEvent(event.event, event.payload, 'inline_push', ns);
|
|
535
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
536
|
+
return false;
|
|
537
|
+
const snapshot = client._seqTracker.snapshotNamespace(ns);
|
|
538
|
+
try {
|
|
539
|
+
client._seqTracker.forceContiguousSeq(ns, Math.max(state.ack, seq));
|
|
540
|
+
await client._commitSeqTrackerState(ns);
|
|
541
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
if (this.isInlineGenerationCurrent(generation))
|
|
546
|
+
client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
await client._v2E2EE.ackPeerV2({ sender_aid: sender, up_to_seq: client._seqTracker.getContiguousSeq(ns) });
|
|
550
|
+
return true;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
consumed = false;
|
|
555
|
+
}
|
|
556
|
+
if (!consumed && this.isInlineGenerationCurrent(generation))
|
|
557
|
+
this.schedulePeerRecovery(sender, seq, false);
|
|
558
|
+
})());
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.schedulePeerRecovery(sender, seq, volatile);
|
|
562
|
+
}
|
|
123
563
|
runtime;
|
|
124
564
|
syncRun = 0;
|
|
125
565
|
syncingNamespaces = new Set();
|
|
@@ -133,11 +573,15 @@ export class MessageDeliveryEngine {
|
|
|
133
573
|
pendingPullNoProgressAcks = null;
|
|
134
574
|
onlineUnreadHintTargets = null;
|
|
135
575
|
onlineUnreadHintOwners = null;
|
|
576
|
+
backgroundPullDemands = new Map();
|
|
577
|
+
backgroundPullTimer = null;
|
|
578
|
+
backgroundPullRecoveryQueued = false;
|
|
136
579
|
realtimeAcking = null;
|
|
137
580
|
pendingP2PInlineAcks = null;
|
|
138
581
|
pendingGroupInlineAcks = null;
|
|
139
582
|
pendingGroupInlineAckNamespaces = null;
|
|
140
583
|
pendingTailWindows = new Map();
|
|
584
|
+
deferredApplicationPages = new Map();
|
|
141
585
|
volatileContext = '';
|
|
142
586
|
volatilePullCursorValue = '';
|
|
143
587
|
volatileSeenMessages = new Map();
|
|
@@ -149,10 +593,143 @@ export class MessageDeliveryEngine {
|
|
|
149
593
|
constructor(runtime) {
|
|
150
594
|
this.runtime = runtime;
|
|
151
595
|
}
|
|
596
|
+
beginApplicationPage(namespace, source = 'pull', reservation) {
|
|
597
|
+
const client = this.runtime.client;
|
|
598
|
+
if (typeof client._dispatcher?._snapshotApplicationHandlers !== 'function')
|
|
599
|
+
return undefined;
|
|
600
|
+
const generation = this.captureInlineGeneration();
|
|
601
|
+
const runtimeGeneration = Number(client._v2RuntimeGeneration ?? 0);
|
|
602
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(namespace);
|
|
603
|
+
let resolve;
|
|
604
|
+
let reject;
|
|
605
|
+
const done = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
606
|
+
void done.catch(() => { });
|
|
607
|
+
const handlers = client._dispatcher._snapshotApplicationHandlers(Object.values(APPLICATION_EVENT_FAMILIES));
|
|
608
|
+
const family = namespace.startsWith('group_event:') ? 'group.event_batch' : namespace.startsWith('group:') ? 'group.message_batch' : 'message.batch';
|
|
609
|
+
return { namespace, source: canonicalDeliverySource(source), handlers, batch: (handlers.get(family)?.length ?? 0) > 0,
|
|
610
|
+
items: [], effects: [], afterDelivery: [], cleanup: [], claims: new Map(), consumed: new Set(), seen: new Set(), done, resolve, reject, closed: false, flushed: false, deferred: false, reservation,
|
|
611
|
+
ensureCurrent: () => {
|
|
612
|
+
if (!this.isSeqNamespaceGenerationCurrent(namespace, namespaceGeneration) || !this.isInlineGenerationCurrent(generation)
|
|
613
|
+
|| Number(client._v2RuntimeGeneration ?? 0) !== runtimeGeneration
|
|
614
|
+
|| client._rpcPipeline?.isPullNamespaceCurrent?.(namespace) === false)
|
|
615
|
+
throw new StateError('delivery invalidated');
|
|
616
|
+
} };
|
|
617
|
+
}
|
|
618
|
+
async flushApplicationPage(page) {
|
|
619
|
+
if (!page)
|
|
620
|
+
return;
|
|
621
|
+
if (page.flushed)
|
|
622
|
+
return;
|
|
623
|
+
if (page.closed)
|
|
624
|
+
throw new StateError('delivery page closed');
|
|
625
|
+
if (page.deferred) {
|
|
626
|
+
page.ensureCurrent();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
page.ensureCurrent();
|
|
631
|
+
const family = page.items.length > 0 ? APPLICATION_EVENT_FAMILIES[page.items[0].event] : '';
|
|
632
|
+
const handlers = page.handlers.get(family) ?? [];
|
|
633
|
+
if (page.batch && page.items.length > 0 && handlers.length > 0) {
|
|
634
|
+
await this.runtime.client._dispatcher._publishAndWait(family, {
|
|
635
|
+
namespace: page.namespace, items: page.items, source: page.source,
|
|
636
|
+
}, handlers, page.reservation, () => { page.ensureCurrent(); return true; });
|
|
637
|
+
}
|
|
638
|
+
page.ensureCurrent();
|
|
639
|
+
for (const effect of page.effects) {
|
|
640
|
+
await effect();
|
|
641
|
+
page.ensureCurrent();
|
|
642
|
+
}
|
|
643
|
+
for (const effect of page.afterDelivery) {
|
|
644
|
+
await effect();
|
|
645
|
+
page.ensureCurrent();
|
|
646
|
+
}
|
|
647
|
+
page.closed = true;
|
|
648
|
+
page.flushed = true;
|
|
649
|
+
page.resolve();
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
this.abortApplicationPage(page, error);
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
for (const cleanup of page.cleanup.splice(0))
|
|
657
|
+
cleanup();
|
|
658
|
+
for (const key of page.claims.keys())
|
|
659
|
+
if (this.p2pDeliveryInflight.get(key) === page.done)
|
|
660
|
+
this.p2pDeliveryInflight.delete(key);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
abortApplicationPage(page, error) {
|
|
664
|
+
if (!page || page.closed)
|
|
665
|
+
return;
|
|
666
|
+
page.closed = true;
|
|
667
|
+
page.reject(error);
|
|
668
|
+
for (const cleanup of page.cleanup.splice(0))
|
|
669
|
+
cleanup();
|
|
670
|
+
for (const key of page.claims.keys())
|
|
671
|
+
if (this.p2pDeliveryInflight.get(key) === page.done)
|
|
672
|
+
this.p2pDeliveryInflight.delete(key);
|
|
673
|
+
}
|
|
674
|
+
deferApplicationEffect(page, effect) {
|
|
675
|
+
if (page?.batch) {
|
|
676
|
+
if (page.closed)
|
|
677
|
+
throw new StateError('delivery page closed');
|
|
678
|
+
page.effects.push(effect);
|
|
679
|
+
}
|
|
680
|
+
else
|
|
681
|
+
void effect();
|
|
682
|
+
}
|
|
683
|
+
deferApplicationAfterDelivery(page, effect) {
|
|
684
|
+
if (page?.batch) {
|
|
685
|
+
if (page.closed)
|
|
686
|
+
throw new StateError('delivery page closed');
|
|
687
|
+
page.afterDelivery.push(effect);
|
|
688
|
+
}
|
|
689
|
+
else
|
|
690
|
+
void effect();
|
|
691
|
+
}
|
|
692
|
+
async applicationOwnership(key, page, operation) {
|
|
693
|
+
if (page?.batch && page.claims.has(key))
|
|
694
|
+
return undefined;
|
|
695
|
+
for (;;) {
|
|
696
|
+
const pending = this.p2pDeliveryInflight.get(key);
|
|
697
|
+
if (!pending)
|
|
698
|
+
break;
|
|
699
|
+
await pending;
|
|
700
|
+
page?.ensureCurrent();
|
|
701
|
+
}
|
|
702
|
+
if (page?.batch) {
|
|
703
|
+
if (page.closed)
|
|
704
|
+
throw new StateError('delivery page closed');
|
|
705
|
+
page.claims.set(key, { sender_aid: '', message_id: '' });
|
|
706
|
+
this.p2pDeliveryInflight.set(key, page.done);
|
|
707
|
+
return await operation();
|
|
708
|
+
}
|
|
709
|
+
const task = Promise.resolve().then(operation);
|
|
710
|
+
const owner = task.then(() => undefined);
|
|
711
|
+
this.p2pDeliveryInflight.set(key, owner);
|
|
712
|
+
try {
|
|
713
|
+
return await task;
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
if (this.p2pDeliveryInflight.get(key) === owner)
|
|
717
|
+
this.p2pDeliveryInflight.delete(key);
|
|
718
|
+
void owner.catch(() => { });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
152
721
|
resetInlineAckState() {
|
|
153
722
|
this.syncStopped('aborted');
|
|
154
723
|
this.inlineGeneration += 1;
|
|
155
724
|
this.deliveryGeneration += 1;
|
|
725
|
+
this.peerRecoveries.clear();
|
|
726
|
+
this.peerPendingSeqHints.clear();
|
|
727
|
+
this.peerPendingConfirmedThrough.clear();
|
|
728
|
+
this.peerDirectoryDirty = false;
|
|
729
|
+
this.peerDirectoryRefreshing = false;
|
|
730
|
+
if (this.peerRecoveryTimer !== null)
|
|
731
|
+
clearTimeout(this.peerRecoveryTimer);
|
|
732
|
+
this.peerRecoveryTimer = null;
|
|
156
733
|
this.pendingPullDeliveryChanges = null;
|
|
157
734
|
void this.runtime.client._rpcPipeline?.invalidatePulls?.();
|
|
158
735
|
this.realtimeSyncing = null;
|
|
@@ -161,12 +738,21 @@ export class MessageDeliveryEngine {
|
|
|
161
738
|
this.pendingPullNoProgressAcks = null;
|
|
162
739
|
this.onlineUnreadHintTargets = null;
|
|
163
740
|
this.onlineUnreadHintOwners = null;
|
|
741
|
+
this.backgroundPullDemands.clear();
|
|
742
|
+
if (this.backgroundPullTimer !== null)
|
|
743
|
+
clearTimeout(this.backgroundPullTimer);
|
|
744
|
+
this.backgroundPullTimer = null;
|
|
745
|
+
this.backgroundPullRecoveryQueued = false;
|
|
164
746
|
this.realtimeTailResults = null;
|
|
165
747
|
this.realtimeAcking = null;
|
|
166
748
|
this.pendingP2PInlineAcks = null;
|
|
167
749
|
this.pendingGroupInlineAcks = null;
|
|
168
750
|
this.pendingGroupInlineAckNamespaces = null;
|
|
169
751
|
this.pendingTailWindows.clear();
|
|
752
|
+
for (const pages of this.deferredApplicationPages.values())
|
|
753
|
+
for (const page of new Set(pages.values()))
|
|
754
|
+
this.abortApplicationPage(page, new StateError('delivery invalidated'));
|
|
755
|
+
this.deferredApplicationPages.clear();
|
|
170
756
|
this.syncingNamespaces.clear();
|
|
171
757
|
this.syncSessions.clear();
|
|
172
758
|
}
|
|
@@ -189,34 +775,57 @@ export class MessageDeliveryEngine {
|
|
|
189
775
|
captureInlineGeneration() {
|
|
190
776
|
return this.inlineGeneration;
|
|
191
777
|
}
|
|
192
|
-
deferTailWindow(ns, proof) {
|
|
778
|
+
deferTailWindow(ns, proof, page) {
|
|
193
779
|
const tracker = new SeqTracker();
|
|
194
780
|
tracker.restoreNamespaceSnapshot(ns, this.runtime.client._seqTracker.snapshotNamespace(ns));
|
|
195
781
|
tracker.commitTailWindow(ns, proof);
|
|
782
|
+
if (page)
|
|
783
|
+
page.tailProof = proof;
|
|
196
784
|
this.pendingTailWindows.set(ns, proof);
|
|
197
785
|
}
|
|
786
|
+
pendingTailProofs(ns) {
|
|
787
|
+
const proofs = new Set();
|
|
788
|
+
const latest = this.pendingTailWindows.get(ns);
|
|
789
|
+
if (latest)
|
|
790
|
+
proofs.add(latest);
|
|
791
|
+
for (const page of this.deferredApplicationPages.get(ns)?.values() ?? [])
|
|
792
|
+
if (page.tailProof)
|
|
793
|
+
proofs.add(page.tailProof);
|
|
794
|
+
return [...proofs].sort((a, b) => a.windowStartSeq - b.windowStartSeq);
|
|
795
|
+
}
|
|
198
796
|
prospectiveTailState(ns) {
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
797
|
+
const proofs = this.pendingTailProofs(ns);
|
|
798
|
+
if (proofs.length === 0)
|
|
201
799
|
return this.syncState(ns);
|
|
202
800
|
const tracker = new SeqTracker();
|
|
203
801
|
tracker.restoreNamespaceSnapshot(ns, this.runtime.client._seqTracker.snapshotNamespace(ns));
|
|
204
|
-
|
|
802
|
+
for (const proof of proofs)
|
|
803
|
+
tracker.commitTailWindow(ns, proof);
|
|
205
804
|
return tracker.getSyncState(ns);
|
|
206
805
|
}
|
|
207
806
|
pendingTailDeliveryThrough(ns, scanThrough) {
|
|
208
|
-
const proof
|
|
209
|
-
|
|
807
|
+
for (const proof of this.pendingTailProofs(ns))
|
|
808
|
+
if (scanThrough >= proof.windowStartSeq - 1)
|
|
809
|
+
scanThrough = Math.max(scanThrough, proof.headSeq);
|
|
810
|
+
return scanThrough;
|
|
210
811
|
}
|
|
211
812
|
commitPendingTailWindow(ns, scanThrough) {
|
|
212
|
-
const proof
|
|
213
|
-
|
|
214
|
-
|
|
813
|
+
for (const proof of this.pendingTailProofs(ns))
|
|
814
|
+
if (scanThrough >= proof.windowStartSeq - 1) {
|
|
815
|
+
this.runtime.client._seqTracker.commitTailWindow(ns, proof);
|
|
816
|
+
scanThrough = Math.max(scanThrough, proof.headSeq);
|
|
817
|
+
}
|
|
215
818
|
}
|
|
216
819
|
completePendingTailWindow(ns) {
|
|
217
820
|
const proof = this.pendingTailWindows.get(ns);
|
|
218
821
|
if (proof && this.runtime.client._seqTracker.getContiguousSeq(ns) >= proof.headSeq)
|
|
219
822
|
this.pendingTailWindows.delete(ns);
|
|
823
|
+
const pages = this.deferredApplicationPages.get(ns);
|
|
824
|
+
for (const [seq, page] of pages ?? [])
|
|
825
|
+
if (page.flushed && seq <= this.runtime.client._seqTracker.getContiguousSeq(ns))
|
|
826
|
+
pages.delete(seq);
|
|
827
|
+
if (pages?.size === 0)
|
|
828
|
+
this.deferredApplicationPages.delete(ns);
|
|
220
829
|
}
|
|
221
830
|
beginRealtimeSync(ns) {
|
|
222
831
|
if (!ns || this.realtimeSyncing?.has(ns))
|
|
@@ -326,9 +935,116 @@ export class MessageDeliveryEngine {
|
|
|
326
935
|
if (!ns)
|
|
327
936
|
return;
|
|
328
937
|
this.schedulePendingPullIfNeeded(ns, 'pull-gate-idle');
|
|
938
|
+
this.requestBackgroundPullRecovery();
|
|
939
|
+
}
|
|
940
|
+
recordBackgroundPullDemand(ns, throughSeq = 0, probe = false) {
|
|
941
|
+
const demand = this.backgroundPullDemands.get(ns) ?? {
|
|
942
|
+
throughSeq: 0, hasMore: false, probe: false, afterSeq: -1, retryAt: 0,
|
|
943
|
+
};
|
|
944
|
+
demand.throughSeq = Math.max(demand.throughSeq, positiveSafeSequenceHint(throughSeq));
|
|
945
|
+
demand.probe ||= probe;
|
|
946
|
+
this.backgroundPullDemands.set(ns, demand);
|
|
947
|
+
return demand;
|
|
948
|
+
}
|
|
949
|
+
observeBackgroundPullResponse(ns, afterSeq, result, limit = 50) {
|
|
950
|
+
if (!ns || this.runtime.client._sessionOptions?.background_sync === false)
|
|
951
|
+
return;
|
|
952
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
953
|
+
const rows = Array.isArray(result.messages) ? result.messages : Array.isArray(result.events) ? result.events : [];
|
|
954
|
+
let throughSeq = Math.max(positiveSafeSequenceHint(cursor?.latest_seq), positiveSafeSequenceHint(result.latest_seq), positiveSafeSequenceHint(result.latest_message_seq), positiveSafeSequenceHint(result.latest_event_seq));
|
|
955
|
+
for (const row of rows) {
|
|
956
|
+
if (!isJsonObject(row) || this.isVolatileMessage(row))
|
|
957
|
+
continue;
|
|
958
|
+
throughSeq = Math.max(throughSeq, positiveSafeSequenceHint(row.seq ?? row.event_seq));
|
|
959
|
+
}
|
|
960
|
+
const demand = this.recordBackgroundPullDemand(ns, throughSeq);
|
|
961
|
+
if (afterSeq >= demand.afterSeq) {
|
|
962
|
+
demand.afterSeq = afterSeq;
|
|
963
|
+
demand.hasMore = result.has_more === true
|
|
964
|
+
|| positiveSafeSequenceHint(result.remaining) > 0
|
|
965
|
+
|| (!Object.prototype.hasOwnProperty.call(result, 'has_more') && limit > 0 && rows.length >= limit);
|
|
966
|
+
demand.probe = false;
|
|
967
|
+
}
|
|
968
|
+
this.requestBackgroundPullRecovery();
|
|
969
|
+
}
|
|
970
|
+
finishBackgroundPull(ns, afterSeq, failed) {
|
|
971
|
+
const demand = this.backgroundPullDemands.get(ns);
|
|
972
|
+
if (demand) {
|
|
973
|
+
const state = this.syncState(ns);
|
|
974
|
+
const ack = state.ack;
|
|
975
|
+
if (ack < state.tail - 1)
|
|
976
|
+
demand.throughSeq = Math.max(demand.throughSeq, state.tail - 1);
|
|
977
|
+
demand.probe = failed;
|
|
978
|
+
if (!demand.probe && !demand.hasMore && ack >= demand.throughSeq)
|
|
979
|
+
this.backgroundPullDemands.delete(ns);
|
|
980
|
+
else
|
|
981
|
+
demand.retryAt = failed || ack <= afterSeq || ack <= (this.pendingPullNoProgressAcks?.get(ns) ?? -1)
|
|
982
|
+
? Date.now() + P2P_GAP_FILL_RETRY_BASE_MS
|
|
983
|
+
: 0;
|
|
984
|
+
}
|
|
985
|
+
this.requestBackgroundPullRecovery();
|
|
986
|
+
}
|
|
987
|
+
requestBackgroundPullRecovery() {
|
|
988
|
+
this.requestPeerRecovery();
|
|
989
|
+
if (this.runtime.client._p2pPeerPullEnabled && this.peerDirectoryDirty)
|
|
990
|
+
this.requestPeerDirectoryRefresh();
|
|
991
|
+
if (this.backgroundPullRecoveryQueued)
|
|
992
|
+
return;
|
|
993
|
+
const generation = this.captureInlineGeneration();
|
|
994
|
+
this.backgroundPullRecoveryQueued = true;
|
|
995
|
+
void Promise.resolve().then(() => {
|
|
996
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
997
|
+
return;
|
|
998
|
+
this.backgroundPullRecoveryQueued = false;
|
|
999
|
+
const client = this.runtime.client;
|
|
1000
|
+
if (!client._aid || client._sessionOptions?.background_sync === false
|
|
1001
|
+
|| client.state !== ConnectionState.READY || client._closing)
|
|
1002
|
+
return;
|
|
1003
|
+
if (this.backgroundPullTimer !== null)
|
|
1004
|
+
clearTimeout(this.backgroundPullTimer);
|
|
1005
|
+
this.backgroundPullTimer = null;
|
|
1006
|
+
let retryAt = 0;
|
|
1007
|
+
for (const [ns, demand] of this.backgroundPullDemands) {
|
|
1008
|
+
if (ns.startsWith('p2p:') && ns !== `p2p:${client._aid}`)
|
|
1009
|
+
continue;
|
|
1010
|
+
const dedupKey = ns.startsWith('p2p:') ? `p2p_pull:${ns}`
|
|
1011
|
+
: ns.startsWith('group_event:') ? `group_event_pull:${ns}` : `group_pull:${ns}`;
|
|
1012
|
+
if (client._gapFillDone.has(dedupKey) || this.onlineUnreadHintOwners?.has(ns)
|
|
1013
|
+
|| this.realtimeSyncing?.has(ns) || client._rpcPipeline?.hasPullActivity?.(ns, false))
|
|
1014
|
+
continue;
|
|
1015
|
+
const ack = this.syncState(ns).ack;
|
|
1016
|
+
if (!demand.probe && !demand.hasMore && ack >= demand.throughSeq) {
|
|
1017
|
+
this.backgroundPullDemands.delete(ns);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (demand.retryAt > Date.now()) {
|
|
1021
|
+
retryAt = retryAt === 0 ? demand.retryAt : Math.min(retryAt, demand.retryAt);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (ns.startsWith('p2p:'))
|
|
1025
|
+
client._safeAsync(this.fillP2pGap());
|
|
1026
|
+
else if (ns.startsWith('group_event:'))
|
|
1027
|
+
client._safeAsync(this.fillGroupEventGap(ns.slice('group_event:'.length)));
|
|
1028
|
+
else if (ns.startsWith('group:'))
|
|
1029
|
+
client._safeAsync(this.fillGroupGap(ns.slice('group:'.length)));
|
|
1030
|
+
}
|
|
1031
|
+
if (retryAt > 0) {
|
|
1032
|
+
this.backgroundPullTimer = globalThis.setTimeout(() => {
|
|
1033
|
+
this.backgroundPullTimer = null;
|
|
1034
|
+
if (this.isInlineGenerationCurrent(generation))
|
|
1035
|
+
this.requestBackgroundPullRecovery();
|
|
1036
|
+
}, Math.max(0, retryAt - Date.now()));
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
329
1039
|
}
|
|
330
1040
|
onForegroundPullsDrained() {
|
|
1041
|
+
this.requestBackgroundPullRecovery();
|
|
331
1042
|
const client = this.runtime.client;
|
|
1043
|
+
if (client._p2pPeerPullEnabled) {
|
|
1044
|
+
if (this.peerDirectoryDirty)
|
|
1045
|
+
this.requestPeerDirectoryRefresh();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
332
1048
|
if (!client._aid || client._sessionOptions?.background_sync === false
|
|
333
1049
|
|| client.state !== ConnectionState.READY || client._closing)
|
|
334
1050
|
return;
|
|
@@ -340,6 +1056,7 @@ export class MessageDeliveryEngine {
|
|
|
340
1056
|
client._safeAsync(this.fillP2pGap());
|
|
341
1057
|
}
|
|
342
1058
|
onPullWorkSettled() {
|
|
1059
|
+
this.requestBackgroundPullRecovery();
|
|
343
1060
|
const pipeline = this.runtime.client._rpcPipeline;
|
|
344
1061
|
if (pipeline?.hasAnyPullActivity?.() === true)
|
|
345
1062
|
return;
|
|
@@ -537,7 +1254,12 @@ export class MessageDeliveryEngine {
|
|
|
537
1254
|
client._pushedSeqs.set(ns, new Set(keep));
|
|
538
1255
|
}
|
|
539
1256
|
}
|
|
540
|
-
markPublishedSeq(ns, seq) {
|
|
1257
|
+
markPublishedSeq(ns, seq, page) {
|
|
1258
|
+
if (page?.batch) {
|
|
1259
|
+
page.consumed.add(seq);
|
|
1260
|
+
this.deferApplicationEffect(page, () => this.markPublishedSeq(ns, seq));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
541
1263
|
const client = this.runtime.client;
|
|
542
1264
|
let pushed = client._pushedSeqs.get(ns);
|
|
543
1265
|
if (!pushed) {
|
|
@@ -984,12 +1706,12 @@ export class MessageDeliveryEngine {
|
|
|
984
1706
|
throw new Error(`${method} response must be an object`);
|
|
985
1707
|
}
|
|
986
1708
|
const client = this.runtime.client;
|
|
1709
|
+
this.rememberCommittedSeqState(ns);
|
|
1710
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
987
1711
|
this.ensurePullOperationCurrent(ns);
|
|
1712
|
+
const applicationPage = this.beginApplicationPage(ns);
|
|
988
1713
|
const response = result;
|
|
989
1714
|
const volatileMessages = this.volatileMessages(response.messages);
|
|
990
|
-
if (method === 'message.pull' && volatileMessages.length > 0) {
|
|
991
|
-
await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
|
|
992
|
-
}
|
|
993
1715
|
const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
|
|
994
1716
|
const coordinator = this.forwardCoordinator();
|
|
995
1717
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
@@ -1016,12 +1738,14 @@ export class MessageDeliveryEngine {
|
|
|
1016
1738
|
let publishedCount = 0;
|
|
1017
1739
|
let committed = false;
|
|
1018
1740
|
try {
|
|
1741
|
+
if (method === 'message.pull' && volatileMessages.length > 0)
|
|
1742
|
+
await this.publishPlainVolatileMessages(volatileMessages, 'pull', true, applicationPage);
|
|
1019
1743
|
for (const rawMessage of messages) {
|
|
1020
1744
|
this.ensurePullOperationCurrent(ns);
|
|
1021
1745
|
const seq = positiveSafeSequenceHint(rawMessage.seq);
|
|
1022
1746
|
if (method === 'message.pull') {
|
|
1023
1747
|
const appEvent = p2pAppEventFromPlainPullMessage(rawMessage);
|
|
1024
|
-
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
1748
|
+
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false, 'pull', undefined, applicationPage)) {
|
|
1025
1749
|
publishedCount += 1;
|
|
1026
1750
|
}
|
|
1027
1751
|
this.ensurePullOperationCurrent(ns);
|
|
@@ -1029,16 +1753,16 @@ export class MessageDeliveryEngine {
|
|
|
1029
1753
|
}
|
|
1030
1754
|
const message = normalizeGroupMentionMode(rawMessage);
|
|
1031
1755
|
if (this.recallEventFromGroupMessage(message)) {
|
|
1032
|
-
if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
|
|
1756
|
+
if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull', applicationPage)) {
|
|
1033
1757
|
this.ensurePullOperationCurrent(ns);
|
|
1034
|
-
this.markPublishedSeq(ns, seq);
|
|
1758
|
+
this.markPublishedSeq(ns, seq, applicationPage);
|
|
1035
1759
|
publishedCount += 1;
|
|
1036
1760
|
}
|
|
1037
1761
|
}
|
|
1038
1762
|
else if (this.isSelfSentGroupMessage(message)) {
|
|
1039
|
-
this.markPublishedSeq(ns, seq);
|
|
1763
|
+
this.markPublishedSeq(ns, seq, applicationPage);
|
|
1040
1764
|
}
|
|
1041
|
-
else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
|
|
1765
|
+
else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false, 'pull', undefined, applicationPage)) {
|
|
1042
1766
|
this.ensurePullOperationCurrent(ns);
|
|
1043
1767
|
publishedCount += 1;
|
|
1044
1768
|
}
|
|
@@ -1050,10 +1774,13 @@ export class MessageDeliveryEngine {
|
|
|
1050
1774
|
const scanThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, retentionFloor, visibilityFloor, deferredServerCursor, ...messages.map((message) => positiveSafeSequenceHint(message.seq)));
|
|
1051
1775
|
const state = this.syncState(ns);
|
|
1052
1776
|
const deliveryThrough = this.pendingTailDeliveryThrough(ns, scanThrough >= state.tail - 1 ? Math.max(scanThrough, state.head) : scanThrough);
|
|
1053
|
-
await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough);
|
|
1777
|
+
await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough, applicationPage);
|
|
1778
|
+
await this.flushApplicationPage(applicationPage);
|
|
1054
1779
|
this.ensurePullOperationCurrent(ns);
|
|
1055
1780
|
if (!this.isInlineGenerationCurrent(pageInlineGeneration))
|
|
1056
1781
|
throw new Error('pull invalidated');
|
|
1782
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
1783
|
+
throw new Error('pull invalidated');
|
|
1057
1784
|
pageTrackerSnapshot = client._seqTracker.snapshotNamespace(ns);
|
|
1058
1785
|
this.commitPendingTailWindow(ns, scanThrough);
|
|
1059
1786
|
if (contiguousSeq === undefined)
|
|
@@ -1068,14 +1795,16 @@ export class MessageDeliveryEngine {
|
|
|
1068
1795
|
this.ensurePullOperationCurrent(ns);
|
|
1069
1796
|
await client._commitSeqTrackerState(ns);
|
|
1070
1797
|
this.ensurePullOperationCurrent(ns);
|
|
1071
|
-
if (!this.isInlineGenerationCurrent(pageInlineGeneration))
|
|
1798
|
+
if (!this.isInlineGenerationCurrent(pageInlineGeneration) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
1072
1799
|
throw new Error('pull invalidated');
|
|
1073
1800
|
}
|
|
1074
1801
|
this.completePendingTailWindow(ns);
|
|
1075
1802
|
committed = true;
|
|
1076
1803
|
}
|
|
1077
1804
|
catch (exc) {
|
|
1805
|
+
this.abortApplicationPage(applicationPage, exc);
|
|
1078
1806
|
if (this.isPullOperationCurrent(ns) && this.isInlineGenerationCurrent(pageInlineGeneration)
|
|
1807
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
1079
1808
|
&& pageTrackerSnapshot !== undefined && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
1080
1809
|
client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
|
|
1081
1810
|
this.dropSeqTrackerPending(ns);
|
|
@@ -1138,49 +1867,73 @@ export class MessageDeliveryEngine {
|
|
|
1138
1867
|
isGroupEventNamespace(ns) {
|
|
1139
1868
|
return ns.startsWith('group_event:');
|
|
1140
1869
|
}
|
|
1141
|
-
async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false, source = 'push', batch) {
|
|
1870
|
+
async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false, source = 'push', batch, page) {
|
|
1142
1871
|
const client = this.runtime.client;
|
|
1143
1872
|
const pullScoped = this.isPullScopedSource(source);
|
|
1144
1873
|
if (pullScoped)
|
|
1145
1874
|
this.ensurePullOperationCurrent(ns);
|
|
1146
1875
|
if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
|
|
1147
|
-
await this.publishOrderedGroupChanged(payload, source);
|
|
1876
|
+
await this.publishOrderedGroupChanged(payload, source, page, ns);
|
|
1148
1877
|
if (pullScoped)
|
|
1149
1878
|
this.ensurePullOperationCurrent(ns);
|
|
1150
1879
|
return;
|
|
1151
1880
|
}
|
|
1152
1881
|
if (event === 'message.recalled') {
|
|
1153
|
-
await this.publishMessageRecallTombstone(seq, payload, source);
|
|
1882
|
+
await this.publishMessageRecallTombstone(seq, payload, source, page);
|
|
1154
1883
|
if (pullScoped)
|
|
1155
1884
|
this.ensurePullOperationCurrent(ns);
|
|
1156
1885
|
return;
|
|
1157
1886
|
}
|
|
1158
1887
|
if (pullResponse) {
|
|
1159
|
-
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
|
|
1888
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch, page));
|
|
1160
1889
|
if (pullScoped)
|
|
1161
1890
|
this.ensurePullOperationCurrent(ns);
|
|
1162
1891
|
return;
|
|
1163
1892
|
}
|
|
1164
|
-
await client._publishAppEvent(event, payload, source, ns, batch);
|
|
1893
|
+
await client._publishAppEvent(event, payload, source, ns, batch, page);
|
|
1165
1894
|
if (pullScoped)
|
|
1166
1895
|
this.ensurePullOperationCurrent(ns);
|
|
1167
1896
|
}
|
|
1168
|
-
async publishOrderedGroupChanged(payload, source = 'ordered') {
|
|
1897
|
+
async publishOrderedGroupChanged(payload, source = 'ordered', page, ns = '', owned = false) {
|
|
1169
1898
|
const client = this.runtime.client;
|
|
1899
|
+
if (!owned && isJsonObject(payload) && Number.isSafeInteger(payload.event_seq) && Number(payload.event_seq) > 0) {
|
|
1900
|
+
const key = JSON.stringify([this.peerContext(), 'group.changed.action', ns || this.applicationNamespace('group.changed', payload), payload.event_seq]);
|
|
1901
|
+
const batchActive = page ? page.batch : (client._dispatcher?._snapshotApplicationHandlers?.(['group.event_batch']).get('group.event_batch')?.length ?? 0) > 0;
|
|
1902
|
+
if (batchActive || this.p2pDeliveryInflight.has(key)) {
|
|
1903
|
+
await this.applicationOwnership(key, page, async () => {
|
|
1904
|
+
if (this.p2pDeliveryCompleted.has(key))
|
|
1905
|
+
return;
|
|
1906
|
+
await this.publishOrderedGroupChanged(payload, source, page, ns, true);
|
|
1907
|
+
this.deferApplicationEffect(page, () => {
|
|
1908
|
+
this.p2pDeliveryCompleted.set(key, { sender_aid: '', message_id: '' });
|
|
1909
|
+
if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT)
|
|
1910
|
+
this.p2pDeliveryCompleted.delete(this.p2pDeliveryCompleted.keys().next().value);
|
|
1911
|
+
});
|
|
1912
|
+
});
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1170
1916
|
const generation = this.captureInlineGeneration();
|
|
1171
|
-
await client._publishAppEvent('group.changed', payload, source);
|
|
1917
|
+
await client._publishAppEvent('group.changed', payload, source, ns, undefined, page);
|
|
1172
1918
|
if (!this.isInlineGenerationCurrent(generation))
|
|
1173
1919
|
throw new Error('pull invalidated');
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1920
|
+
this.deferApplicationEffect(page, () => {
|
|
1921
|
+
if (isJsonObject(payload)) {
|
|
1922
|
+
const eventPayload = payload;
|
|
1923
|
+
client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
|
|
1924
|
+
if (eventPayload.action === 'dissolved') {
|
|
1925
|
+
const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
|
|
1926
|
+
const groupId = String(rawGroupId).trim();
|
|
1927
|
+
if (groupId) {
|
|
1928
|
+
if (page)
|
|
1929
|
+
page.cleanup.push(() => { if (page.flushed)
|
|
1930
|
+
client._cleanupDissolvedGroup?.(groupId); });
|
|
1931
|
+
else
|
|
1932
|
+
client._cleanupDissolvedGroup?.(groupId);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1182
1935
|
}
|
|
1183
|
-
}
|
|
1936
|
+
});
|
|
1184
1937
|
}
|
|
1185
1938
|
isInstanceScopedMessageEvent(event) {
|
|
1186
1939
|
return event === 'message.received'
|
|
@@ -1297,6 +2050,7 @@ export class MessageDeliveryEngine {
|
|
|
1297
2050
|
if (protectedHeaders)
|
|
1298
2051
|
envelope.protected_headers = protectedHeaders;
|
|
1299
2052
|
setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
|
|
2053
|
+
setIfPresent('need_render', message.need_render !== false);
|
|
1300
2054
|
setIfPresent('mention_mode', message.mention_mode);
|
|
1301
2055
|
return envelope;
|
|
1302
2056
|
}
|
|
@@ -1365,6 +2119,7 @@ export class MessageDeliveryEngine {
|
|
|
1365
2119
|
if (protectedHeaders)
|
|
1366
2120
|
envelope.protected_headers = protectedHeaders;
|
|
1367
2121
|
setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
|
|
2122
|
+
setIfPresent('need_render', params.need_render !== false);
|
|
1368
2123
|
if (method === 'group.send')
|
|
1369
2124
|
setIfPresent('mention_mode', resultObj.mention_mode);
|
|
1370
2125
|
return envelope;
|
|
@@ -1510,7 +2265,7 @@ export class MessageDeliveryEngine {
|
|
|
1510
2265
|
const sender = String(message.from_aid ?? message.from ?? '').trim();
|
|
1511
2266
|
const messageId = String(message.message_id ?? '').trim();
|
|
1512
2267
|
const cursor = String(message.volatile_cursor ?? '').trim();
|
|
1513
|
-
return `${sender}|${messageId
|
|
2268
|
+
return `${sender}|${messageId}|${cursor}`;
|
|
1514
2269
|
}
|
|
1515
2270
|
isVolatileSeen(message) {
|
|
1516
2271
|
this.ensureVolatileContext();
|
|
@@ -1521,7 +2276,12 @@ export class MessageDeliveryEngine {
|
|
|
1521
2276
|
}
|
|
1522
2277
|
return this.volatileSeenMessages.has(this.volatileSeenKey(message));
|
|
1523
2278
|
}
|
|
1524
|
-
markVolatileSeen(message) {
|
|
2279
|
+
markVolatileSeen(message, page) {
|
|
2280
|
+
if (page?.batch) {
|
|
2281
|
+
page.seen.add(`volatile:${this.volatileSeenKey(message)}`);
|
|
2282
|
+
this.deferApplicationEffect(page, () => this.markVolatileSeen(message));
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
1525
2285
|
this.ensureVolatileContext();
|
|
1526
2286
|
const now = Date.now();
|
|
1527
2287
|
const expiresAt = Math.max(now + 60_000, Number(message.expires_at ?? now + 1_800_000) || now + 1_800_000);
|
|
@@ -1533,16 +2293,37 @@ export class MessageDeliveryEngine {
|
|
|
1533
2293
|
this.volatileSeenMessages.delete(oldest);
|
|
1534
2294
|
}
|
|
1535
2295
|
}
|
|
1536
|
-
|
|
2296
|
+
async waitVolatileApplicationDelivery(message, page) {
|
|
2297
|
+
const key = JSON.stringify([this.peerContext(), 'volatile', this.volatileSeenKey(message)]);
|
|
2298
|
+
if (page?.claims.has(key))
|
|
2299
|
+
return;
|
|
2300
|
+
const pending = this.p2pDeliveryInflight.get(key);
|
|
2301
|
+
if (pending) {
|
|
2302
|
+
await pending;
|
|
2303
|
+
page?.ensureCurrent();
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
beginVolatileDelivery(message, page) {
|
|
2307
|
+
if (page?.seen.has(`volatile:${this.volatileSeenKey(message)}`))
|
|
2308
|
+
return 'seen';
|
|
1537
2309
|
if (this.isVolatileSeen(message))
|
|
1538
2310
|
return 'seen';
|
|
1539
2311
|
const key = this.volatileSeenKey(message);
|
|
1540
2312
|
if (this.volatileInFlightMessages.has(key))
|
|
1541
2313
|
return 'inflight';
|
|
1542
2314
|
this.volatileInFlightMessages.add(key);
|
|
2315
|
+
if (page?.batch) {
|
|
2316
|
+
const ownerKey = JSON.stringify([this.peerContext(), 'volatile', key]);
|
|
2317
|
+
page.claims.set(ownerKey, { sender_aid: '', message_id: '' });
|
|
2318
|
+
this.p2pDeliveryInflight.set(ownerKey, page.done);
|
|
2319
|
+
}
|
|
1543
2320
|
return 'claimed';
|
|
1544
2321
|
}
|
|
1545
|
-
finishVolatileDelivery(message) {
|
|
2322
|
+
finishVolatileDelivery(message, page) {
|
|
2323
|
+
if (page?.batch) {
|
|
2324
|
+
page.cleanup.push(() => this.finishVolatileDelivery(message));
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
1546
2327
|
this.ensureVolatileContext();
|
|
1547
2328
|
this.volatileInFlightMessages.delete(this.volatileSeenKey(message));
|
|
1548
2329
|
}
|
|
@@ -1551,7 +2332,11 @@ export class MessageDeliveryEngine {
|
|
|
1551
2332
|
delete result.seq;
|
|
1552
2333
|
return result;
|
|
1553
2334
|
}
|
|
1554
|
-
async ackVolatileMessages(messages, v2, advanceCursor) {
|
|
2335
|
+
async ackVolatileMessages(messages, v2, advanceCursor, page) {
|
|
2336
|
+
if (page?.batch) {
|
|
2337
|
+
this.deferApplicationAfterDelivery(page, async () => { await this.ackVolatileMessages(messages, v2, advanceCursor); });
|
|
2338
|
+
return undefined;
|
|
2339
|
+
}
|
|
1555
2340
|
const cursors = [...new Set(messages
|
|
1556
2341
|
.map((item) => String(item.volatile_cursor ?? '').trim())
|
|
1557
2342
|
.filter(Boolean))];
|
|
@@ -1565,11 +2350,12 @@ export class MessageDeliveryEngine {
|
|
|
1565
2350
|
}
|
|
1566
2351
|
return result;
|
|
1567
2352
|
}
|
|
1568
|
-
async publishPlainVolatileMessages(messages, source, advanceCursor) {
|
|
2353
|
+
async publishPlainVolatileMessages(messages, source, advanceCursor, page) {
|
|
1569
2354
|
const processed = [];
|
|
1570
2355
|
for (const rawMessage of messages) {
|
|
1571
2356
|
const message = this.publicVolatileMessage(rawMessage);
|
|
1572
|
-
|
|
2357
|
+
await this.waitVolatileApplicationDelivery(message, page);
|
|
2358
|
+
const claim = this.beginVolatileDelivery(message, page);
|
|
1573
2359
|
if (claim === 'seen') {
|
|
1574
2360
|
processed.push(rawMessage);
|
|
1575
2361
|
continue;
|
|
@@ -1578,16 +2364,16 @@ export class MessageDeliveryEngine {
|
|
|
1578
2364
|
continue;
|
|
1579
2365
|
try {
|
|
1580
2366
|
const appEvent = this.p2pAppEventForMessage(message);
|
|
1581
|
-
await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
1582
|
-
this.markVolatileSeen(message);
|
|
2367
|
+
await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source, page?.namespace ?? '', undefined, page);
|
|
2368
|
+
this.markVolatileSeen(message, page);
|
|
1583
2369
|
}
|
|
1584
2370
|
finally {
|
|
1585
|
-
this.finishVolatileDelivery(message);
|
|
2371
|
+
this.finishVolatileDelivery(message, page);
|
|
1586
2372
|
}
|
|
1587
2373
|
processed.push(rawMessage);
|
|
1588
2374
|
}
|
|
1589
2375
|
if (processed.length > 0)
|
|
1590
|
-
await this.ackVolatileMessages(processed, false, advanceCursor);
|
|
2376
|
+
await this.ackVolatileMessages(processed, false, advanceCursor, page);
|
|
1591
2377
|
return processed.map((item) => this.publicVolatileMessage(item));
|
|
1592
2378
|
}
|
|
1593
2379
|
messageRecallDedupKey(payload) {
|
|
@@ -1616,31 +2402,36 @@ export class MessageDeliveryEngine {
|
|
|
1616
2402
|
return `p2p|tombstone:${tombstoneId}`;
|
|
1617
2403
|
return `p2p|unknown:${Date.now()}:${Math.random()}`;
|
|
1618
2404
|
}
|
|
1619
|
-
async publishMessageRecallTombstone(seq, message, source = 'push') {
|
|
2405
|
+
async publishMessageRecallTombstone(seq, message, source = 'push', page, owned = false) {
|
|
1620
2406
|
const client = this.runtime.client;
|
|
1621
2407
|
const generation = this.captureInlineGeneration();
|
|
1622
2408
|
const eventPayload = this.recallEventFromMessage(message);
|
|
1623
2409
|
if (!eventPayload)
|
|
1624
2410
|
return false;
|
|
1625
2411
|
const dedupKey = this.messageRecallDedupKey(eventPayload);
|
|
2412
|
+
if (!owned)
|
|
2413
|
+
return await this.applicationOwnership(JSON.stringify([this.peerContext(), 'message.recalled', dedupKey]), page, () => this.publishMessageRecallTombstone(seq, message, source, page, true)) ?? false;
|
|
1626
2414
|
let seen = client._messageRecallSeen;
|
|
1627
2415
|
if (!seen) {
|
|
1628
2416
|
seen = new Map();
|
|
1629
2417
|
client._messageRecallSeen = seen;
|
|
1630
2418
|
}
|
|
1631
|
-
if (seen.has(dedupKey)) {
|
|
2419
|
+
if (seen.has(dedupKey) || page?.seen.has(`message.recalled:${dedupKey}`)) {
|
|
1632
2420
|
client._clientLog.debug(`message.recalled dedup suppressed: seq=${String(seq)} key=${dedupKey}`);
|
|
1633
2421
|
return false;
|
|
1634
2422
|
}
|
|
1635
|
-
await client._publishAppEvent('message.recalled', eventPayload, source);
|
|
2423
|
+
await client._publishAppEvent('message.recalled', eventPayload, source, page?.namespace ?? '', undefined, page);
|
|
1636
2424
|
if (!this.isInlineGenerationCurrent(generation))
|
|
1637
2425
|
throw new Error('pull invalidated');
|
|
1638
|
-
seen.
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
seen.
|
|
1643
|
-
|
|
2426
|
+
page?.seen.add(`message.recalled:${dedupKey}`);
|
|
2427
|
+
this.deferApplicationEffect(page, () => {
|
|
2428
|
+
seen.set(dedupKey, Date.now());
|
|
2429
|
+
if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
|
|
2430
|
+
const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
|
|
2431
|
+
for (const [oldKey] of drop)
|
|
2432
|
+
seen.delete(oldKey);
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
1644
2435
|
client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
|
|
1645
2436
|
return true;
|
|
1646
2437
|
}
|
|
@@ -1723,7 +2514,7 @@ export class MessageDeliveryEngine {
|
|
|
1723
2514
|
return `${normalizedGroupId}|tombstone:${tombstoneId}`;
|
|
1724
2515
|
return `${normalizedGroupId}|unknown:${Date.now()}:${Math.random()}`;
|
|
1725
2516
|
}
|
|
1726
|
-
async publishGroupRecallTombstone(groupId, seq, message, source = 'push') {
|
|
2517
|
+
async publishGroupRecallTombstone(groupId, seq, message, source = 'push', page, owned = false) {
|
|
1727
2518
|
const client = this.runtime.client;
|
|
1728
2519
|
const generation = this.captureInlineGeneration();
|
|
1729
2520
|
const eventPayload = this.recallEventFromGroupMessage(message);
|
|
@@ -1732,24 +2523,29 @@ export class MessageDeliveryEngine {
|
|
|
1732
2523
|
const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
|
|
1733
2524
|
const dedupGroupId = rawGroupId;
|
|
1734
2525
|
const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
|
|
2526
|
+
if (!owned)
|
|
2527
|
+
return await this.applicationOwnership(JSON.stringify([this.peerContext(), 'group.message_recalled', dedupKey]), page, () => this.publishGroupRecallTombstone(groupId, seq, message, source, page, true)) ?? false;
|
|
1735
2528
|
let seen = client._groupRecallSeen;
|
|
1736
2529
|
if (!seen) {
|
|
1737
2530
|
seen = new Map();
|
|
1738
2531
|
client._groupRecallSeen = seen;
|
|
1739
2532
|
}
|
|
1740
|
-
if (seen.has(dedupKey)) {
|
|
2533
|
+
if (seen.has(dedupKey) || page?.seen.has(`group.message_recalled:${dedupKey}`)) {
|
|
1741
2534
|
client._clientLog.debug(`group.message_recalled dedup suppressed: group=${groupId} seq=${String(seq)} key=${dedupKey}`);
|
|
1742
2535
|
return false;
|
|
1743
2536
|
}
|
|
1744
|
-
await client._publishAppEvent('group.message_recalled', eventPayload, source);
|
|
2537
|
+
await client._publishAppEvent('group.message_recalled', eventPayload, source, page?.namespace ?? '', undefined, page);
|
|
1745
2538
|
if (!this.isInlineGenerationCurrent(generation))
|
|
1746
2539
|
throw new Error('pull invalidated');
|
|
1747
|
-
seen.
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
seen.
|
|
1752
|
-
|
|
2540
|
+
page?.seen.add(`group.message_recalled:${dedupKey}`);
|
|
2541
|
+
this.deferApplicationEffect(page, () => {
|
|
2542
|
+
seen.set(dedupKey, Date.now());
|
|
2543
|
+
if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
|
|
2544
|
+
const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
|
|
2545
|
+
for (const [oldKey] of drop)
|
|
2546
|
+
seen.delete(oldKey);
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
1753
2549
|
client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
|
|
1754
2550
|
return true;
|
|
1755
2551
|
}
|
|
@@ -1791,9 +2587,139 @@ export class MessageDeliveryEngine {
|
|
|
1791
2587
|
client._safeAsync(this.fillGroupGap(groupId));
|
|
1792
2588
|
await this.publishGroupRecallTombstone(groupId, seq, wrapped);
|
|
1793
2589
|
}
|
|
1794
|
-
async publishAppEvent(event, payload, source = 'direct', ns = '', batch) {
|
|
2590
|
+
async publishAppEvent(event, payload, source = 'direct', ns = '', batch, page) {
|
|
1795
2591
|
const client = this.runtime.client;
|
|
2592
|
+
const seq = isJsonObject(payload) ? payload.seq : undefined;
|
|
2593
|
+
const groupSeq = isJsonObject(payload) && event === 'group.changed' ? payload.event_seq : seq;
|
|
2594
|
+
if ((event === 'group.message_created' || event === 'group.changed') && source !== 'history'
|
|
2595
|
+
&& isJsonObject(payload) && !this.isVolatileMessage(payload) && Number.isSafeInteger(groupSeq) && Number(groupSeq) > 0) {
|
|
2596
|
+
const groupNs = ns || this.applicationNamespace(event, payload);
|
|
2597
|
+
const key = JSON.stringify([this.peerContext(), event, groupNs, groupSeq]);
|
|
2598
|
+
const family = APPLICATION_EVENT_FAMILIES[event];
|
|
2599
|
+
const batchActive = page ? page.batch : (client._dispatcher?._snapshotApplicationHandlers?.([family]).get(family)?.length ?? 0) > 0;
|
|
2600
|
+
if (batchActive || this.p2pDeliveryInflight.has(key)) {
|
|
2601
|
+
await this.applicationOwnership(key, page, async () => {
|
|
2602
|
+
if (this.p2pDeliveryCompleted.has(key))
|
|
2603
|
+
return;
|
|
2604
|
+
await this.publishAppEventInternal(event, payload, source, ns, batch, page);
|
|
2605
|
+
this.deferApplicationEffect(page, () => {
|
|
2606
|
+
this.p2pDeliveryCompleted.set(key, { sender_aid: String(payload.from ?? ''), message_id: String(payload.message_id ?? '') });
|
|
2607
|
+
if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT)
|
|
2608
|
+
this.p2pDeliveryCompleted.delete(this.p2pDeliveryCompleted.keys().next().value);
|
|
2609
|
+
});
|
|
2610
|
+
});
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
if (!client._p2pPeerPullEnabled || event !== 'message.received' || source === 'history'
|
|
2615
|
+
|| !isJsonObject(payload) || this.isVolatileMessage(payload) || !Number.isSafeInteger(seq) || Number(seq) <= 0) {
|
|
2616
|
+
return await this.publishAppEventInternal(event, payload, source, ns, batch, page);
|
|
2617
|
+
}
|
|
2618
|
+
const context = this.peerContext();
|
|
2619
|
+
const generation = this.captureInlineGeneration();
|
|
2620
|
+
const aid = client._aid;
|
|
2621
|
+
const deviceId = client._deviceId;
|
|
2622
|
+
const slotId = client._slotId;
|
|
2623
|
+
const ensureCurrent = () => {
|
|
2624
|
+
if (client._aid !== aid || client._deviceId !== deviceId || client._slotId !== slotId
|
|
2625
|
+
|| this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
|
|
2626
|
+
throw new StateError('delivery invalidated');
|
|
2627
|
+
};
|
|
2628
|
+
const sender = String(payload.sender_aid ?? payload.from_aid ?? payload.from ?? '');
|
|
2629
|
+
if (!sender)
|
|
2630
|
+
return await this.publishAppEventInternal(event, payload, source, ns, batch, page);
|
|
2631
|
+
const key = JSON.stringify([context, event, seq]);
|
|
2632
|
+
const receipt = { sender_aid: sender, message_id: String(payload.message_id ?? '') };
|
|
2633
|
+
const validate = (saved, expected = receipt) => {
|
|
2634
|
+
if (saved.sender_aid !== expected.sender_aid || (saved.message_id && expected.message_id && saved.message_id !== expected.message_id))
|
|
2635
|
+
throw new ValidationError('delivery receipt identity conflict');
|
|
2636
|
+
};
|
|
2637
|
+
const local = page?.batch ? page.claims.get(key) : undefined;
|
|
2638
|
+
if (local) {
|
|
2639
|
+
validate(local);
|
|
2640
|
+
if (!local.message_id)
|
|
2641
|
+
local.message_id = receipt.message_id;
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
for (;;) {
|
|
2645
|
+
const inflight = this.p2pDeliveryInflight.get(key);
|
|
2646
|
+
if (!inflight)
|
|
2647
|
+
break;
|
|
2648
|
+
await inflight;
|
|
2649
|
+
ensureCurrent();
|
|
2650
|
+
const completed = this.p2pDeliveryCompleted.get(key);
|
|
2651
|
+
if (completed)
|
|
2652
|
+
validate(completed);
|
|
2653
|
+
if (completed?.message_id || !receipt.message_id)
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
if (page?.batch) {
|
|
2657
|
+
page.ensureCurrent();
|
|
2658
|
+
if (page.closed || (ns && ns !== page.namespace))
|
|
2659
|
+
throw new StateError('delivery page invalidated');
|
|
2660
|
+
const completed = this.p2pDeliveryCompleted.get(key);
|
|
2661
|
+
if (completed) {
|
|
2662
|
+
validate(completed);
|
|
2663
|
+
if (!completed.message_id && receipt.message_id)
|
|
2664
|
+
this.deferApplicationEffect(page, () => { completed.message_id = receipt.message_id; });
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
page.claims.set(key, receipt);
|
|
2668
|
+
this.p2pDeliveryInflight.set(key, page.done);
|
|
2669
|
+
await this.publishAppEventInternal(event, payload, source, ns, batch, page);
|
|
2670
|
+
this.deferApplicationEffect(page, () => {
|
|
2671
|
+
this.p2pDeliveryCompleted.set(key, { ...receipt });
|
|
2672
|
+
if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT)
|
|
2673
|
+
this.p2pDeliveryCompleted.delete(this.p2pDeliveryCompleted.keys().next().value);
|
|
2674
|
+
});
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
const task = Promise.resolve().then(async () => {
|
|
2678
|
+
ensureCurrent();
|
|
2679
|
+
let pending = this.p2pDeliveryCompleted.get(key);
|
|
2680
|
+
if (pending)
|
|
2681
|
+
validate(pending);
|
|
2682
|
+
if (!pending) {
|
|
2683
|
+
await this.publishAppEventInternal(event, payload, source, ns, batch, page);
|
|
2684
|
+
ensureCurrent();
|
|
2685
|
+
pending = { ...receipt };
|
|
2686
|
+
this.p2pDeliveryCompleted.set(key, pending);
|
|
2687
|
+
}
|
|
2688
|
+
else if (!pending.message_id && receipt.message_id) {
|
|
2689
|
+
pending = { ...receipt };
|
|
2690
|
+
this.p2pDeliveryCompleted.set(key, pending);
|
|
2691
|
+
}
|
|
2692
|
+
if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT) {
|
|
2693
|
+
const oldest = this.p2pDeliveryCompleted.keys().next().value;
|
|
2694
|
+
if (oldest !== undefined)
|
|
2695
|
+
this.p2pDeliveryCompleted.delete(oldest);
|
|
2696
|
+
}
|
|
2697
|
+
});
|
|
2698
|
+
this.p2pDeliveryInflight.set(key, task);
|
|
2699
|
+
try {
|
|
2700
|
+
await task;
|
|
2701
|
+
}
|
|
2702
|
+
finally {
|
|
2703
|
+
if (this.p2pDeliveryInflight.get(key) === task)
|
|
2704
|
+
this.p2pDeliveryInflight.delete(key);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
async publishAppEventInternal(event, payload, source = 'direct', ns = '', batch, page) {
|
|
2708
|
+
const client = this.runtime.client;
|
|
2709
|
+
const family = APPLICATION_EVENT_FAMILIES[event];
|
|
2710
|
+
const handlers = page?.handlers ?? (family ? client._dispatcher._snapshotApplicationHandlers?.([event, family]) : undefined);
|
|
1796
2711
|
const generation = this.deliveryGeneration;
|
|
2712
|
+
const runtimeGeneration = Number(client._v2RuntimeGeneration ?? 0);
|
|
2713
|
+
const namespace = ns || this.applicationNamespace(event, payload);
|
|
2714
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(namespace);
|
|
2715
|
+
const guard = () => {
|
|
2716
|
+
page?.ensureCurrent();
|
|
2717
|
+
if (generation !== this.deliveryGeneration || Number(client._v2RuntimeGeneration ?? 0) !== runtimeGeneration
|
|
2718
|
+
|| !this.isSeqNamespaceGenerationCurrent(namespace, namespaceGeneration)
|
|
2719
|
+
|| client._rpcPipeline?.isPullNamespaceCurrent?.(namespace) === false)
|
|
2720
|
+
throw new StateError('delivery invalidated');
|
|
2721
|
+
return true;
|
|
2722
|
+
};
|
|
1797
2723
|
if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
|
|
1798
2724
|
client._maybeAppendEchoTraceReceive(payload);
|
|
1799
2725
|
}
|
|
@@ -1813,6 +2739,8 @@ export class MessageDeliveryEngine {
|
|
|
1813
2739
|
client._clientLog.debug(`agent_md etag inject skipped: ${String(exc)}`);
|
|
1814
2740
|
}
|
|
1815
2741
|
}
|
|
2742
|
+
if (client._p2pPeerPullEnabled && generation !== this.deliveryGeneration)
|
|
2743
|
+
throw new StateError('delivery invalidated');
|
|
1816
2744
|
const canonicalSource = canonicalDeliverySource(source);
|
|
1817
2745
|
let normalized = this.normalizePublishedMessagePayload(event, payload, source);
|
|
1818
2746
|
if (isJsonObject(normalized)
|
|
@@ -1820,13 +2748,46 @@ export class MessageDeliveryEngine {
|
|
|
1820
2748
|
&& canonicalSource !== 'direct') {
|
|
1821
2749
|
normalized = { ...normalized, source: canonicalSource };
|
|
1822
2750
|
}
|
|
1823
|
-
if (
|
|
2751
|
+
if (page?.batch && family) {
|
|
2752
|
+
page.ensureCurrent();
|
|
2753
|
+
if (page.closed || (ns && ns !== page.namespace))
|
|
2754
|
+
throw new StateError('delivery page invalidated');
|
|
2755
|
+
page.items.push({ event, payload: normalized });
|
|
2756
|
+
}
|
|
2757
|
+
else if (family && handlers && !page) {
|
|
2758
|
+
const batchHandlers = handlers.get(family) ?? [];
|
|
2759
|
+
if (batchHandlers.length > 0) {
|
|
2760
|
+
await client._dispatcher._publishAndWait(family, { namespace: ns || this.applicationNamespace(event, payload),
|
|
2761
|
+
items: [{ event, payload: normalized }], source: canonicalSource }, batchHandlers, undefined, guard);
|
|
2762
|
+
}
|
|
2763
|
+
else
|
|
2764
|
+
await client._dispatcher._publishAndWait(event, normalized, undefined, undefined, guard);
|
|
2765
|
+
}
|
|
2766
|
+
else if (['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
|
|
1824
2767
|
&& typeof client._dispatcher._publishAndWait === 'function') {
|
|
1825
|
-
await client._dispatcher._publishAndWait(event, normalized);
|
|
2768
|
+
await client._dispatcher._publishAndWait(event, normalized, undefined, page?.reservation, guard);
|
|
1826
2769
|
}
|
|
1827
2770
|
else {
|
|
1828
2771
|
client._dispatcher.enqueue(event, normalized);
|
|
1829
2772
|
}
|
|
2773
|
+
if (page?.batch) {
|
|
2774
|
+
this.deferApplicationEffect(page, () => this.recordApplicationDeliveryChange(event, payload, source, ns, batch, generation));
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
if (family)
|
|
2778
|
+
guard();
|
|
2779
|
+
await this.recordApplicationDeliveryChange(event, payload, source, ns, batch, generation);
|
|
2780
|
+
}
|
|
2781
|
+
applicationNamespace(event, payload) {
|
|
2782
|
+
if (isJsonObject(payload) && event.startsWith('group.'))
|
|
2783
|
+
return `${event === 'group.changed' ? 'group_event' : 'group'}:${String(payload.group_id ?? payload.group_aid ?? '')}`;
|
|
2784
|
+
const client = this.runtime.client;
|
|
2785
|
+
const sender = isJsonObject(payload) ? String(payload.sender_aid ?? payload.from_aid ?? payload.from ?? '') : '';
|
|
2786
|
+
return client._p2pPeerPullEnabled && sender ? this.peerNamespace(sender) : `p2p:${client._aid ?? ''}`;
|
|
2787
|
+
}
|
|
2788
|
+
async recordApplicationDeliveryChange(event, payload, source, ns, batch, generation) {
|
|
2789
|
+
const client = this.runtime.client;
|
|
2790
|
+
const canonicalSource = canonicalDeliverySource(source);
|
|
1830
2791
|
if (generation !== this.deliveryGeneration)
|
|
1831
2792
|
return;
|
|
1832
2793
|
if (canonicalSource !== 'pull' && canonicalSource !== 'pending_retry'
|
|
@@ -1974,6 +2935,10 @@ export class MessageDeliveryEngine {
|
|
|
1974
2935
|
}
|
|
1975
2936
|
onRawMessageReceived(data) {
|
|
1976
2937
|
const client = this.runtime.client;
|
|
2938
|
+
if (client._p2pPeerPullEnabled) {
|
|
2939
|
+
this.handlePeerNotification(data);
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
1977
2942
|
client._clientLog.debug(`_onRawMessageReceived enter: from=${data?.from ?? '-'} mid=${data?.message_id ?? '-'} seq=${data?.seq ?? '-'}`);
|
|
1978
2943
|
const ns = isJsonObject(data) && client._aid && data.seq !== undefined
|
|
1979
2944
|
? `p2p:${client._aid}`
|
|
@@ -2001,6 +2966,11 @@ export class MessageDeliveryEngine {
|
|
|
2001
2966
|
msg.type = 'message.recalled';
|
|
2002
2967
|
if (!this.messageTargetsCurrentInstance(msg))
|
|
2003
2968
|
return;
|
|
2969
|
+
if (client._p2pPeerPullEnabled) {
|
|
2970
|
+
this.handlePeerNotification(msg);
|
|
2971
|
+
await this.publishMessageRecallTombstone(msg.seq, msg);
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2004
2974
|
const seq = positiveSafeSequenceHint(msg.seq);
|
|
2005
2975
|
if (client._aid && seq > 0) {
|
|
2006
2976
|
const ns = `p2p:${client._aid}`;
|
|
@@ -2219,7 +3189,7 @@ export class MessageDeliveryEngine {
|
|
|
2219
3189
|
if (client._v2Session) {
|
|
2220
3190
|
try {
|
|
2221
3191
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
2222
|
-
await client._rpcPipeline.
|
|
3192
|
+
await client._rpcPipeline.runPullNamespaceSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true }), true);
|
|
2223
3193
|
return;
|
|
2224
3194
|
}
|
|
2225
3195
|
catch (exc) {
|
|
@@ -2228,28 +3198,40 @@ export class MessageDeliveryEngine {
|
|
|
2228
3198
|
return;
|
|
2229
3199
|
}
|
|
2230
3200
|
}
|
|
3201
|
+
const generation = this.captureInlineGeneration();
|
|
3202
|
+
const dedupKey = `group_pull:${ns}`;
|
|
3203
|
+
this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(notification.seq), true);
|
|
3204
|
+
if (client._gapFillDone.has(dedupKey))
|
|
3205
|
+
return;
|
|
3206
|
+
client._gapFillDone.add(dedupKey);
|
|
2231
3207
|
let rawCompleted = false;
|
|
3208
|
+
let failed = false;
|
|
2232
3209
|
try {
|
|
2233
3210
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', {
|
|
2234
3211
|
group_id: groupId,
|
|
2235
3212
|
after_message_seq: afterSeq,
|
|
2236
3213
|
limit: 50,
|
|
2237
3214
|
});
|
|
2238
|
-
await client._rpcPipeline.
|
|
2239
|
-
const result = await client._rpcPipeline.rawCall('group.pull', {
|
|
3215
|
+
await client._rpcPipeline.runPullNamespaceSerialized(key, async () => {
|
|
3216
|
+
const result = await client._rpcPipeline.runPullSerialized(key, () => client._rpcPipeline.rawCall('group.pull', {
|
|
2240
3217
|
group_id: groupId,
|
|
2241
3218
|
after_message_seq: afterSeq,
|
|
2242
3219
|
device_id: client._deviceId,
|
|
2243
3220
|
slot_id: client._slotId,
|
|
2244
3221
|
limit: 50,
|
|
2245
3222
|
_rpc_background: true,
|
|
2246
|
-
}, { background: true });
|
|
3223
|
+
}, { background: true, pullGateKey: key }), true);
|
|
3224
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
3225
|
+
throw new Error('pull invalidated');
|
|
2247
3226
|
rawCompleted = true;
|
|
3227
|
+
if (isJsonObject(result))
|
|
3228
|
+
this.observeBackgroundPullResponse(ns, afterSeq, result, 50);
|
|
2248
3229
|
await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
|
|
2249
3230
|
this.prunePushedSeqs(ns);
|
|
2250
3231
|
}, true);
|
|
2251
3232
|
}
|
|
2252
3233
|
catch (exc) {
|
|
3234
|
+
failed = true;
|
|
2253
3235
|
// 页发布或本地提交失败不能 ACK;提交后 ACK 失败则保留 pending。
|
|
2254
3236
|
// 两种情况都不能再把原始通知当作第二条业务消息发布。
|
|
2255
3237
|
client._clientLog.warn(`auto pull group message commit/ack failed:${String(exc)}`);
|
|
@@ -2257,6 +3239,12 @@ export class MessageDeliveryEngine {
|
|
|
2257
3239
|
await client._publishAppEvent('group.message_created', notification, 'push');
|
|
2258
3240
|
}
|
|
2259
3241
|
}
|
|
3242
|
+
finally {
|
|
3243
|
+
if (this.isInlineGenerationCurrent(generation)) {
|
|
3244
|
+
client._gapFillDone.delete(dedupKey);
|
|
3245
|
+
this.finishBackgroundPull(ns, afterSeq, failed);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
2260
3248
|
}
|
|
2261
3249
|
scheduleP2pGapRetry(ns, retryAttempt) {
|
|
2262
3250
|
const client = this.runtime.client;
|
|
@@ -2279,7 +3267,12 @@ export class MessageDeliveryEngine {
|
|
|
2279
3267
|
}, delayMs);
|
|
2280
3268
|
}
|
|
2281
3269
|
async fillP2pGap(retryAttempt = 0) {
|
|
3270
|
+
if (this.runtime.client._p2pPeerPullEnabled) {
|
|
3271
|
+
this.requestPeerDirectoryRefresh();
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
2282
3274
|
const client = this.runtime.client;
|
|
3275
|
+
const generation = this.captureInlineGeneration();
|
|
2283
3276
|
if (client._sessionOptions?.background_sync === false)
|
|
2284
3277
|
return;
|
|
2285
3278
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
@@ -2291,9 +3284,11 @@ export class MessageDeliveryEngine {
|
|
|
2291
3284
|
const dedupKey = `p2p_pull:${ns}`;
|
|
2292
3285
|
if (client._gapFillDone.has(dedupKey))
|
|
2293
3286
|
return;
|
|
3287
|
+
this.recordBackgroundPullDemand(ns, 0, true);
|
|
2294
3288
|
client._gapFillDone.add(dedupKey);
|
|
2295
3289
|
this.runtime.delivery.setGapFillActive(true);
|
|
2296
3290
|
let filled = 0;
|
|
3291
|
+
let failed = false;
|
|
2297
3292
|
try {
|
|
2298
3293
|
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 });
|
|
2299
3294
|
const messages = await client._rpcPipeline.runPullSerialized(key, () => client._pullV2(afterSeq, 50, { gateLocked: true }), true);
|
|
@@ -2309,22 +3304,27 @@ export class MessageDeliveryEngine {
|
|
|
2309
3304
|
client._clientLog.warn(`P2P message gap-fill V2 unavailable, used plain pull fallback:${String(formatDeliveryError(exc))}`);
|
|
2310
3305
|
}
|
|
2311
3306
|
catch (fallbackExc) {
|
|
3307
|
+
failed = true;
|
|
2312
3308
|
client._clientLog.warn(`P2P message gap-fill plain fallback failed:${String(formatDeliveryError(fallbackExc))}`);
|
|
2313
|
-
this.scheduleP2pGapRetry(ns, retryAttempt + 1);
|
|
2314
3309
|
}
|
|
2315
3310
|
}
|
|
2316
3311
|
else {
|
|
3312
|
+
failed = true;
|
|
2317
3313
|
client._clientLog.warn(`P2P message gap-fill failed:${String(formatDeliveryError(exc))}`);
|
|
2318
3314
|
}
|
|
2319
3315
|
}
|
|
2320
3316
|
finally {
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
3317
|
+
if (this.isInlineGenerationCurrent(generation)) {
|
|
3318
|
+
client._gapFillDone.delete(dedupKey);
|
|
3319
|
+
this.runtime.delivery.setGapFillActive(false);
|
|
3320
|
+
this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
|
|
3321
|
+
this.finishBackgroundPull(ns, afterSeq, failed);
|
|
3322
|
+
}
|
|
2324
3323
|
}
|
|
2325
3324
|
}
|
|
2326
3325
|
async fillP2pGapWithPlainPull(ns, afterSeq) {
|
|
2327
3326
|
const client = this.runtime.client;
|
|
3327
|
+
const generation = this.captureInlineGeneration();
|
|
2328
3328
|
const params = {
|
|
2329
3329
|
after_seq: afterSeq,
|
|
2330
3330
|
limit: 50,
|
|
@@ -2333,11 +3333,16 @@ export class MessageDeliveryEngine {
|
|
|
2333
3333
|
_rpc_background: true,
|
|
2334
3334
|
};
|
|
2335
3335
|
const raw = await client._rpcPipeline.rawCall('message.pull', params, { background: true });
|
|
3336
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
3337
|
+
throw new Error('pull invalidated');
|
|
3338
|
+
if (isJsonObject(raw))
|
|
3339
|
+
this.observeBackgroundPullResponse(ns, afterSeq, raw, 50);
|
|
2336
3340
|
const page = await this.commitPlainForwardPage(ns, afterSeq, 'message.pull', raw);
|
|
2337
3341
|
return page.publishedCount;
|
|
2338
3342
|
}
|
|
2339
3343
|
async fillGroupGap(groupId) {
|
|
2340
3344
|
const client = this.runtime.client;
|
|
3345
|
+
const generation = this.captureInlineGeneration();
|
|
2341
3346
|
if (client._sessionOptions?.background_sync === false)
|
|
2342
3347
|
return;
|
|
2343
3348
|
if (client.state !== ConnectionState.READY || client._closing)
|
|
@@ -2350,25 +3355,48 @@ export class MessageDeliveryEngine {
|
|
|
2350
3355
|
const dedupKey = `group_pull:${ns}`;
|
|
2351
3356
|
if (client._gapFillDone.has(dedupKey))
|
|
2352
3357
|
return;
|
|
3358
|
+
this.recordBackgroundPullDemand(ns, 0, true);
|
|
2353
3359
|
client._gapFillDone.add(dedupKey);
|
|
2354
3360
|
this.runtime.delivery.setGapFillActive(true);
|
|
2355
3361
|
let filled = 0;
|
|
3362
|
+
let failed = false;
|
|
2356
3363
|
try {
|
|
2357
3364
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
2358
|
-
const messages = await client._rpcPipeline.
|
|
3365
|
+
const messages = await client._rpcPipeline.runPullNamespaceSerialized(key, async () => {
|
|
3366
|
+
if (client._v2Session)
|
|
3367
|
+
return await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
|
|
3368
|
+
const result = await client._rpcPipeline.runPullSerialized(key, () => client._rpcPipeline.rawCall('group.pull', {
|
|
3369
|
+
group_id: groupId,
|
|
3370
|
+
after_message_seq: afterSeq,
|
|
3371
|
+
device_id: client._deviceId,
|
|
3372
|
+
slot_id: client._slotId,
|
|
3373
|
+
limit: 50,
|
|
3374
|
+
_rpc_background: true,
|
|
3375
|
+
}, { background: true, pullGateKey: key }), true);
|
|
3376
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
3377
|
+
throw new Error('pull invalidated');
|
|
3378
|
+
if (isJsonObject(result))
|
|
3379
|
+
this.observeBackgroundPullResponse(ns, afterSeq, result, 50);
|
|
3380
|
+
await this.commitPlainForwardPage(ns, afterSeq, 'group.pull', result, groupId);
|
|
3381
|
+
return isJsonObject(result) && Array.isArray(result.messages) ? result.messages : [];
|
|
3382
|
+
}, true);
|
|
2359
3383
|
filled = messages.length;
|
|
2360
3384
|
this.prunePushedSeqs(ns);
|
|
2361
3385
|
}
|
|
2362
3386
|
catch (exc) {
|
|
3387
|
+
failed = true;
|
|
2363
3388
|
client._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
|
|
2364
3389
|
}
|
|
2365
3390
|
finally {
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
client.
|
|
3391
|
+
if (this.isInlineGenerationCurrent(generation)) {
|
|
3392
|
+
client._gapFillDone.delete(dedupKey);
|
|
3393
|
+
this.runtime.delivery.setGapFillActive(false);
|
|
3394
|
+
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
3395
|
+
this.recordBackgroundPullDemand(ns, 0, true);
|
|
3396
|
+
}
|
|
3397
|
+
this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
|
|
3398
|
+
this.finishBackgroundPull(ns, afterSeq, failed);
|
|
2370
3399
|
}
|
|
2371
|
-
this.schedulePendingPullIfNeeded(ns, 'gap-fill-complete');
|
|
2372
3400
|
}
|
|
2373
3401
|
}
|
|
2374
3402
|
async fillGroupEventGap(groupId, options) {
|
|
@@ -2382,6 +3410,8 @@ export class MessageDeliveryEngine {
|
|
|
2382
3410
|
if (!groupId)
|
|
2383
3411
|
return;
|
|
2384
3412
|
const ns = `group_event:${groupId}`;
|
|
3413
|
+
this.rememberCommittedSeqState(ns);
|
|
3414
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
2385
3415
|
const requestedAfterSeq = Number(options?.afterSeq ?? 0);
|
|
2386
3416
|
const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
|
|
2387
3417
|
? requestedAfterSeq
|
|
@@ -2390,6 +3420,7 @@ export class MessageDeliveryEngine {
|
|
|
2390
3420
|
const dedupKey = `group_event_pull:${ns}`;
|
|
2391
3421
|
if (client._gapFillDone.has(dedupKey))
|
|
2392
3422
|
return;
|
|
3423
|
+
this.recordBackgroundPullDemand(ns, 0, true);
|
|
2393
3424
|
client._gapFillDone.add(dedupKey);
|
|
2394
3425
|
this.runtime.delivery.setGapFillActive(true);
|
|
2395
3426
|
let continuationAfterSeq = 0;
|
|
@@ -2409,6 +3440,10 @@ export class MessageDeliveryEngine {
|
|
|
2409
3440
|
_rpc_background: true,
|
|
2410
3441
|
};
|
|
2411
3442
|
const result = await client.call('group.pull_events', request);
|
|
3443
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
3444
|
+
throw new Error('pull invalidated');
|
|
3445
|
+
if (isJsonObject(result))
|
|
3446
|
+
this.observeBackgroundPullResponse(ns, nextAfterSeq, result, 50);
|
|
2412
3447
|
if (!isJsonObject(result))
|
|
2413
3448
|
return;
|
|
2414
3449
|
const events = result.events;
|
|
@@ -2426,47 +3461,69 @@ export class MessageDeliveryEngine {
|
|
|
2426
3461
|
const eventSeqs = [];
|
|
2427
3462
|
let hasDissolvedEvent = false;
|
|
2428
3463
|
let publishedEventCount = 0;
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
3464
|
+
const applicationPage = this.beginApplicationPage(ns);
|
|
3465
|
+
try {
|
|
3466
|
+
for (const evt of eventObjects) {
|
|
3467
|
+
const eventSeq = Number(evt.event_seq ?? 0);
|
|
3468
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0)
|
|
3469
|
+
eventSeqs.push(eventSeq);
|
|
3470
|
+
evt._from_gap_fill = true;
|
|
3471
|
+
if (evt.action === 'dissolved')
|
|
3472
|
+
hasDissolvedEvent = true;
|
|
3473
|
+
const et = String(evt.event_type ?? '');
|
|
3474
|
+
if (et === 'group.message_created')
|
|
3475
|
+
continue;
|
|
3476
|
+
const cs = evt.client_signature;
|
|
3477
|
+
if (cs && typeof cs === 'object') {
|
|
3478
|
+
if (client._shouldSkipEventSignature(evt)) {
|
|
3479
|
+
delete evt.client_signature;
|
|
3480
|
+
}
|
|
3481
|
+
else {
|
|
3482
|
+
const verified = await client._verifyEventSignature(evt, cs);
|
|
3483
|
+
evt._verified = client._isEventSignatureVerified(verified);
|
|
3484
|
+
}
|
|
2443
3485
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
3486
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
|
|
3487
|
+
this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
|
|
3488
|
+
publishedEventCount += 1;
|
|
2447
3489
|
}
|
|
2448
3490
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
3491
|
+
const deliveryThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, effectiveFloor, ...eventSeqs);
|
|
3492
|
+
await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough, applicationPage);
|
|
3493
|
+
await this.flushApplicationPage(applicationPage);
|
|
3494
|
+
}
|
|
3495
|
+
catch (error) {
|
|
3496
|
+
this.abortApplicationPage(applicationPage, error);
|
|
3497
|
+
throw error;
|
|
2453
3498
|
}
|
|
2454
|
-
const deliveryThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, effectiveFloor, ...eventSeqs);
|
|
2455
|
-
await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough);
|
|
2456
3499
|
if (!this.isInlineGenerationCurrent(generation))
|
|
2457
3500
|
throw new Error('pull invalidated');
|
|
2458
|
-
if (hasDissolvedEvent)
|
|
3501
|
+
if (hasDissolvedEvent) {
|
|
3502
|
+
this.backgroundPullDemands.delete(ns);
|
|
2459
3503
|
break;
|
|
2460
|
-
if (contiguousSeq === undefined)
|
|
2461
|
-
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
|
|
2462
|
-
else
|
|
2463
|
-
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
|
|
2464
|
-
if (effectiveFloor > client._seqTracker.getContiguousSeq(ns)) {
|
|
2465
|
-
client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
|
|
2466
3504
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
3505
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
3506
|
+
throw new Error('pull invalidated');
|
|
3507
|
+
const pageSnapshot = client._seqTracker.snapshotNamespace(ns);
|
|
3508
|
+
let ackContig = pageContigBefore;
|
|
3509
|
+
try {
|
|
3510
|
+
if (contiguousSeq === undefined)
|
|
3511
|
+
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
|
|
3512
|
+
else
|
|
3513
|
+
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
|
|
3514
|
+
if (effectiveFloor > client._seqTracker.getContiguousSeq(ns))
|
|
3515
|
+
client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
|
|
3516
|
+
ackContig = client._seqTracker.getContiguousSeq(ns);
|
|
3517
|
+
if (ackContig !== pageContigBefore) {
|
|
3518
|
+
await this.commitSeqTrackerState(ns);
|
|
3519
|
+
if (!this.isInlineGenerationCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
3520
|
+
throw new Error('pull invalidated');
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
catch (error) {
|
|
3524
|
+
if (this.isInlineGenerationCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
3525
|
+
client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageSnapshot);
|
|
3526
|
+
throw error;
|
|
2470
3527
|
}
|
|
2471
3528
|
if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
|
|
2472
3529
|
pendingAckSeq = Math.max(pendingAckSeq, ackContig);
|
|
@@ -2504,15 +3561,12 @@ export class MessageDeliveryEngine {
|
|
|
2504
3561
|
client._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
|
|
2505
3562
|
}
|
|
2506
3563
|
finally {
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
this.
|
|
2513
|
-
group_id: groupId,
|
|
2514
|
-
_online_hint_after_event_seq: continuationAfterSeq,
|
|
2515
|
-
});
|
|
3564
|
+
if (this.isInlineGenerationCurrent(generation)) {
|
|
3565
|
+
client._gapFillDone.delete(dedupKey);
|
|
3566
|
+
this.runtime.delivery.setGapFillActive(false);
|
|
3567
|
+
if (!client._rpcPipeline)
|
|
3568
|
+
this.syncStopped(failed ? 'aborted' : 'pull_drained');
|
|
3569
|
+
this.finishBackgroundPull(ns, afterSeq, failed);
|
|
2516
3570
|
}
|
|
2517
3571
|
}
|
|
2518
3572
|
}
|
|
@@ -2568,6 +3622,9 @@ export class MessageDeliveryEngine {
|
|
|
2568
3622
|
if (!groupId)
|
|
2569
3623
|
return;
|
|
2570
3624
|
const ns = `group:${groupId}`;
|
|
3625
|
+
const generation = this.captureInlineGeneration();
|
|
3626
|
+
const afterSeq = this.syncState(ns).ack;
|
|
3627
|
+
this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(data.seq), true);
|
|
2571
3628
|
const targets = this.onlineUnreadHintTargets ?? new Map();
|
|
2572
3629
|
const owners = this.onlineUnreadHintOwners ?? new Set();
|
|
2573
3630
|
this.onlineUnreadHintTargets = targets;
|
|
@@ -2577,11 +3634,16 @@ export class MessageDeliveryEngine {
|
|
|
2577
3634
|
return;
|
|
2578
3635
|
owners.add(ns);
|
|
2579
3636
|
client._safeAsync((async () => {
|
|
3637
|
+
let failed = false;
|
|
3638
|
+
let attemptAfterSeq = afterSeq;
|
|
2580
3639
|
try {
|
|
2581
3640
|
while (true) {
|
|
2582
3641
|
const throughSeq = targets.get(ns) ?? 0;
|
|
2583
3642
|
const ackBefore = this.syncState(ns).ack;
|
|
3643
|
+
attemptAfterSeq = ackBefore;
|
|
2584
3644
|
await this.runGroupForwardRecovery(groupId, ns, 50, true, throughSeq);
|
|
3645
|
+
if (!this.isInlineGenerationCurrent(generation))
|
|
3646
|
+
return;
|
|
2585
3647
|
const ackAfter = this.syncState(ns).ack;
|
|
2586
3648
|
const target = targets.get(ns) ?? 0;
|
|
2587
3649
|
if (ackAfter >= target || ackAfter <= ackBefore)
|
|
@@ -2589,6 +3651,7 @@ export class MessageDeliveryEngine {
|
|
|
2589
3651
|
}
|
|
2590
3652
|
}
|
|
2591
3653
|
catch (exc) {
|
|
3654
|
+
failed = true;
|
|
2592
3655
|
client._clientLog?.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
2593
3656
|
}
|
|
2594
3657
|
finally {
|
|
@@ -2596,6 +3659,8 @@ export class MessageDeliveryEngine {
|
|
|
2596
3659
|
targets.delete(ns);
|
|
2597
3660
|
if (this.onlineUnreadHintOwners === owners)
|
|
2598
3661
|
owners.delete(ns);
|
|
3662
|
+
if (this.isInlineGenerationCurrent(generation))
|
|
3663
|
+
this.finishBackgroundPull(ns, attemptAfterSeq, failed);
|
|
2599
3664
|
}
|
|
2600
3665
|
})());
|
|
2601
3666
|
}
|
|
@@ -2604,6 +3669,7 @@ export class MessageDeliveryEngine {
|
|
|
2604
3669
|
const groupId = String(data.group_id ?? '').trim();
|
|
2605
3670
|
if (!groupId || client._sessionOptions?.background_sync === false)
|
|
2606
3671
|
return;
|
|
3672
|
+
this.recordBackgroundPullDemand(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
|
|
2607
3673
|
const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
|
|
2608
3674
|
const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
|
|
2609
3675
|
client._safeAsync(this.fillGroupEventGap(groupId, {
|
|
@@ -2639,14 +3705,14 @@ export class MessageDeliveryEngine {
|
|
|
2639
3705
|
max_pages: maxPages,
|
|
2640
3706
|
};
|
|
2641
3707
|
const invoke = async () => {
|
|
2642
|
-
const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages });
|
|
3708
|
+
const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, background, maxPages });
|
|
2643
3709
|
return { messages, raw_count: messages.length };
|
|
2644
3710
|
};
|
|
2645
3711
|
const pipeline = client._rpcPipeline;
|
|
2646
3712
|
if (!pipeline)
|
|
2647
3713
|
return await invoke();
|
|
2648
3714
|
const key = pipeline.pullGateKeyForCall('group.pull', request);
|
|
2649
|
-
return await pipeline.
|
|
3715
|
+
return await pipeline.runPullNamespaceSerialized(key, invoke, background);
|
|
2650
3716
|
}
|
|
2651
3717
|
syncState(ns) {
|
|
2652
3718
|
const tracker = this.runtime.client._seqTracker;
|
|
@@ -2953,6 +4019,8 @@ export class MessageDeliveryEngine {
|
|
|
2953
4019
|
}
|
|
2954
4020
|
async commitInlineMessage(ns, seq, event, payload, generation) {
|
|
2955
4021
|
const client = this.runtime.client;
|
|
4022
|
+
this.rememberCommittedSeqState(ns);
|
|
4023
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
2956
4024
|
const tracker = client._seqTracker;
|
|
2957
4025
|
let snapshot = null;
|
|
2958
4026
|
let trackerChanged = false;
|
|
@@ -2962,7 +4030,7 @@ export class MessageDeliveryEngine {
|
|
|
2962
4030
|
const published = selfSync
|
|
2963
4031
|
? (this.markPublishedSeq(ns, seq), true)
|
|
2964
4032
|
: await this.publishPulledMessage(event, ns, seq, payload, true, 'inline_push');
|
|
2965
|
-
if (!this.isInlineGenerationCurrent(generation))
|
|
4033
|
+
if (!this.isInlineGenerationCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
2966
4034
|
return false;
|
|
2967
4035
|
if (!published)
|
|
2968
4036
|
return false;
|
|
@@ -2976,12 +4044,13 @@ export class MessageDeliveryEngine {
|
|
|
2976
4044
|
if (!this.isInlineGenerationCurrent(generation))
|
|
2977
4045
|
return false;
|
|
2978
4046
|
await client._commitSeqTrackerState(ns);
|
|
2979
|
-
if (!this.isInlineGenerationCurrent(generation))
|
|
4047
|
+
if (!this.isInlineGenerationCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
2980
4048
|
return false;
|
|
2981
4049
|
return true;
|
|
2982
4050
|
}
|
|
2983
4051
|
catch (exc) {
|
|
2984
|
-
if (trackerChanged && this.isInlineGenerationCurrent(generation) &&
|
|
4052
|
+
if (trackerChanged && this.isInlineGenerationCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
4053
|
+
&& typeof tracker.restoreNamespaceSnapshot === 'function') {
|
|
2985
4054
|
tracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
|
|
2986
4055
|
}
|
|
2987
4056
|
client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
|
|
@@ -3220,16 +4289,17 @@ export class MessageDeliveryEngine {
|
|
|
3220
4289
|
return { messages: [], raw_count: 0 };
|
|
3221
4290
|
const request = { group_id: groupId, ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
|
|
3222
4291
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
3223
|
-
return await client._rpcPipeline.
|
|
4292
|
+
return await client._rpcPipeline.runPullNamespaceSerialized(key, async () => {
|
|
3224
4293
|
if (tail) {
|
|
3225
4294
|
const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
|
|
3226
4295
|
group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
|
|
3227
|
-
}, true);
|
|
4296
|
+
}, true, true);
|
|
3228
4297
|
tailCompleted = true;
|
|
3229
4298
|
return tailResult;
|
|
3230
4299
|
}
|
|
3231
4300
|
const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
|
|
3232
4301
|
gateLocked: true,
|
|
4302
|
+
background,
|
|
3233
4303
|
...(maxPages ? { maxPages } : {}),
|
|
3234
4304
|
});
|
|
3235
4305
|
return { messages, raw_count: messages.length };
|
|
@@ -3378,26 +4448,38 @@ export class MessageDeliveryEngine {
|
|
|
3378
4448
|
}
|
|
3379
4449
|
return;
|
|
3380
4450
|
}
|
|
4451
|
+
if (client._sessionOptions?.background_sync !== false)
|
|
4452
|
+
this.recordBackgroundPullDemand(ns, seq);
|
|
3381
4453
|
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
3382
4454
|
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
3383
4455
|
this.recordPendingPull(ns, seq);
|
|
3384
4456
|
client._clientLog?.debug(`V2 group push queued behind in-flight realtime sync: ns=${ns} seq=${seq}`);
|
|
3385
4457
|
return;
|
|
3386
4458
|
}
|
|
4459
|
+
const generation = this.captureInlineGeneration();
|
|
4460
|
+
let failed = false;
|
|
3387
4461
|
try {
|
|
3388
4462
|
// namespace 串行区必须覆盖 Tail 与其后至多一次 Forward 的完整周期。
|
|
3389
4463
|
await this.runGroupRealtimeSync(groupId, ns, seq, 50, false);
|
|
3390
4464
|
}
|
|
3391
4465
|
catch (exc) {
|
|
4466
|
+
failed = true;
|
|
3392
4467
|
client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
3393
4468
|
}
|
|
3394
4469
|
finally {
|
|
3395
|
-
this.
|
|
3396
|
-
|
|
4470
|
+
if (this.isInlineGenerationCurrent(generation)) {
|
|
4471
|
+
this.endRealtimeSync(ns);
|
|
4472
|
+
this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
4473
|
+
this.finishBackgroundPull(ns, before.ack, failed);
|
|
4474
|
+
}
|
|
3397
4475
|
}
|
|
3398
4476
|
}
|
|
3399
4477
|
async onV2PushNotification(data) {
|
|
3400
4478
|
const client = this.runtime.client;
|
|
4479
|
+
if (client._p2pPeerPullEnabled) {
|
|
4480
|
+
this.handlePeerNotification(data);
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
3401
4483
|
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
3402
4484
|
if (!ns)
|
|
3403
4485
|
return;
|
|
@@ -3466,31 +4548,64 @@ export class MessageDeliveryEngine {
|
|
|
3466
4548
|
}
|
|
3467
4549
|
return;
|
|
3468
4550
|
}
|
|
4551
|
+
if (client._sessionOptions?.background_sync !== false)
|
|
4552
|
+
this.recordBackgroundPullDemand(ns, pushSeq);
|
|
3469
4553
|
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
3470
4554
|
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
3471
4555
|
this.recordPendingPull(ns, pushSeq);
|
|
3472
4556
|
client._clientLog?.debug(`V2 P2P push queued behind in-flight realtime sync: ns=${ns} seq=${pushSeq}`);
|
|
3473
4557
|
return;
|
|
3474
4558
|
}
|
|
4559
|
+
const generation = this.captureInlineGeneration();
|
|
4560
|
+
let failed = false;
|
|
3475
4561
|
try {
|
|
3476
4562
|
await this.runP2PRealtimeSync(ns, pushSeq, 50, false);
|
|
3477
4563
|
}
|
|
3478
4564
|
catch (exc) {
|
|
4565
|
+
failed = true;
|
|
3479
4566
|
client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
|
|
3480
4567
|
}
|
|
3481
4568
|
finally {
|
|
3482
|
-
this.
|
|
3483
|
-
|
|
4569
|
+
if (this.isInlineGenerationCurrent(generation)) {
|
|
4570
|
+
this.endRealtimeSync(ns);
|
|
4571
|
+
this.schedulePendingPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
4572
|
+
this.finishBackgroundPull(ns, before.ack, failed);
|
|
4573
|
+
}
|
|
3484
4574
|
}
|
|
3485
4575
|
}
|
|
3486
4576
|
enqueueOnlineUnreadP2pHint(data) {
|
|
3487
4577
|
const client = this.runtime.client;
|
|
3488
|
-
if (
|
|
4578
|
+
if (client._p2pPeerPullEnabled) {
|
|
4579
|
+
this.requestPeerDirectoryRefresh();
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
4582
|
+
if (!client._aid || !client._v2Session || client._sessionOptions?.background_sync === false)
|
|
3489
4583
|
return;
|
|
3490
4584
|
const ns = `p2p:${client._aid}`;
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
4585
|
+
const generation = this.captureInlineGeneration();
|
|
4586
|
+
const afterSeq = this.syncState(ns).ack;
|
|
4587
|
+
this.recordBackgroundPullDemand(ns, positiveSafeSequenceHint(data.seq), true);
|
|
4588
|
+
const owners = this.onlineUnreadHintOwners ?? new Set();
|
|
4589
|
+
this.onlineUnreadHintOwners = owners;
|
|
4590
|
+
if (owners.has(ns))
|
|
4591
|
+
return;
|
|
4592
|
+
owners.add(ns);
|
|
4593
|
+
client._safeAsync((async () => {
|
|
4594
|
+
let failed = false;
|
|
4595
|
+
try {
|
|
4596
|
+
await this.runP2PForwardRecovery(ns, 50, true);
|
|
4597
|
+
}
|
|
4598
|
+
catch (exc) {
|
|
4599
|
+
failed = true;
|
|
4600
|
+
client._clientLog?.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
4601
|
+
}
|
|
4602
|
+
finally {
|
|
4603
|
+
if (this.onlineUnreadHintOwners === owners)
|
|
4604
|
+
owners.delete(ns);
|
|
4605
|
+
if (this.isInlineGenerationCurrent(generation))
|
|
4606
|
+
this.finishBackgroundPull(ns, afterSeq, failed);
|
|
4607
|
+
}
|
|
4608
|
+
})());
|
|
3494
4609
|
}
|
|
3495
4610
|
async restoreSeqTrackerState() {
|
|
3496
4611
|
const client = this.runtime.client;
|
|
@@ -3511,6 +4626,7 @@ export class MessageDeliveryEngine {
|
|
|
3511
4626
|
if (state && typeof state === 'object' && Object.keys(state).length > 0) {
|
|
3512
4627
|
state = await this.migrateSeqStateGroupIds(state);
|
|
3513
4628
|
client._seqTracker.restoreSyncState(state);
|
|
4629
|
+
this.restoreCommittedSeqStates();
|
|
3514
4630
|
return;
|
|
3515
4631
|
}
|
|
3516
4632
|
}
|
|
@@ -3522,6 +4638,7 @@ export class MessageDeliveryEngine {
|
|
|
3522
4638
|
if (state && typeof state === 'object' && Object.keys(state).length > 0) {
|
|
3523
4639
|
state = await this.migrateSeqStateGroupIds(state);
|
|
3524
4640
|
client._seqTracker.restoreState(state);
|
|
4641
|
+
this.restoreCommittedSeqStates();
|
|
3525
4642
|
}
|
|
3526
4643
|
return;
|
|
3527
4644
|
}
|
|
@@ -3536,6 +4653,7 @@ export class MessageDeliveryEngine {
|
|
|
3536
4653
|
if (isJsonObject(state)) {
|
|
3537
4654
|
const migrated = await this.migrateSeqStateGroupIds(state);
|
|
3538
4655
|
client._seqTracker.restoreState(migrated);
|
|
4656
|
+
this.restoreCommittedSeqStates();
|
|
3539
4657
|
}
|
|
3540
4658
|
}
|
|
3541
4659
|
}
|
|
@@ -3552,6 +4670,16 @@ export class MessageDeliveryEngine {
|
|
|
3552
4670
|
async migrateSeqStateGroupIds(state) {
|
|
3553
4671
|
return state;
|
|
3554
4672
|
}
|
|
4673
|
+
restoreCommittedSeqStates() {
|
|
4674
|
+
this.peerContext();
|
|
4675
|
+
for (const [ns, state] of Object.entries(this.runtime.client._seqTracker.exportSyncState())) {
|
|
4676
|
+
if (!this.isReliableSeqNamespace(ns))
|
|
4677
|
+
continue;
|
|
4678
|
+
this.committedSeqTrackerStates.set(ns, { ...state });
|
|
4679
|
+
if (ns.startsWith('p2p.peer.v1/'))
|
|
4680
|
+
this.peerInitialized.add(ns);
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
3555
4683
|
async saveSeqTrackerState() {
|
|
3556
4684
|
const client = this.runtime.client;
|
|
3557
4685
|
const context = this.currentSeqTrackerContext();
|
|
@@ -3562,6 +4690,7 @@ export class MessageDeliveryEngine {
|
|
|
3562
4690
|
const state = typeof client._seqTracker.exportSyncState === 'function'
|
|
3563
4691
|
? client._seqTracker.exportSyncState()
|
|
3564
4692
|
: Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
|
|
4693
|
+
this.useCommittedSeqStates(state);
|
|
3565
4694
|
const pending = this.seqTrackerPending();
|
|
3566
4695
|
if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
|
|
3567
4696
|
const liveNamespaces = new Set(Object.keys(state));
|
|
@@ -3592,11 +4721,15 @@ export class MessageDeliveryEngine {
|
|
|
3592
4721
|
const state = typeof client._seqTracker.getSyncState === 'function'
|
|
3593
4722
|
? client._seqTracker.getSyncState(ns)
|
|
3594
4723
|
: { ack: seq, tail: seq, head: seq };
|
|
3595
|
-
|
|
4724
|
+
const pendingState = { [ns]: state };
|
|
4725
|
+
this.useCommittedSeqStates(pendingState);
|
|
4726
|
+
this.mergeSeqTrackerPending(context, pendingState);
|
|
3596
4727
|
this.scheduleSeqTrackerFlush();
|
|
3597
4728
|
}
|
|
3598
4729
|
async commitSeqTrackerState(ns) {
|
|
3599
4730
|
const client = this.runtime.client;
|
|
4731
|
+
const tracker = client._seqTracker;
|
|
4732
|
+
const namespaceGeneration = this.seqTrackerNamespaceGenerations.get(ns) ?? 0;
|
|
3600
4733
|
const context = this.currentSeqTrackerContext();
|
|
3601
4734
|
if (!context || !ns)
|
|
3602
4735
|
return;
|
|
@@ -3609,10 +4742,18 @@ export class MessageDeliveryEngine {
|
|
|
3609
4742
|
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
3610
4743
|
const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
|
|
3611
4744
|
try {
|
|
3612
|
-
await this.
|
|
4745
|
+
await this.enqueueSeqTrackerWrite(async () => {
|
|
4746
|
+
if (client._seqTracker !== tracker)
|
|
4747
|
+
throw new StateError('seq tracker commit invalidated');
|
|
4748
|
+
await this.writeSeqTrackerStateNow(aid, deviceId, slotId, { [ns]: state }, true);
|
|
4749
|
+
if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration
|
|
4750
|
+
&& this.currentSeqTrackerContext() === context && this.isReliableSeqNamespace(ns))
|
|
4751
|
+
this.committedSeqTrackerStates.set(ns, { ...state });
|
|
4752
|
+
});
|
|
3613
4753
|
}
|
|
3614
4754
|
catch (exc) {
|
|
3615
|
-
this.
|
|
4755
|
+
if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration)
|
|
4756
|
+
this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
|
|
3616
4757
|
throw exc;
|
|
3617
4758
|
}
|
|
3618
4759
|
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
@@ -3719,7 +4860,25 @@ export class MessageDeliveryEngine {
|
|
|
3719
4860
|
}
|
|
3720
4861
|
}
|
|
3721
4862
|
async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
3722
|
-
await this.enqueueSeqTrackerWrite(() =>
|
|
4863
|
+
await this.enqueueSeqTrackerWrite(() => {
|
|
4864
|
+
if (!throwOnError && this.currentSeqTrackerContext() === JSON.stringify([aid, deviceId, slotId]))
|
|
4865
|
+
this.useCommittedSeqStates(state);
|
|
4866
|
+
return this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError);
|
|
4867
|
+
});
|
|
4868
|
+
}
|
|
4869
|
+
useCommittedSeqStates(state) {
|
|
4870
|
+
this.peerContext();
|
|
4871
|
+
for (const ns of Object.keys(state)) {
|
|
4872
|
+
if (!this.isReliableSeqNamespace(ns))
|
|
4873
|
+
continue;
|
|
4874
|
+
if (!ns.startsWith('p2p.peer.v1/'))
|
|
4875
|
+
this.rememberCommittedSeqState(ns);
|
|
4876
|
+
const durable = this.committedSeqTrackerStates.get(ns);
|
|
4877
|
+
if (durable)
|
|
4878
|
+
state[ns] = { ...durable };
|
|
4879
|
+
else
|
|
4880
|
+
delete state[ns];
|
|
4881
|
+
}
|
|
3723
4882
|
}
|
|
3724
4883
|
enqueueSeqTrackerWrite(operation) {
|
|
3725
4884
|
const run = this.seqTrackerWriteChain.then(operation, operation);
|
|
@@ -3773,6 +4932,11 @@ export class MessageDeliveryEngine {
|
|
|
3773
4932
|
const deviceId = client._deviceId;
|
|
3774
4933
|
const slotId = client._slotId;
|
|
3775
4934
|
const seq = client._seqTracker.getContiguousSeq(ns);
|
|
4935
|
+
if (seq <= 0) {
|
|
4936
|
+
this.seqTrackerNamespaceGenerations.set(ns, (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) + 1);
|
|
4937
|
+
this.committedSeqTrackerStates.delete(ns);
|
|
4938
|
+
this.peerInitialized.delete(ns);
|
|
4939
|
+
}
|
|
3776
4940
|
this.dropSeqTrackerPending(ns);
|
|
3777
4941
|
const saveSeq = client._tokenStore.saveSeq;
|
|
3778
4942
|
if (seq > 0 && typeof saveSeq === 'function') {
|
|
@@ -3830,7 +4994,9 @@ export class MessageDeliveryEngine {
|
|
|
3830
4994
|
if (next < 0)
|
|
3831
4995
|
next = 0;
|
|
3832
4996
|
if (ns) {
|
|
3833
|
-
|
|
4997
|
+
if (ns.startsWith('group:') || ns.startsWith('group_event:'))
|
|
4998
|
+
this.rememberCommittedSeqState(ns);
|
|
4999
|
+
const committedAck = this.isReliableSeqNamespace(ns) ? this.committedSeqTrackerAck(ns) : client._seqTracker.getContiguousSeq(ns);
|
|
3834
5000
|
if (next > committedAck) {
|
|
3835
5001
|
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
|
|
3836
5002
|
next = committedAck;
|
|
@@ -3860,11 +5026,11 @@ export class MessageDeliveryEngine {
|
|
|
3860
5026
|
return next === raw ? params : { ...params, [field]: next };
|
|
3861
5027
|
};
|
|
3862
5028
|
if (method === 'message.v2.ack') {
|
|
3863
|
-
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
5029
|
+
const ns = client._p2pPeerPullEnabled ? this.peerNamespace(String(params.sender_aid ?? params.peer_aid ?? '').trim()) : client._aid ? `p2p:${client._aid}` : '';
|
|
3864
5030
|
return clampField('up_to_seq', ns);
|
|
3865
5031
|
}
|
|
3866
5032
|
if (method === 'message.ack') {
|
|
3867
|
-
const ns = client._aid ? `p2p:${client._aid}` : '';
|
|
5033
|
+
const ns = client._p2pPeerPullEnabled ? this.peerNamespace(String(params.sender_aid ?? params.peer_aid ?? '').trim()) : client._aid ? `p2p:${client._aid}` : '';
|
|
3868
5034
|
return clampField('seq', ns);
|
|
3869
5035
|
}
|
|
3870
5036
|
if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
|
|
@@ -3879,7 +5045,18 @@ export class MessageDeliveryEngine {
|
|
|
3879
5045
|
}
|
|
3880
5046
|
return params;
|
|
3881
5047
|
}
|
|
3882
|
-
|
|
5048
|
+
consumeOrderedSequence(ns, seq, page) {
|
|
5049
|
+
const client = this.runtime.client;
|
|
5050
|
+
const queue = client._pendingOrderedMsgs.get(ns);
|
|
5051
|
+
if (page?.batch)
|
|
5052
|
+
page.consumed.add(seq);
|
|
5053
|
+
this.deferApplicationEffect(page, () => {
|
|
5054
|
+
queue?.delete(seq);
|
|
5055
|
+
if (queue?.size === 0 && client._pendingOrderedMsgs.get(ns) === queue)
|
|
5056
|
+
client._pendingOrderedMsgs.delete(ns);
|
|
5057
|
+
});
|
|
5058
|
+
}
|
|
5059
|
+
async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, batch, source = 'pull', throughSeq, page) {
|
|
3883
5060
|
const client = this.runtime.client;
|
|
3884
5061
|
const pullScoped = this.isPullScopedSource(source);
|
|
3885
5062
|
if (pullScoped)
|
|
@@ -3894,15 +5071,40 @@ export class MessageDeliveryEngine {
|
|
|
3894
5071
|
.sort((a, b) => a - b);
|
|
3895
5072
|
let delivered = false;
|
|
3896
5073
|
const drainBatches = new Map();
|
|
5074
|
+
const deferredPages = this.deferredApplicationPages.get(ns);
|
|
5075
|
+
const isolatedPage = page && deferredPages && [...deferredPages.values()].includes(page);
|
|
3897
5076
|
try {
|
|
3898
5077
|
for (const seq of ready) {
|
|
3899
5078
|
if (pullScoped)
|
|
3900
5079
|
this.ensurePullOperationCurrent(ns);
|
|
3901
5080
|
const item = queue.get(seq);
|
|
3902
|
-
if (!item)
|
|
5081
|
+
if (!item || page?.consumed.has(seq))
|
|
5082
|
+
continue;
|
|
5083
|
+
let deferredPage = deferredPages?.get(seq);
|
|
5084
|
+
if (isolatedPage && deferredPage !== page)
|
|
3903
5085
|
continue;
|
|
5086
|
+
if (deferredPage && deferredPage !== page) {
|
|
5087
|
+
if ([...deferredPages].some(([pendingSeq, pendingPage]) => pendingPage === deferredPage && !ready.includes(pendingSeq)))
|
|
5088
|
+
continue;
|
|
5089
|
+
await this.flushApplicationPage(page);
|
|
5090
|
+
deferredPage.ensureCurrent();
|
|
5091
|
+
if (deferredPage.closed) {
|
|
5092
|
+
const retry = this.beginApplicationPage(ns, deferredPage.source);
|
|
5093
|
+
retry.handlers = deferredPage.handlers;
|
|
5094
|
+
retry.batch = deferredPage.batch;
|
|
5095
|
+
retry.tailProof = deferredPage.tailProof;
|
|
5096
|
+
for (const [pendingSeq, pendingPage] of deferredPages)
|
|
5097
|
+
if (pendingPage === deferredPage)
|
|
5098
|
+
deferredPages.set(pendingSeq, retry);
|
|
5099
|
+
deferredPage = retry;
|
|
5100
|
+
}
|
|
5101
|
+
deferredPage.deferred = false;
|
|
5102
|
+
await this.drainOrderedMessages(ns, beforeSeq, pullResponse, false, batch, source, throughSeq, deferredPage);
|
|
5103
|
+
await this.flushApplicationPage(deferredPage);
|
|
5104
|
+
continue;
|
|
5105
|
+
}
|
|
3904
5106
|
if (client._pushedSeqs.get(ns)?.has(seq)) {
|
|
3905
|
-
|
|
5107
|
+
this.consumeOrderedSequence(ns, seq, page);
|
|
3906
5108
|
continue;
|
|
3907
5109
|
}
|
|
3908
5110
|
const itemSource = item.source ?? source;
|
|
@@ -3915,15 +5117,16 @@ export class MessageDeliveryEngine {
|
|
|
3915
5117
|
drainBatches.set(itemSource, itemBatch);
|
|
3916
5118
|
}
|
|
3917
5119
|
}
|
|
3918
|
-
|
|
5120
|
+
if (!page?.seen.has(`ordered:${seq}`))
|
|
5121
|
+
await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse, itemSource, itemBatch, page);
|
|
3919
5122
|
if (!this.isInlineGenerationCurrent(generation))
|
|
3920
5123
|
throw new Error('pull invalidated');
|
|
3921
5124
|
if (pullScoped)
|
|
3922
5125
|
this.ensurePullOperationCurrent(ns);
|
|
3923
|
-
|
|
5126
|
+
this.consumeOrderedSequence(ns, seq, page);
|
|
3924
5127
|
if (item.event === 'group.changed' && isJsonObject(item.payload) && item.payload.action === 'dissolved')
|
|
3925
5128
|
continue;
|
|
3926
|
-
this.markPublishedSeq(ns, seq);
|
|
5129
|
+
this.markPublishedSeq(ns, seq, page);
|
|
3927
5130
|
delivered = true;
|
|
3928
5131
|
}
|
|
3929
5132
|
}
|
|
@@ -3932,7 +5135,7 @@ export class MessageDeliveryEngine {
|
|
|
3932
5135
|
await this.flushRealtimeDeliveryChanges(drainBatch);
|
|
3933
5136
|
}
|
|
3934
5137
|
}
|
|
3935
|
-
if (queue.size === 0) {
|
|
5138
|
+
if (!page?.batch && queue.size === 0) {
|
|
3936
5139
|
client._pendingOrderedMsgs.delete(ns);
|
|
3937
5140
|
if (delivered && persist) {
|
|
3938
5141
|
if (pullScoped)
|
|
@@ -3941,7 +5144,7 @@ export class MessageDeliveryEngine {
|
|
|
3941
5144
|
}
|
|
3942
5145
|
}
|
|
3943
5146
|
}
|
|
3944
|
-
async publishOrderedMessage(event, ns, seq, payload, source = 'push', operationBatch) {
|
|
5147
|
+
async publishOrderedMessage(event, ns, seq, payload, source = 'push', operationBatch, page) {
|
|
3945
5148
|
const client = this.runtime.client;
|
|
3946
5149
|
const generation = this.captureInlineGeneration();
|
|
3947
5150
|
const ownsBatch = !operationBatch && (source === 'push' || source === 'inline_push');
|
|
@@ -3949,39 +5152,55 @@ export class MessageDeliveryEngine {
|
|
|
3949
5152
|
try {
|
|
3950
5153
|
const seqNum = Number(seq);
|
|
3951
5154
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
|
|
3952
|
-
await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch);
|
|
5155
|
+
await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch, page);
|
|
3953
5156
|
return true;
|
|
3954
5157
|
}
|
|
3955
|
-
if (client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
5158
|
+
if (page?.consumed.has(seqNum) || client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
3956
5159
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
3957
|
-
|
|
3958
|
-
if (queue && queue.size === 0)
|
|
5160
|
+
this.consumeOrderedSequence(ns, seqNum, page);
|
|
5161
|
+
if (!page?.batch && queue && queue.size === 0)
|
|
3959
5162
|
client._pendingOrderedMsgs.delete(ns);
|
|
3960
5163
|
return false;
|
|
3961
5164
|
}
|
|
3962
5165
|
if (event === 'message.received' && this.isSelfSentP2PMessage(payload)) {
|
|
3963
|
-
this.markPublishedSeq(ns, seqNum);
|
|
5166
|
+
this.markPublishedSeq(ns, seqNum, page);
|
|
3964
5167
|
client._clientLog.debug(`publish ordered skipped current-device self-sync: ns=${ns}, seq=${seqNum}`);
|
|
3965
5168
|
return false;
|
|
3966
5169
|
}
|
|
3967
5170
|
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
3968
5171
|
if (seqNum > contig) {
|
|
5172
|
+
const owner = this.deferredApplicationPages.get(ns)?.get(seqNum);
|
|
5173
|
+
if (page && source === 'tail' && owner && owner !== page)
|
|
5174
|
+
return false;
|
|
3969
5175
|
this.enqueueOrderedMessage(ns, event, seqNum, payload, source);
|
|
5176
|
+
if (page && source === 'tail') {
|
|
5177
|
+
let pages = this.deferredApplicationPages.get(ns);
|
|
5178
|
+
if (!pages) {
|
|
5179
|
+
pages = new Map();
|
|
5180
|
+
this.deferredApplicationPages.set(ns, pages);
|
|
5181
|
+
}
|
|
5182
|
+
pages.set(seqNum, page);
|
|
5183
|
+
page.deferred = true;
|
|
5184
|
+
if (page.batch) {
|
|
5185
|
+
await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch, page);
|
|
5186
|
+
page.seen.add(`ordered:${seqNum}`);
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
3970
5189
|
return false;
|
|
3971
5190
|
}
|
|
3972
|
-
await this.drainOrderedMessages(ns, seqNum, true, true, batch, source);
|
|
3973
|
-
if (client._pushedSeqs.get(ns)?.has(seqNum))
|
|
5191
|
+
await this.drainOrderedMessages(ns, seqNum, true, true, batch, source, undefined, page);
|
|
5192
|
+
if (page?.consumed.has(seqNum) || client._pushedSeqs.get(ns)?.has(seqNum))
|
|
3974
5193
|
return false;
|
|
3975
5194
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
3976
|
-
await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch);
|
|
5195
|
+
await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch, page);
|
|
3977
5196
|
if (!this.isInlineGenerationCurrent(generation))
|
|
3978
5197
|
throw new Error('pull invalidated');
|
|
3979
|
-
|
|
3980
|
-
if (queue && queue.size === 0)
|
|
5198
|
+
this.consumeOrderedSequence(ns, seqNum, page);
|
|
5199
|
+
if (!page?.batch && queue && queue.size === 0)
|
|
3981
5200
|
client._pendingOrderedMsgs.delete(ns);
|
|
3982
|
-
this.markPublishedSeq(ns, seqNum);
|
|
3983
|
-
await this.drainOrderedMessages(ns, undefined, false, true, batch, source);
|
|
3984
|
-
if (!client._pendingOrderedMsgs.get(ns))
|
|
5201
|
+
this.markPublishedSeq(ns, seqNum, page);
|
|
5202
|
+
await this.drainOrderedMessages(ns, undefined, false, true, batch, source, undefined, page);
|
|
5203
|
+
if (!page?.batch && !client._pendingOrderedMsgs.get(ns))
|
|
3985
5204
|
await this.saveSeqTrackerState();
|
|
3986
5205
|
return true;
|
|
3987
5206
|
}
|
|
@@ -3990,8 +5209,16 @@ export class MessageDeliveryEngine {
|
|
|
3990
5209
|
await this.flushRealtimeDeliveryChanges(batch);
|
|
3991
5210
|
}
|
|
3992
5211
|
}
|
|
3993
|
-
async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull', operationBatch) {
|
|
5212
|
+
async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull', operationBatch, page) {
|
|
3994
5213
|
const client = this.runtime.client;
|
|
5214
|
+
const deferredPage = this.deferredApplicationPages.get(ns)?.get(Number(seq));
|
|
5215
|
+
if (deferredPage && deferredPage !== page && !deferredPage.flushed)
|
|
5216
|
+
return false;
|
|
5217
|
+
if (client._p2pPeerPullEnabled && event === 'message.received' && ns.startsWith('p2p.peer.v1/')) {
|
|
5218
|
+
await client._publishAppEvent(event, payload, source, ns, operationBatch, page);
|
|
5219
|
+
this.markPublishedSeq(ns, Number(seq), page);
|
|
5220
|
+
return true;
|
|
5221
|
+
}
|
|
3995
5222
|
const generation = this.captureInlineGeneration();
|
|
3996
5223
|
const ownsBatch = !operationBatch && (source === 'push' || source === 'inline_push');
|
|
3997
5224
|
const batch = operationBatch ?? (ownsBatch ? this.createDeliveryChangeBatch(source) : undefined);
|
|
@@ -4002,52 +5229,52 @@ export class MessageDeliveryEngine {
|
|
|
4002
5229
|
const seqNum = Number(seq);
|
|
4003
5230
|
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
|
|
4004
5231
|
if (event === 'message.recalled') {
|
|
4005
|
-
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload, source));
|
|
5232
|
+
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload, source, page));
|
|
4006
5233
|
if (pullScoped)
|
|
4007
5234
|
this.ensurePullOperationCurrent(ns);
|
|
4008
5235
|
return published;
|
|
4009
5236
|
}
|
|
4010
|
-
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
|
|
5237
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch, page));
|
|
4011
5238
|
if (pullScoped)
|
|
4012
5239
|
this.ensurePullOperationCurrent(ns);
|
|
4013
5240
|
return true;
|
|
4014
5241
|
}
|
|
4015
5242
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
4016
|
-
if (client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
4017
|
-
|
|
4018
|
-
if (queue && queue.size === 0)
|
|
5243
|
+
if (page?.consumed.has(seqNum) || client._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
5244
|
+
this.consumeOrderedSequence(ns, seqNum, page);
|
|
5245
|
+
if (!page?.batch && queue && queue.size === 0)
|
|
4019
5246
|
client._pendingOrderedMsgs.delete(ns);
|
|
4020
5247
|
return false;
|
|
4021
5248
|
}
|
|
4022
5249
|
if (event === 'message.received' && this.isSelfSentP2PMessage(payload)) {
|
|
4023
|
-
this.markPublishedSeq(ns, seqNum);
|
|
5250
|
+
this.markPublishedSeq(ns, seqNum, page);
|
|
4024
5251
|
client._clientLog.debug(`publish pulled skipped current-device self-sync: ns=${ns}, seq=${seqNum}`);
|
|
4025
5252
|
return source === 'inline_push';
|
|
4026
5253
|
}
|
|
4027
|
-
await this.drainOrderedMessages(ns, seqNum, false, persist, batch, source);
|
|
5254
|
+
await this.drainOrderedMessages(ns, seqNum, false, persist, batch, source, undefined, page);
|
|
4028
5255
|
if (pullScoped)
|
|
4029
5256
|
this.ensurePullOperationCurrent(ns);
|
|
4030
5257
|
if (event === 'message.recalled') {
|
|
4031
|
-
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload, source));
|
|
5258
|
+
const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload, source, page));
|
|
4032
5259
|
if (!this.isInlineGenerationCurrent(generation))
|
|
4033
5260
|
throw new Error('pull invalidated');
|
|
4034
5261
|
if (pullScoped)
|
|
4035
5262
|
this.ensurePullOperationCurrent(ns);
|
|
4036
|
-
|
|
4037
|
-
if (queue && queue.size === 0)
|
|
5263
|
+
this.consumeOrderedSequence(ns, seqNum, page);
|
|
5264
|
+
if (!page?.batch && queue && queue.size === 0)
|
|
4038
5265
|
client._pendingOrderedMsgs.delete(ns);
|
|
4039
|
-
this.markPublishedSeq(ns, seqNum);
|
|
5266
|
+
this.markPublishedSeq(ns, seqNum, page);
|
|
4040
5267
|
return published;
|
|
4041
5268
|
}
|
|
4042
|
-
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
|
|
5269
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch, page));
|
|
4043
5270
|
if (!this.isInlineGenerationCurrent(generation))
|
|
4044
5271
|
throw new Error('pull invalidated');
|
|
4045
5272
|
if (pullScoped)
|
|
4046
5273
|
this.ensurePullOperationCurrent(ns);
|
|
4047
|
-
|
|
4048
|
-
if (queue && queue.size === 0)
|
|
5274
|
+
this.consumeOrderedSequence(ns, seqNum, page);
|
|
5275
|
+
if (!page?.batch && queue && queue.size === 0)
|
|
4049
5276
|
client._pendingOrderedMsgs.delete(ns);
|
|
4050
|
-
this.markPublishedSeq(ns, seqNum);
|
|
5277
|
+
this.markPublishedSeq(ns, seqNum, page);
|
|
4051
5278
|
return true;
|
|
4052
5279
|
}
|
|
4053
5280
|
finally {
|