@agentunion/fastaun 0.5.20 → 0.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/_packed_docs/CHANGELOG.md +104 -0
- package/_packed_docs/INDEX.md +59 -34
- package/_packed_docs/KITE_DOCS_GUIDE.md +59 -52
- 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/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +307 -67
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +41 -15
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +33 -22
- 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/0.5.22-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +212 -0
- package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +199 -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 +4 -1
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -14
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
- package/_packed_docs/sdk/09-group-rpc-manual.md +15 -2
- package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +73 -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 +90 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +33 -25
- package/_packed_docs/sdk/INDEX.md +67 -33
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +74 -57
- 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 +312 -300
- 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 +322 -382
- 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 -708
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +28 -20
- 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 +190 -97
- 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 +158 -127
- package/dist/agent-md-schema.js +6 -4
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +22 -2
- package/dist/agent-md.js +92 -68
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +13 -0
- package/dist/aid-store.js +50 -15
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +22 -2
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.js +13 -0
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +87 -1
- package/dist/client/delivery.js +1387 -139
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +3 -0
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.js +149 -37
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +12 -2
- package/dist/client/v2-e2ee.js +831 -304
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +26 -2
- package/dist/client.js +412 -68
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +18 -1
- package/dist/events.js +148 -9
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +43 -1
- package/dist/facades.js +363 -7
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.d.ts +1 -0
- package/dist/group-fs.js +37 -19
- package/dist/group-fs.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +6 -1
- package/dist/keystore/aid-db.js +184 -42
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +14 -0
- package/dist/keystore/local-token-store.d.ts +9 -1
- package/dist/keystore/local-token-store.js +79 -14
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +46 -0
- package/dist/org-protocol.js +802 -12
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +36 -5
- package/dist/ownership-v21.js +168 -52
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +24 -2
- package/dist/ownership-workflow.js +682 -41
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/secret-store/file-store.js +84 -0
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +2 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +6 -0
- package/dist/storage/lowlevel.js +24 -7
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/read-cache.d.ts +70 -0
- package/dist/storage/read-cache.js +625 -0
- package/dist/storage/read-cache.js.map +1 -0
- package/dist/storage/vfs.d.ts +2 -0
- package/dist/storage/vfs.js +50 -15
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +86 -3
- package/dist/tools/cross-sdk-agent.js +758 -46
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.js +104 -46
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/v2/e2ee/encrypt-group.js +1 -0
- package/dist/v2/e2ee/encrypt-group.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -0
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/e2ee/types.d.ts +1 -0
- 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,5 +1,8 @@
|
|
|
1
|
-
import { slotIsolationKey } from '../config.js';
|
|
1
|
+
import { _INTERNAL_MESSAGE_FLOW_LIMITS, slotIsolationKey } from '../config.js';
|
|
2
|
+
import { parseFileFrontmatter } from '../agent-md-schema.js';
|
|
3
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
4
|
import { trafficLogContext, withLogContext } from '../logger.js';
|
|
5
|
+
import { normalizeGroupAid } from '../group-id.js';
|
|
3
6
|
import { ConnectionState, isJsonObject } from '../types.js';
|
|
4
7
|
import { validateGroupAIDFormat } from '../validators.js';
|
|
5
8
|
import { hasGroupMentionModeField, normalizeGroupMentionMode } from './mention-mode.js';
|
|
@@ -10,16 +13,35 @@ const GROUP_RECALL_SEEN_LIMIT = 10_000;
|
|
|
10
13
|
const VOLATILE_SEEN_LIMIT = 10_000;
|
|
11
14
|
const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
|
|
12
15
|
const BACKGROUND_PULL_RETRY_DELAY_MS = 250;
|
|
16
|
+
function isTerminalGroupPullError(error) {
|
|
17
|
+
if (!error || typeof error !== 'object')
|
|
18
|
+
return false;
|
|
19
|
+
const failure = error;
|
|
20
|
+
if (typeof failure.data?.retryable === 'boolean')
|
|
21
|
+
return !failure.data.retryable;
|
|
22
|
+
if (failure.retryable === true)
|
|
23
|
+
return false;
|
|
24
|
+
const message = String(failure.message ?? '').trim().toLowerCase();
|
|
25
|
+
if (['timeout', 'deadline exceeded', 'gateway service degraded', 'certificate not loaded',
|
|
26
|
+
'long_connection_already_exists', 'long connection already exists'].some((text) => message.includes(text)))
|
|
27
|
+
return false;
|
|
28
|
+
return [-32001, -32003, -32004, -32600, -32601, -32602].includes(failure.code ?? 0)
|
|
29
|
+
|| (typeof failure.code === 'number' && failure.code >= -33009 && failure.code <= -33001)
|
|
30
|
+
|| ['banned members', 'blocked members', 'member is banned', 'member is blocked',
|
|
31
|
+
'group is not active', 'group is dissolved', 'group is suspended',
|
|
32
|
+
'group not found', 'member not found', 'not a member'].some((text) => message.includes(text));
|
|
33
|
+
}
|
|
13
34
|
const DELIVERY_CHANGED_MESSAGE_EVENTS = new Set([
|
|
14
35
|
'message.received',
|
|
15
36
|
'group.message_created',
|
|
16
37
|
]);
|
|
38
|
+
const APPLICATION_DELIVERY_EVENTS = new Set(['message.received', 'message.recalled', 'group.message_created', 'group.message_recalled', 'group.changed']);
|
|
17
39
|
const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
|
|
18
40
|
const APP_MESSAGE_ENVELOPE_KEYS = [
|
|
19
41
|
'module_id', 'message_type', 'type', 'kind', 'version',
|
|
20
42
|
'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
|
|
21
43
|
'timestamp', 'created_at', 'encrypted',
|
|
22
|
-
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
|
|
44
|
+
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode', 'need_render',
|
|
23
45
|
];
|
|
24
46
|
const RECALL_PAYLOAD_KEYS = [
|
|
25
47
|
'message_ids', 'target_message_seqs',
|
|
@@ -100,9 +122,774 @@ export class MessageDeliveryEngine {
|
|
|
100
122
|
volatileInFlightMessages = new Set();
|
|
101
123
|
// 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
|
|
102
124
|
seqTrackerWriteChain = Promise.resolve();
|
|
125
|
+
peerCatalogTask = null;
|
|
126
|
+
peerCatalogDirty = false;
|
|
127
|
+
peerCatalogQueries = new Map();
|
|
128
|
+
peerContext = '';
|
|
129
|
+
peerTracker = null;
|
|
130
|
+
peerContextGeneration = 0;
|
|
131
|
+
peerBootstrap = null;
|
|
132
|
+
initializedPeers = new Set();
|
|
133
|
+
committedSeqTrackerStates = new Map();
|
|
134
|
+
seqTrackerNamespaceGenerations = new Map();
|
|
135
|
+
peerRecoveryTasks = new Map();
|
|
136
|
+
peerRecoveryNeeds = new Map();
|
|
137
|
+
peerForwardNeeds = new Map();
|
|
138
|
+
peerPushVersions = new Map();
|
|
139
|
+
peerCatalogVersion = null;
|
|
140
|
+
peerPullHints = new Map();
|
|
141
|
+
peerPullConfirmedThrough = new Map();
|
|
142
|
+
peerServerConfirmedAcks = new Map();
|
|
143
|
+
peerVolatileCursors = new Map();
|
|
144
|
+
settledDeliveries = new Map();
|
|
145
|
+
deliveryInflight = new Map();
|
|
146
|
+
applicationPage = new AsyncLocalStorage();
|
|
147
|
+
applicationClaims = new Map();
|
|
148
|
+
volatileCompletions = new Map();
|
|
149
|
+
peerPageChains = new Map();
|
|
150
|
+
automaticPullFailures = new Map();
|
|
151
|
+
terminalGroupPulls = new Map();
|
|
152
|
+
automaticPullOperations = new WeakSet();
|
|
153
|
+
automaticPullContext = new AsyncLocalStorage();
|
|
154
|
+
automaticPullAbort = new AbortController();
|
|
103
155
|
constructor(runtime) {
|
|
104
156
|
this.runtime = runtime;
|
|
105
157
|
}
|
|
158
|
+
resetAutomaticPullBackoff() {
|
|
159
|
+
this.automaticPullAbort.abort();
|
|
160
|
+
this.automaticPullAbort = new AbortController();
|
|
161
|
+
this.automaticPullFailures.clear();
|
|
162
|
+
this.terminalGroupPulls.clear();
|
|
163
|
+
}
|
|
164
|
+
groupPullNamespace(namespace) {
|
|
165
|
+
const groupId = namespace.startsWith('group_event:') ? namespace.slice('group_event:'.length)
|
|
166
|
+
: namespace.startsWith('group:') ? namespace.slice('group:'.length) : '';
|
|
167
|
+
return groupId ? `group:${normalizeGroupAid(groupId)}` : '';
|
|
168
|
+
}
|
|
169
|
+
automaticGroupPullStopped(namespace) {
|
|
170
|
+
return this.terminalGroupPulls.has(this.groupPullNamespace(namespace));
|
|
171
|
+
}
|
|
172
|
+
observeSuccessfulGroupPull(namespace) {
|
|
173
|
+
if (!this.automaticPullContext.getStore())
|
|
174
|
+
this.terminalGroupPulls.delete(this.groupPullNamespace(namespace));
|
|
175
|
+
}
|
|
176
|
+
stopAutomaticGroupPull(namespace, error) {
|
|
177
|
+
const canonical = this.groupPullNamespace(namespace);
|
|
178
|
+
if (!canonical)
|
|
179
|
+
return;
|
|
180
|
+
this.terminalGroupPulls.set(canonical, error);
|
|
181
|
+
for (const pending of [this.automaticPullFailures, this.backgroundPullNeeds, this.pendingGroupPullUpper, this.onlineUnreadHintTargets]) {
|
|
182
|
+
for (const ns of pending?.keys() ?? []) {
|
|
183
|
+
if (this.groupPullNamespace(ns) === canonical)
|
|
184
|
+
pending.delete(ns);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
automaticPullRetryAt(namespace, operation) {
|
|
189
|
+
return this.automaticPullOperations.has(operation) ? this.automaticPullFailures.get(namespace)?.retryAt ?? 0 : 0;
|
|
190
|
+
}
|
|
191
|
+
runPeerPullAttempt(namespace, operation, schedule) {
|
|
192
|
+
return this.automaticPullContext.getStore() ? this.runAutomaticPull(namespace, operation, schedule) : schedule(operation);
|
|
193
|
+
}
|
|
194
|
+
async runAutomaticPull(namespace, operation, schedule) {
|
|
195
|
+
this.ensurePeerContext();
|
|
196
|
+
if (this.automaticGroupPullStopped(namespace))
|
|
197
|
+
throw this.terminalGroupPulls.get(this.groupPullNamespace(namespace));
|
|
198
|
+
const nested = this.automaticPullContext.getStore() === namespace;
|
|
199
|
+
const generation = this.captureInlineAckGeneration();
|
|
200
|
+
const signal = this.automaticPullAbort.signal;
|
|
201
|
+
const client = this.runtime.client;
|
|
202
|
+
while ((this.automaticPullFailures.get(namespace)?.retryAt ?? 0) > Date.now()) {
|
|
203
|
+
const delay = this.automaticPullFailures.get(namespace).retryAt - Date.now();
|
|
204
|
+
await new Promise((resolve, reject) => {
|
|
205
|
+
const abort = () => {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
reject(new Error('pull invalidated'));
|
|
208
|
+
};
|
|
209
|
+
const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, delay);
|
|
210
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (signal.aborted || generation !== this.captureInlineAckGeneration() || client._closing || client.state !== ConnectionState.READY) {
|
|
214
|
+
throw new Error('pull invalidated');
|
|
215
|
+
}
|
|
216
|
+
const attempt = async () => {
|
|
217
|
+
if (this.automaticGroupPullStopped(namespace))
|
|
218
|
+
throw this.terminalGroupPulls.get(this.groupPullNamespace(namespace));
|
|
219
|
+
const previousFailure = this.automaticPullFailures.get(namespace);
|
|
220
|
+
try {
|
|
221
|
+
const result = await this.automaticPullContext.run(namespace, operation);
|
|
222
|
+
if (!nested && !signal.aborted && generation === this.captureInlineAckGeneration())
|
|
223
|
+
this.automaticPullFailures.delete(namespace);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (!signal.aborted && generation === this.captureInlineAckGeneration()
|
|
228
|
+
&& this.groupPullNamespace(namespace) && isTerminalGroupPullError(error)) {
|
|
229
|
+
this.stopAutomaticGroupPull(namespace, error);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
const currentFailure = this.automaticPullFailures.get(namespace);
|
|
233
|
+
if ((!currentFailure || currentFailure === previousFailure)
|
|
234
|
+
&& !signal.aborted && generation === this.captureInlineAckGeneration() && !client._closing && client.state === ConnectionState.READY) {
|
|
235
|
+
const delay = Math.min(30_000, (this.automaticPullFailures.get(namespace)?.delay ?? 500) * 2);
|
|
236
|
+
this.automaticPullFailures.set(namespace, { delay, retryAt: Date.now() + delay });
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
this.automaticPullOperations.add(attempt);
|
|
242
|
+
return await (schedule ? schedule(attempt) : attempt());
|
|
243
|
+
}
|
|
244
|
+
currentApplicationPage() {
|
|
245
|
+
const page = this.applicationPage.getStore();
|
|
246
|
+
return page?.collecting && page.batching ? page : undefined;
|
|
247
|
+
}
|
|
248
|
+
isApplicationPageCurrent(page) {
|
|
249
|
+
return page.context === this.ensurePeerContext() && page.generation === this.captureInlineAckGeneration()
|
|
250
|
+
&& this.isSeqNamespaceGenerationCurrent(page.namespace, page.namespaceGeneration)
|
|
251
|
+
&& (!this.isPullScopedSource(page.source) || this.isPullOperationCurrent(page.namespace));
|
|
252
|
+
}
|
|
253
|
+
ensureApplicationPageCurrent(page) {
|
|
254
|
+
if (!this.isApplicationPageCurrent(page))
|
|
255
|
+
throw new Error(this.isPullScopedSource(page.source) ? 'pull invalidated' : 'delivery invalidated');
|
|
256
|
+
}
|
|
257
|
+
snapshotApplicationHandlers() {
|
|
258
|
+
return this.runtime.client._dispatcher._snapshotApplicationHandlers?.(['message.batch', 'group.message_batch', 'group.event_batch']);
|
|
259
|
+
}
|
|
260
|
+
hasP2pApplicationSubscription() {
|
|
261
|
+
const handlers = this.runtime.client._dispatcher._snapshotApplicationHandlers?.(['message.received', 'message.recalled', 'message.batch']);
|
|
262
|
+
return !handlers || [...handlers.values()].some((selected) => selected.length > 0);
|
|
263
|
+
}
|
|
264
|
+
async runApplicationPage(namespace, source, operation, reservation, snapshot) {
|
|
265
|
+
const dispatcher = this.runtime.client._dispatcher;
|
|
266
|
+
if (typeof dispatcher._snapshotApplicationHandlers !== 'function')
|
|
267
|
+
return await operation();
|
|
268
|
+
const previous = this.applicationPage.getStore();
|
|
269
|
+
if (previous?.collecting && previous.namespace === namespace)
|
|
270
|
+
return await operation();
|
|
271
|
+
let resolve, reject;
|
|
272
|
+
const completion = new Promise((done, fail) => { resolve = done; reject = fail; });
|
|
273
|
+
void completion.catch(() => { });
|
|
274
|
+
const handlers = snapshot ?? this.snapshotApplicationHandlers(), batchEvent = namespace.startsWith('group_event:') ? 'group.event_batch'
|
|
275
|
+
: namespace.startsWith('group:') ? 'group.message_batch' : 'message.batch';
|
|
276
|
+
const page = { namespace, source, context: this.ensurePeerContext(), generation: this.captureInlineAckGeneration(),
|
|
277
|
+
namespaceGeneration: this.captureSeqNamespaceGeneration(namespace),
|
|
278
|
+
collecting: true, batching: (handlers.get(batchEvent)?.length ?? 0) > 0, handlers, reservation,
|
|
279
|
+
items: [], commits: [], afterDelivery: [], cleanup: [], claims: new Set(), bindings: new Map(), volatileSeen: new Set(), consumed: new Set(),
|
|
280
|
+
completion, resolve, reject };
|
|
281
|
+
return await this.applicationPage.run(page, async () => {
|
|
282
|
+
try {
|
|
283
|
+
const result = await operation();
|
|
284
|
+
page.collecting = false;
|
|
285
|
+
this.ensureApplicationPageCurrent(page);
|
|
286
|
+
if (page.items.length > 0) {
|
|
287
|
+
const batchHandlers = page.handlers.get(batchEvent) ?? [];
|
|
288
|
+
await dispatcher._publishAndWait(batchEvent, { namespace, items: page.items, source }, batchHandlers, reservation, () => this.isApplicationPageCurrent(page));
|
|
289
|
+
}
|
|
290
|
+
this.ensureApplicationPageCurrent(page);
|
|
291
|
+
for (const commit of page.commits)
|
|
292
|
+
commit();
|
|
293
|
+
for (const action of page.afterDelivery) {
|
|
294
|
+
await action();
|
|
295
|
+
this.ensureApplicationPageCurrent(page);
|
|
296
|
+
}
|
|
297
|
+
for (const cleanup of page.cleanup)
|
|
298
|
+
cleanup();
|
|
299
|
+
page.resolve();
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
page.reject(error);
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
page.collecting = false;
|
|
308
|
+
for (const key of page.claims)
|
|
309
|
+
if (this.applicationClaims.get(key) === page)
|
|
310
|
+
this.applicationClaims.delete(key);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
completeApplicationDelivery(operation) {
|
|
315
|
+
const page = this.currentApplicationPage();
|
|
316
|
+
if (page)
|
|
317
|
+
page.commits.push(operation);
|
|
318
|
+
else
|
|
319
|
+
operation();
|
|
320
|
+
}
|
|
321
|
+
applicationNamespace(event, payload) {
|
|
322
|
+
const value = payload && typeof payload === 'object' ? payload : {};
|
|
323
|
+
if (event === 'group.changed')
|
|
324
|
+
return `group_event:${String(value.group_id ?? value.group_aid ?? '')}`;
|
|
325
|
+
if (event.startsWith('group.'))
|
|
326
|
+
return `group:${String(value.group_id ?? value.group_aid ?? '')}`;
|
|
327
|
+
const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? '');
|
|
328
|
+
return this.runtime.client._p2pPeerPullEnabled && sender ? this.peerNamespace(sender) : `p2p:${this.runtime.client._aid ?? ''}`;
|
|
329
|
+
}
|
|
330
|
+
applicationClaimKey(event, payload, namespace, context) {
|
|
331
|
+
const value = payload && typeof payload === 'object' ? payload : {};
|
|
332
|
+
const seq = Number(event === 'group.changed' ? value.event_seq : value.seq);
|
|
333
|
+
const identity = event === 'message.recalled' ? this.messageRecallDedupKey(value)
|
|
334
|
+
: event === 'group.message_recalled' ? this.groupRecallDedupKey(String(value.group_id ?? ''), value)
|
|
335
|
+
: Number.isSafeInteger(seq) && seq > 0 ? seq : value.volatile_cursor ?? value.message_id;
|
|
336
|
+
return identity === undefined ? '' : JSON.stringify([context, event, event.startsWith('message.') ? 'p2p' : namespace,
|
|
337
|
+
value.storage === 'volatile' ? String(value.sender_aid ?? value.from_aid ?? value.from ?? '') : '', identity]);
|
|
338
|
+
}
|
|
339
|
+
async waitForApplicationOwner(owner) {
|
|
340
|
+
if (this.runtime.client._dispatcher._isApplicationHandler?.())
|
|
341
|
+
throw new Error('delivery cannot wait inside an application handler');
|
|
342
|
+
await owner.completion;
|
|
343
|
+
}
|
|
344
|
+
async collectApplicationEvent(page, event, payload, completed) {
|
|
345
|
+
this.ensureApplicationPageCurrent(page);
|
|
346
|
+
const key = this.applicationClaimKey(event, payload, page.namespace, page.context);
|
|
347
|
+
if (key) {
|
|
348
|
+
if (page.claims.has(key))
|
|
349
|
+
return;
|
|
350
|
+
const owner = this.applicationClaims.get(key);
|
|
351
|
+
if (owner) {
|
|
352
|
+
await this.waitForApplicationOwner(owner);
|
|
353
|
+
this.ensureApplicationPageCurrent(page);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.applicationClaims.set(key, page);
|
|
357
|
+
page.claims.add(key);
|
|
358
|
+
}
|
|
359
|
+
page.items.push({ event, payload });
|
|
360
|
+
page.commits.push(completed);
|
|
361
|
+
}
|
|
362
|
+
peerNamespace(senderAid) {
|
|
363
|
+
if (!senderAid.trim())
|
|
364
|
+
throw new Error('sender_aid is required');
|
|
365
|
+
return `p2p.peer.v1/${Buffer.from(senderAid, 'utf8').toString('base64url')}`;
|
|
366
|
+
}
|
|
367
|
+
async runPeerPageSerialized(namespace, operation) {
|
|
368
|
+
const previous = this.peerPageChains.get(namespace) ?? Promise.resolve();
|
|
369
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
370
|
+
this.peerPageChains.set(namespace, current);
|
|
371
|
+
try {
|
|
372
|
+
return await current;
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
if (this.peerPageChains.get(namespace) === current)
|
|
376
|
+
this.peerPageChains.delete(namespace);
|
|
377
|
+
if (this.peerCatalogDirty && this.peerPageChains.size === 0 && this.peerRecoveryTasks.size === 0)
|
|
378
|
+
this.refreshPendingPeers();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
hasPeerPageActivity(namespace) {
|
|
382
|
+
return this.peerPageChains.has(namespace);
|
|
383
|
+
}
|
|
384
|
+
peerAckChanged() {
|
|
385
|
+
this.requestBackgroundPullRecovery();
|
|
386
|
+
}
|
|
387
|
+
peerPullPriority(namespace) {
|
|
388
|
+
this.ensurePeerContext();
|
|
389
|
+
return this.peerPullHints.get(namespace) ?? Math.max(this.peerPullConfirmedThrough.get(namespace) ?? 0, this.syncState(namespace).ack);
|
|
390
|
+
}
|
|
391
|
+
consumePeerPullHint(namespace, coveredThroughSeq, nextPendingSeq) {
|
|
392
|
+
const hint = this.peerPullPriority(namespace);
|
|
393
|
+
if (coveredThroughSeq < (this.peerPullConfirmedThrough.get(namespace) ?? 0))
|
|
394
|
+
return;
|
|
395
|
+
this.peerPullConfirmedThrough.set(namespace, coveredThroughSeq);
|
|
396
|
+
if (hint !== null && hint <= coveredThroughSeq)
|
|
397
|
+
this.peerPullHints.delete(namespace);
|
|
398
|
+
if (nextPendingSeq !== undefined && nextPendingSeq > coveredThroughSeq) {
|
|
399
|
+
this.peerPullHints.set(namespace, Math.max(this.peerPullHints.get(namespace) ?? 0, nextPendingSeq));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
peerSender(namespace) {
|
|
403
|
+
return namespace.startsWith('p2p.peer.v1/') ? Buffer.from(namespace.slice(12), 'base64url').toString('utf8') : '';
|
|
404
|
+
}
|
|
405
|
+
ensurePeerContext() {
|
|
406
|
+
const client = this.runtime.client;
|
|
407
|
+
const context = JSON.stringify([client._aid ?? '', client._deviceId ?? '', slotIsolationKey(client._slotId)]);
|
|
408
|
+
if (context !== this.peerContext || client._seqTracker !== this.peerTracker) {
|
|
409
|
+
this.resetAutomaticPullBackoff();
|
|
410
|
+
this.peerContext = context;
|
|
411
|
+
this.peerTracker = client._seqTracker;
|
|
412
|
+
this.peerContextGeneration += 1;
|
|
413
|
+
this.peerBootstrap = null;
|
|
414
|
+
this.initializedPeers.clear();
|
|
415
|
+
this.committedSeqTrackerStates.clear();
|
|
416
|
+
this.seqTrackerNamespaceGenerations.clear();
|
|
417
|
+
this.peerVolatileCursors.clear();
|
|
418
|
+
this.peerRecoveryNeeds.clear();
|
|
419
|
+
this.peerForwardNeeds.clear();
|
|
420
|
+
this.peerPushVersions.clear();
|
|
421
|
+
this.peerCatalogVersion = null;
|
|
422
|
+
this.peerPullHints.clear();
|
|
423
|
+
this.peerPullConfirmedThrough.clear();
|
|
424
|
+
this.peerServerConfirmedAcks.clear();
|
|
425
|
+
this.peerRecoveryTasks.clear();
|
|
426
|
+
this.settledDeliveries.clear();
|
|
427
|
+
this.deliveryInflight.clear();
|
|
428
|
+
this.peerCatalogTask = null;
|
|
429
|
+
this.peerCatalogDirty = false;
|
|
430
|
+
this.peerCatalogQueries.clear();
|
|
431
|
+
}
|
|
432
|
+
return `${context}/${this.peerContextGeneration}`;
|
|
433
|
+
}
|
|
434
|
+
peerOwnType() {
|
|
435
|
+
const client = this.runtime.client;
|
|
436
|
+
try {
|
|
437
|
+
const current = client._currentAid;
|
|
438
|
+
if (!current?.isCertValid() || current.aid.toLowerCase() !== String(client._aid ?? '').toLowerCase())
|
|
439
|
+
return '';
|
|
440
|
+
const content = client._agentMdManager?.readContent?.(client._aid);
|
|
441
|
+
if (typeof content !== 'string' || !content)
|
|
442
|
+
return '';
|
|
443
|
+
const verified = current.verifyAgentMd(content);
|
|
444
|
+
if (!verified.ok || verified.data.status !== 'verified')
|
|
445
|
+
return '';
|
|
446
|
+
const type = parseFileFrontmatter(verified.data.payload).type;
|
|
447
|
+
return typeof type === 'string' ? type.trim() : '';
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return '';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async initializePeer(senderAid, initialAfterSeq) {
|
|
454
|
+
const client = this.runtime.client;
|
|
455
|
+
const context = this.ensurePeerContext();
|
|
456
|
+
const ns = this.peerNamespace(senderAid);
|
|
457
|
+
if (this.initializedPeers.has(ns)) {
|
|
458
|
+
if (client._seqTracker.snapshotNamespace(ns) !== null)
|
|
459
|
+
return ns;
|
|
460
|
+
this.initializedPeers.delete(ns);
|
|
461
|
+
this.committedSeqTrackerStates.delete(ns);
|
|
462
|
+
}
|
|
463
|
+
const hadState = client._seqTracker.snapshotNamespace(ns) !== null;
|
|
464
|
+
if (this.peerBootstrap === null)
|
|
465
|
+
await this.peers({}, true);
|
|
466
|
+
if (context !== this.ensurePeerContext())
|
|
467
|
+
throw new Error('peer initialization invalidated');
|
|
468
|
+
if (this.initializedPeers.has(ns))
|
|
469
|
+
return ns;
|
|
470
|
+
if (!hadState) {
|
|
471
|
+
if (initialAfterSeq === undefined) {
|
|
472
|
+
let pageToken;
|
|
473
|
+
do {
|
|
474
|
+
const catalog = await this.peers({ filter: 'all', ...(pageToken ? { page_token: pageToken } : {}) }, true);
|
|
475
|
+
if (context !== this.ensurePeerContext())
|
|
476
|
+
throw new Error('peer initialization invalidated');
|
|
477
|
+
if (this.initializedPeers.has(ns))
|
|
478
|
+
return ns;
|
|
479
|
+
pageToken = catalog.has_more === true ? String(catalog.next_page_token ?? '') : undefined;
|
|
480
|
+
if (catalog.has_more === true && !pageToken)
|
|
481
|
+
throw new Error('all peer catalog is incomplete');
|
|
482
|
+
} while (pageToken);
|
|
483
|
+
}
|
|
484
|
+
const initial = initialAfterSeq ?? this.peerBootstrap ?? 0;
|
|
485
|
+
if (!Number.isSafeInteger(initial) || initial < 0)
|
|
486
|
+
throw new Error('invalid peer initialization baseline');
|
|
487
|
+
client._seqTracker.forceContiguousSeq(ns, initial);
|
|
488
|
+
}
|
|
489
|
+
const state = { ...client._seqTracker.getSyncState(ns) };
|
|
490
|
+
await this.writeSeqTrackerState(client._aid, client._deviceId, client._slotId, { [ns]: state }, true);
|
|
491
|
+
if (context !== this.ensurePeerContext())
|
|
492
|
+
throw new Error('peer initialization invalidated');
|
|
493
|
+
this.committedSeqTrackerStates.set(ns, state);
|
|
494
|
+
this.initializedPeers.add(ns);
|
|
495
|
+
return ns;
|
|
496
|
+
}
|
|
497
|
+
peerCommittedAck(namespace) {
|
|
498
|
+
this.ensurePeerContext();
|
|
499
|
+
const committed = this.committedSeqTrackerStates.get(namespace);
|
|
500
|
+
return committed ? Math.min(committed.ack, this.syncState(namespace).ack) : 0;
|
|
501
|
+
}
|
|
502
|
+
observePeerServerAck(namespace, sequence) {
|
|
503
|
+
this.ensurePeerContext();
|
|
504
|
+
if (!namespace.startsWith('p2p.peer.v1/') || !Number.isSafeInteger(sequence) || sequence < 0)
|
|
505
|
+
return;
|
|
506
|
+
this.peerServerConfirmedAcks.set(namespace, Math.max(this.peerServerConfirmedAcks.get(namespace) ?? 0, sequence));
|
|
507
|
+
}
|
|
508
|
+
rememberCommittedSeqState(namespace) {
|
|
509
|
+
this.ensurePeerContext();
|
|
510
|
+
if (this.isReliableSeqNamespace(namespace) && !this.committedSeqTrackerStates.has(namespace)) {
|
|
511
|
+
this.committedSeqTrackerStates.set(namespace, { ...this.syncState(namespace) });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
isReliableSeqNamespace(namespace) {
|
|
515
|
+
return namespace.startsWith('p2p.peer.v1/') || namespace.startsWith('group:') || namespace.startsWith('group_event:');
|
|
516
|
+
}
|
|
517
|
+
captureSeqNamespaceGeneration(namespace) {
|
|
518
|
+
return JSON.stringify([this.ensurePeerContext(), this.seqTrackerNamespaceGenerations.get(namespace) ?? 0]);
|
|
519
|
+
}
|
|
520
|
+
isSeqNamespaceGenerationCurrent(namespace, generation) {
|
|
521
|
+
return this.captureSeqNamespaceGeneration(namespace) === generation;
|
|
522
|
+
}
|
|
523
|
+
async peers(params = {}, background = false) {
|
|
524
|
+
const context = this.ensurePeerContext();
|
|
525
|
+
const key = JSON.stringify([context, params.filter ?? 'pending', params.limit ?? null, params.page_token ?? null]);
|
|
526
|
+
const existing = this.peerCatalogQueries.get(key);
|
|
527
|
+
if (existing) {
|
|
528
|
+
existing.application ||= !background;
|
|
529
|
+
return await existing.promise;
|
|
530
|
+
}
|
|
531
|
+
const query = { application: !background, promise: Promise.resolve({}) };
|
|
532
|
+
query.promise = this.queryPeers(params, () => !query.application);
|
|
533
|
+
this.peerCatalogQueries.set(key, query);
|
|
534
|
+
try {
|
|
535
|
+
return await query.promise;
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
if (this.peerCatalogQueries.get(key) === query)
|
|
539
|
+
this.peerCatalogQueries.delete(key);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async queryPeers(params, isBackground) {
|
|
543
|
+
const client = this.runtime.client;
|
|
544
|
+
const context = this.ensurePeerContext();
|
|
545
|
+
const generation = this.captureInlineAckGeneration();
|
|
546
|
+
const aid = client._aid;
|
|
547
|
+
const deviceId = client._deviceId;
|
|
548
|
+
const slotId = client._slotId;
|
|
549
|
+
const filter = String(params.filter ?? 'pending');
|
|
550
|
+
if (filter !== 'pending' && filter !== 'all')
|
|
551
|
+
throw new Error('invalid peers filter');
|
|
552
|
+
if (filter === 'pending' && ('limit' in params || 'page_token' in params))
|
|
553
|
+
throw new Error('pending peers cannot be paginated');
|
|
554
|
+
const raw = await client._rpcPipeline.rawCall('message.peers', {
|
|
555
|
+
...params, filter, device_id: deviceId, slot_id: slotId,
|
|
556
|
+
}, { background: isBackground() });
|
|
557
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
558
|
+
throw new Error('peer catalog invalidated');
|
|
559
|
+
if (!raw || raw.cursor_scope !== 'peer' || raw.catalog_ready !== true || !Array.isArray(raw.peers))
|
|
560
|
+
throw new Error('peer catalog is not ready');
|
|
561
|
+
if (filter === 'pending' && (raw.has_more === true || raw.next_page_token))
|
|
562
|
+
throw new Error('pending peer catalog is incomplete');
|
|
563
|
+
const baseline = nonNegativeSafeSequenceHint(raw.bootstrap_floor_seq);
|
|
564
|
+
if (baseline === null)
|
|
565
|
+
throw new Error('invalid peer bootstrap_floor_seq');
|
|
566
|
+
if (this.peerBootstrap !== null && this.peerBootstrap !== baseline)
|
|
567
|
+
throw new Error('peer bootstrap changed');
|
|
568
|
+
if (this.peerBootstrap === null) {
|
|
569
|
+
const store = client._tokenStore;
|
|
570
|
+
const existing = await store.loadInstanceState?.(aid, deviceId, slotId);
|
|
571
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
572
|
+
throw new Error('peer catalog invalidated');
|
|
573
|
+
const saved = existing?.p2p_peer_bootstrap_floor_seq;
|
|
574
|
+
if (saved !== undefined && saved !== baseline)
|
|
575
|
+
throw new Error('persisted peer bootstrap changed');
|
|
576
|
+
if (typeof store.updateInstanceState !== 'function')
|
|
577
|
+
throw new Error('peer bootstrap persistence is unavailable');
|
|
578
|
+
await store.updateInstanceState(aid, deviceId, slotId, (state) => ({ ...state, p2p_peer_bootstrap_floor_seq: baseline }));
|
|
579
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
580
|
+
throw new Error('peer catalog invalidated');
|
|
581
|
+
this.peerBootstrap = baseline;
|
|
582
|
+
}
|
|
583
|
+
const rows = [];
|
|
584
|
+
for (const entry of raw.peers) {
|
|
585
|
+
if (!entry || typeof entry.sender_aid !== 'string' || !entry.sender_aid.trim())
|
|
586
|
+
throw new Error('peer catalog sender is invalid');
|
|
587
|
+
await this.initializePeer(entry.sender_aid, nonNegativeSafeSequenceHint(entry.initial_after_seq) ?? baseline);
|
|
588
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
589
|
+
throw new Error('peer catalog invalidated');
|
|
590
|
+
rows.push({ ...entry });
|
|
591
|
+
}
|
|
592
|
+
for (const row of rows) {
|
|
593
|
+
const ns = this.peerNamespace(String(row.sender_aid));
|
|
594
|
+
if (row.volatile_has_more === true || (nonNegativeSafeSequenceHint(row.head_seq) ?? 0) > Math.max(this.syncState(ns).head, client._seqTracker.getMaxSeenSeq(ns))) {
|
|
595
|
+
this.peerPushVersions.set(ns, {});
|
|
596
|
+
}
|
|
597
|
+
const head = nonNegativeSafeSequenceHint(row.head_seq);
|
|
598
|
+
if (head !== null)
|
|
599
|
+
client._seqTracker.updateMaxSeen(ns, head);
|
|
600
|
+
const hint = nonNegativeSafeSequenceHint(row.next_pending_seq);
|
|
601
|
+
const serverAck = nonNegativeSafeSequenceHint(row.server_ack_seq);
|
|
602
|
+
if (serverAck !== null)
|
|
603
|
+
this.observePeerServerAck(ns, serverAck);
|
|
604
|
+
const confirmed = Math.max(this.peerPullConfirmedThrough.get(ns) ?? 0, nonNegativeSafeSequenceHint(row.server_ack_seq) ?? nonNegativeSafeSequenceHint(row.initial_after_seq) ?? 0);
|
|
605
|
+
this.peerPullConfirmedThrough.set(ns, confirmed);
|
|
606
|
+
const current = this.peerPullHints.get(ns);
|
|
607
|
+
if (current !== undefined && current <= confirmed)
|
|
608
|
+
this.peerPullHints.delete(ns);
|
|
609
|
+
if (hint !== null && hint >= confirmed)
|
|
610
|
+
this.peerPullHints.set(ns, Math.max(this.peerPullHints.get(ns) ?? 0, hint));
|
|
611
|
+
}
|
|
612
|
+
const snapshot = {
|
|
613
|
+
...raw, filter, peers: rows, aid, device_id: deviceId, slot_id: slotId,
|
|
614
|
+
trigger: isBackground() ? 'background' : 'application', peer_count: rows.length,
|
|
615
|
+
};
|
|
616
|
+
if (!isBackground() || rows.length > 0)
|
|
617
|
+
client._dispatcher.enqueue('message.peers_snapshot', snapshot, () => context === this.ensurePeerContext() && generation === this.captureInlineAckGeneration());
|
|
618
|
+
return snapshot;
|
|
619
|
+
}
|
|
620
|
+
capturePeerTailVersion(senderAid) {
|
|
621
|
+
const context = this.ensurePeerContext();
|
|
622
|
+
return { context, push: this.peerPushVersions.get(this.peerNamespace(senderAid)), catalog: this.peerCatalogVersion };
|
|
623
|
+
}
|
|
624
|
+
isPeerTailCurrent(senderAid, version, committedAck) {
|
|
625
|
+
const namespace = this.peerNamespace(senderAid);
|
|
626
|
+
return version.context === this.ensurePeerContext() && this.peerPushVersions.get(namespace) === version.push
|
|
627
|
+
&& this.peerCatalogVersion === version.catalog && this.runtime.client._seqTracker.getMaxSeenSeq(namespace) <= committedAck
|
|
628
|
+
&& this.peerPullPriority(namespace) <= committedAck && this.syncState(namespace).head <= committedAck;
|
|
629
|
+
}
|
|
630
|
+
shouldTailPeer(namespace, head, limit, expectedForwardNeed) {
|
|
631
|
+
const state = this.syncState(namespace);
|
|
632
|
+
const knownHead = Math.max(head, state.head, this.runtime.client._seqTracker.getMaxSeenSeq(namespace), this.peerPullPriority(namespace));
|
|
633
|
+
return this.peerOwnType() === 'human' && (!this.peerForwardNeeds.has(namespace) || this.peerForwardNeeds.get(namespace) === expectedForwardNeed) && state.ack >= state.head
|
|
634
|
+
&& Number.isSafeInteger(limit) && limit > 0 && knownHead - this.peerCommittedAck(namespace) > limit;
|
|
635
|
+
}
|
|
636
|
+
canClosePeerTail(namespace, tail, pushVersion, catalogVersion, expectedNeed) {
|
|
637
|
+
const head = nonNegativeSafeSequenceHint(tail.head_seq);
|
|
638
|
+
const committedAck = this.peerCommittedAck(namespace);
|
|
639
|
+
const need = this.peerRecoveryNeeds.get(this.peerSender(namespace));
|
|
640
|
+
return tail.complete === true && tail.volatile_has_more === false && tail.pending_ack_seq === 0
|
|
641
|
+
&& head !== null && committedAck >= head && this.syncState(namespace).head <= committedAck
|
|
642
|
+
&& (this.peerServerConfirmedAcks.get(namespace) ?? 0) >= committedAck
|
|
643
|
+
&& this.runtime.client._seqTracker.getMaxSeenSeq(namespace) <= committedAck
|
|
644
|
+
&& this.peerPullPriority(namespace) <= committedAck
|
|
645
|
+
&& this.peerPushVersions.get(namespace) === pushVersion
|
|
646
|
+
&& this.peerCatalogVersion === catalogVersion
|
|
647
|
+
&& (!need || (need === expectedNeed && !need.pending && need.head <= committedAck));
|
|
648
|
+
}
|
|
649
|
+
async pullPeers(params = {}, background = false) {
|
|
650
|
+
const client = this.runtime.client;
|
|
651
|
+
const context = this.ensurePeerContext();
|
|
652
|
+
const generation = this.captureInlineAckGeneration();
|
|
653
|
+
const isCurrent = () => context === this.ensurePeerContext() && generation === this.captureInlineAckGeneration();
|
|
654
|
+
const sender = String(params.sender_aid ?? '').trim();
|
|
655
|
+
if (!sender && ('after_seq' in params || 'after_volatile_cursor' in params || 'window_mode' in params || params.force === true))
|
|
656
|
+
throw new Error('explicit peer is required for after_seq, after_volatile_cursor, window_mode or force');
|
|
657
|
+
const snapshot = sender ? null : await this.peers({}, background);
|
|
658
|
+
const rows = sender ? [{ sender_aid: sender }] : snapshot.peers;
|
|
659
|
+
const budget = { remaining: Math.max(1, Math.trunc(Number(params.max_pages ?? 100) || 100)) };
|
|
660
|
+
const results = await Promise.all(rows.map(async (row) => {
|
|
661
|
+
const peer = String(row.sender_aid);
|
|
662
|
+
const peerPullVersion = this.capturePeerTailVersion(peer);
|
|
663
|
+
const ns = await this.initializePeer(peer);
|
|
664
|
+
try {
|
|
665
|
+
if (!isCurrent())
|
|
666
|
+
throw new Error('pull invalidated');
|
|
667
|
+
let forwardNeed = this.peerForwardNeeds.get(ns);
|
|
668
|
+
if (!params.window_mode && params.force !== true && this.shouldTailPeer(ns, Number(row.head_seq ?? 0), Number(params.limit ?? _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize)) && budget.remaining > 0) {
|
|
669
|
+
forwardNeed = {};
|
|
670
|
+
this.peerForwardNeeds.set(ns, forwardNeed);
|
|
671
|
+
const pushVersion = this.peerPushVersions.get(ns);
|
|
672
|
+
const catalogVersion = this.peerCatalogVersion;
|
|
673
|
+
const autoTail = { head: Number(row.head_seq ?? 0), forwardNeed, forward: false };
|
|
674
|
+
const tail = await client._v2E2EE.pullPeerV2(peer, { ...params, window_mode: 'tail', budget, background, peerPullVersion, autoTail });
|
|
675
|
+
if (!isCurrent())
|
|
676
|
+
throw new Error('pull invalidated');
|
|
677
|
+
if (autoTail.forward) {
|
|
678
|
+
const notified = this.peerPushVersions.get(ns) !== pushVersion || this.peerCatalogVersion !== catalogVersion;
|
|
679
|
+
if (notified)
|
|
680
|
+
this.peerForwardNeeds.set(ns, {});
|
|
681
|
+
else if (tail.complete === true && this.peerForwardNeeds.get(ns) === forwardNeed)
|
|
682
|
+
this.peerForwardNeeds.delete(ns);
|
|
683
|
+
return { ...tail, complete: tail.complete === true && !notified };
|
|
684
|
+
}
|
|
685
|
+
if (this.canClosePeerTail(ns, tail, pushVersion, catalogVersion)) {
|
|
686
|
+
if (this.peerForwardNeeds.get(ns) === forwardNeed)
|
|
687
|
+
this.peerForwardNeeds.delete(ns);
|
|
688
|
+
return tail;
|
|
689
|
+
}
|
|
690
|
+
if (!budget.remaining) {
|
|
691
|
+
this.schedulePeerRecovery(peer, Number(tail.head_seq ?? 0), true);
|
|
692
|
+
return { ...tail, complete: false };
|
|
693
|
+
}
|
|
694
|
+
forwardNeed = this.peerForwardNeeds.get(ns);
|
|
695
|
+
const forwardPushVersion = this.peerPushVersions.get(ns);
|
|
696
|
+
const forwardCatalogVersion = this.peerCatalogVersion;
|
|
697
|
+
const forward = await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background });
|
|
698
|
+
if (!isCurrent())
|
|
699
|
+
throw new Error('pull invalidated');
|
|
700
|
+
const notified = this.peerPushVersions.get(ns) !== forwardPushVersion || this.peerCatalogVersion !== forwardCatalogVersion;
|
|
701
|
+
if (notified)
|
|
702
|
+
this.peerForwardNeeds.set(ns, {});
|
|
703
|
+
else if (forward.complete === true && this.peerForwardNeeds.get(ns) === forwardNeed)
|
|
704
|
+
this.peerForwardNeeds.delete(ns);
|
|
705
|
+
const messages = [...new Map([...tail.messages, ...forward.messages].map((message) => [message.seq ?? message.volatile_cursor, message])).values()];
|
|
706
|
+
return { ...forward, messages, complete: forward.complete === true && !notified };
|
|
707
|
+
}
|
|
708
|
+
const pushVersion = this.peerPushVersions.get(ns);
|
|
709
|
+
const catalogVersion = this.peerCatalogVersion;
|
|
710
|
+
const result = await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background, peerPullVersion });
|
|
711
|
+
if (!isCurrent())
|
|
712
|
+
throw new Error('pull invalidated');
|
|
713
|
+
if (params.window_mode === 'tail') {
|
|
714
|
+
const complete = this.canClosePeerTail(ns, result, pushVersion, catalogVersion);
|
|
715
|
+
if (!complete) {
|
|
716
|
+
this.peerForwardNeeds.set(ns, {});
|
|
717
|
+
this.schedulePeerRecovery(peer, Number(result.head_seq ?? 0), true);
|
|
718
|
+
}
|
|
719
|
+
else if (this.peerForwardNeeds.get(ns) === forwardNeed)
|
|
720
|
+
this.peerForwardNeeds.delete(ns);
|
|
721
|
+
return { ...result, complete };
|
|
722
|
+
}
|
|
723
|
+
if (params.force !== true && (this.peerPushVersions.get(ns) !== pushVersion || this.peerCatalogVersion !== catalogVersion)) {
|
|
724
|
+
this.peerForwardNeeds.set(ns, {});
|
|
725
|
+
return { ...result, complete: false };
|
|
726
|
+
}
|
|
727
|
+
if (params.force !== true && result.complete === true && this.peerForwardNeeds.get(ns) === forwardNeed)
|
|
728
|
+
this.peerForwardNeeds.delete(ns);
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
if (isCurrent())
|
|
733
|
+
this.schedulePeerRecovery(peer, Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
|
|
734
|
+
return { sender_aid: peer, messages: [], complete: false, ack_seq: isCurrent() ? this.syncState(ns).ack : 0, error: error instanceof Error ? error.message : String(error) };
|
|
735
|
+
}
|
|
736
|
+
}));
|
|
737
|
+
return { cursor_scope: 'peer', peers: results, messages: results.flatMap((row) => row.messages), partial: results.some((row) => !row.complete) };
|
|
738
|
+
}
|
|
739
|
+
refreshPendingPeers() {
|
|
740
|
+
this.ensurePeerContext();
|
|
741
|
+
this.peerCatalogVersion = {};
|
|
742
|
+
if (!this.hasP2pApplicationSubscription())
|
|
743
|
+
return;
|
|
744
|
+
const client = this.runtime.client;
|
|
745
|
+
if (client._sessionOptions?.background_sync === false || !client._aid || client._closing || client.state !== ConnectionState.READY)
|
|
746
|
+
return;
|
|
747
|
+
this.ensurePeerContext();
|
|
748
|
+
this.peerCatalogDirty = true;
|
|
749
|
+
if (this.peerCatalogTask)
|
|
750
|
+
return;
|
|
751
|
+
const generation = this.captureInlineAckGeneration();
|
|
752
|
+
const task = (async () => {
|
|
753
|
+
while (this.peerCatalogDirty && generation === this.captureInlineAckGeneration() && this.hasP2pApplicationSubscription()) {
|
|
754
|
+
this.peerCatalogDirty = false;
|
|
755
|
+
const result = await this.runAutomaticPull('message.peers', () => this.peers({}, true));
|
|
756
|
+
if (!this.hasP2pApplicationSubscription()) {
|
|
757
|
+
this.peerCatalogDirty = true;
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const rows = [...result.peers];
|
|
761
|
+
const hint = (row) => this.peerPullPriority(this.peerNamespace(String(row.sender_aid)));
|
|
762
|
+
rows.sort((left, right) => hint(left) - hint(right));
|
|
763
|
+
for (const row of rows)
|
|
764
|
+
this.schedulePeerRecovery(String(row.sender_aid), Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
|
|
765
|
+
}
|
|
766
|
+
})();
|
|
767
|
+
this.peerCatalogTask = task.then(() => ({}), () => ({}));
|
|
768
|
+
client._safeAsync(task.catch(() => {
|
|
769
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
770
|
+
return;
|
|
771
|
+
const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
|
|
772
|
+
this.refreshPendingPeers(); }, BACKGROUND_PULL_RETRY_DELAY_MS);
|
|
773
|
+
timer.unref?.();
|
|
774
|
+
}).finally(() => { if (generation === this.captureInlineAckGeneration())
|
|
775
|
+
this.peerCatalogTask = null; }));
|
|
776
|
+
}
|
|
777
|
+
schedulePeerRecovery(sender, head = 0, volatile = false) {
|
|
778
|
+
const client = this.runtime.client;
|
|
779
|
+
if (!sender || client._closing)
|
|
780
|
+
return;
|
|
781
|
+
this.ensurePeerContext();
|
|
782
|
+
if (Number.isSafeInteger(head) && head > 0 && !volatile) {
|
|
783
|
+
const namespace = this.peerNamespace(sender);
|
|
784
|
+
const committedAck = this.peerCommittedAck(namespace);
|
|
785
|
+
const serverAck = this.peerServerConfirmedAcks.get(namespace) ?? 0;
|
|
786
|
+
if (head <= committedAck && serverAck >= committedAck) {
|
|
787
|
+
const existing = this.peerRecoveryNeeds.get(sender);
|
|
788
|
+
if (!existing || this.peerRecoveryTasks.has(sender))
|
|
789
|
+
return;
|
|
790
|
+
if (Number.isSafeInteger(existing.head) && existing.head > 0 && !existing.volatile && existing.head <= committedAck) {
|
|
791
|
+
this.peerRecoveryNeeds.delete(sender);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const namespace = this.peerNamespace(sender);
|
|
797
|
+
this.peerPushVersions.set(namespace, {});
|
|
798
|
+
if (!this.hasP2pApplicationSubscription() || client._sessionOptions?.background_sync === false) {
|
|
799
|
+
this.peerForwardNeeds.set(namespace, {});
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const need = this.peerRecoveryNeeds.get(sender) ?? { head: 0, volatile: false, pending: false };
|
|
803
|
+
need.head = Math.max(need.head, head);
|
|
804
|
+
need.volatile ||= volatile;
|
|
805
|
+
need.pending = true;
|
|
806
|
+
this.peerRecoveryNeeds.set(sender, need);
|
|
807
|
+
if (this.peerRecoveryTasks.has(sender))
|
|
808
|
+
return;
|
|
809
|
+
const generation = this.captureInlineAckGeneration();
|
|
810
|
+
const task = (async () => {
|
|
811
|
+
await this.runAutomaticPull(`peer-init:${sender}`, () => this.initializePeer(sender));
|
|
812
|
+
if (!this.hasP2pApplicationSubscription())
|
|
813
|
+
return;
|
|
814
|
+
const namespace = this.peerNamespace(sender);
|
|
815
|
+
if (this.shouldTailPeer(namespace, need.head, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize)) {
|
|
816
|
+
const forwardNeed = {};
|
|
817
|
+
this.peerForwardNeeds.set(namespace, forwardNeed);
|
|
818
|
+
need.pending = false;
|
|
819
|
+
const pushVersion = this.peerPushVersions.get(namespace);
|
|
820
|
+
const catalogVersion = this.peerCatalogVersion;
|
|
821
|
+
const autoTail = { head: need.head, forwardNeed, forward: false };
|
|
822
|
+
const tail = await this.runAutomaticPull(namespace, async () => {
|
|
823
|
+
const beforeAck = this.peerCommittedAck(namespace);
|
|
824
|
+
const beforeVolatile = this.peerVolatilePullCursor(sender);
|
|
825
|
+
const result = await client._v2E2EE.pullPeerV2(sender, { window_mode: 'tail', background: true, retainMessages: false, budget: { remaining: 1 }, autoTail });
|
|
826
|
+
if (autoTail.forward && !result.complete && this.peerCommittedAck(namespace) <= beforeAck && this.peerVolatilePullCursor(sender) === beforeVolatile) {
|
|
827
|
+
throw new Error('peer recovery made no progress');
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
});
|
|
831
|
+
if (generation !== this.captureInlineAckGeneration() || this.peerRecoveryNeeds.get(sender) !== need)
|
|
832
|
+
return;
|
|
833
|
+
if (autoTail.forward) {
|
|
834
|
+
if (tail.complete && this.peerForwardNeeds.get(namespace) === forwardNeed
|
|
835
|
+
&& this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion)
|
|
836
|
+
this.peerForwardNeeds.delete(namespace);
|
|
837
|
+
if (tail.complete && Number(tail.ack_seq) >= need.head && !need.pending
|
|
838
|
+
&& this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion) {
|
|
839
|
+
this.peerRecoveryNeeds.delete(sender);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
await new Promise((resolve) => setTimeout(resolve, BACKGROUND_PULL_RETRY_DELAY_MS));
|
|
843
|
+
}
|
|
844
|
+
else if (this.canClosePeerTail(namespace, tail, pushVersion, catalogVersion, need)) {
|
|
845
|
+
this.peerRecoveryNeeds.delete(sender);
|
|
846
|
+
if (this.peerForwardNeeds.get(namespace) === forwardNeed)
|
|
847
|
+
this.peerForwardNeeds.delete(namespace);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
while (generation === this.captureInlineAckGeneration() && this.peerRecoveryNeeds.has(sender) && this.hasP2pApplicationSubscription()) {
|
|
852
|
+
need.pending = false;
|
|
853
|
+
const forwardNeed = this.peerForwardNeeds.get(namespace);
|
|
854
|
+
const pushVersion = this.peerPushVersions.get(namespace);
|
|
855
|
+
const catalogVersion = this.peerCatalogVersion;
|
|
856
|
+
const result = await this.runAutomaticPull(namespace, async () => {
|
|
857
|
+
const beforeAck = this.peerCommittedAck(namespace);
|
|
858
|
+
const beforeVolatile = this.peerVolatilePullCursor(sender);
|
|
859
|
+
const result = await client._v2E2EE.pullPeerV2(sender, { background: true, retainMessages: false, budget: { remaining: 100 } });
|
|
860
|
+
if (!result.complete && this.peerCommittedAck(namespace) <= beforeAck && this.peerVolatilePullCursor(sender) === beforeVolatile) {
|
|
861
|
+
throw new Error('peer recovery made no progress');
|
|
862
|
+
}
|
|
863
|
+
return result;
|
|
864
|
+
});
|
|
865
|
+
if (generation !== this.captureInlineAckGeneration() || this.peerRecoveryNeeds.get(sender) !== need)
|
|
866
|
+
return;
|
|
867
|
+
if (result.complete && this.peerForwardNeeds.get(namespace) === forwardNeed
|
|
868
|
+
&& this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion)
|
|
869
|
+
this.peerForwardNeeds.delete(namespace);
|
|
870
|
+
if (result.complete && result.ack_seq >= need.head && !need.pending
|
|
871
|
+
&& this.peerPushVersions.get(namespace) === pushVersion && this.peerCatalogVersion === catalogVersion) {
|
|
872
|
+
this.peerRecoveryNeeds.delete(sender);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
await new Promise((resolve) => setTimeout(resolve, BACKGROUND_PULL_RETRY_DELAY_MS));
|
|
876
|
+
}
|
|
877
|
+
})();
|
|
878
|
+
this.peerRecoveryTasks.set(sender, task);
|
|
879
|
+
client._safeAsync(task.catch(() => {
|
|
880
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
881
|
+
return;
|
|
882
|
+
const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
|
|
883
|
+
this.schedulePeerRecovery(sender, need.head, need.volatile); }, BACKGROUND_PULL_RETRY_DELAY_MS);
|
|
884
|
+
timer.unref?.();
|
|
885
|
+
}).finally(() => {
|
|
886
|
+
if (generation === this.captureInlineAckGeneration()) {
|
|
887
|
+
this.peerRecoveryTasks.delete(sender);
|
|
888
|
+
if (this.peerCatalogDirty && this.peerRecoveryTasks.size === 0)
|
|
889
|
+
this.refreshPendingPeers();
|
|
890
|
+
}
|
|
891
|
+
}));
|
|
892
|
+
}
|
|
106
893
|
isPullOperationCurrent(namespace = '') {
|
|
107
894
|
const pipeline = this.runtime.client._rpcPipeline;
|
|
108
895
|
return typeof pipeline?.isPullNamespaceCurrent === 'function'
|
|
@@ -170,15 +957,13 @@ export class MessageDeliveryEngine {
|
|
|
170
957
|
const client = this.runtime.client;
|
|
171
958
|
const response = result;
|
|
172
959
|
const volatileMessages = method === 'message.pull' ? this.volatileMessages(response.messages) : [];
|
|
173
|
-
if (volatileMessages.length > 0) {
|
|
174
|
-
await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
|
|
175
|
-
}
|
|
176
960
|
this.ensurePullOperationCurrent(ns);
|
|
177
961
|
this.syncStarted(ns);
|
|
178
962
|
const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
|
|
179
963
|
const coordinator = this.forwardCoordinator();
|
|
180
964
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
181
965
|
const pageInlineGeneration = this.captureInlineAckGeneration();
|
|
966
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
182
967
|
const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
|
|
183
968
|
const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
|
|
184
969
|
let pageTrackerSnapshot = null;
|
|
@@ -202,44 +987,51 @@ export class MessageDeliveryEngine {
|
|
|
202
987
|
let publishedCount = 0;
|
|
203
988
|
let committed = false;
|
|
204
989
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const appEvent = this.p2pAppEventForMessage(rawMessage);
|
|
210
|
-
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
211
|
-
publishedCount += 1;
|
|
212
|
-
}
|
|
990
|
+
await this.runApplicationPage(ns, 'pull', async () => {
|
|
991
|
+
if (volatileMessages.length > 0)
|
|
992
|
+
await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
|
|
993
|
+
for (const rawMessage of messages) {
|
|
213
994
|
this.ensurePullOperationCurrent(ns);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
995
|
+
const seq = positiveSafeSequenceHint(rawMessage.seq);
|
|
996
|
+
if (method === 'message.pull') {
|
|
997
|
+
const appEvent = this.p2pAppEventForMessage(rawMessage);
|
|
998
|
+
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
999
|
+
publishedCount += 1;
|
|
1000
|
+
}
|
|
219
1001
|
this.ensurePullOperationCurrent(ns);
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
const message = normalizeGroupMentionMode(rawMessage);
|
|
1005
|
+
if (this.recallEventFromGroupMessage(message)) {
|
|
1006
|
+
if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
|
|
1007
|
+
this.ensurePullOperationCurrent(ns);
|
|
1008
|
+
this.markPublishedSeq(ns, seq);
|
|
1009
|
+
publishedCount += 1;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
else if (this.isSelfSentGroupMessage(message)) {
|
|
220
1013
|
this.markPublishedSeq(ns, seq);
|
|
1014
|
+
}
|
|
1015
|
+
else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
|
|
1016
|
+
this.ensurePullOperationCurrent(ns);
|
|
221
1017
|
publishedCount += 1;
|
|
222
1018
|
}
|
|
223
1019
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
1020
|
+
this.ensurePullOperationCurrent(ns);
|
|
1021
|
+
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
1022
|
+
throw new Error('pull invalidated');
|
|
1023
|
+
const previousWindow = client._seqTracker.getSyncState(ns);
|
|
1024
|
+
const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
|
|
1025
|
+
const deliveryTarget = scanTarget >= previousWindow.tail - 1
|
|
1026
|
+
? Math.max(scanTarget, previousWindow.head) : scanTarget;
|
|
1027
|
+
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
|
|
1028
|
+
});
|
|
232
1029
|
this.ensurePullOperationCurrent(ns);
|
|
233
1030
|
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
234
1031
|
throw new Error('pull invalidated');
|
|
235
|
-
|
|
236
|
-
const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
|
|
237
|
-
const deliveryTarget = scanTarget >= previousWindow.tail - 1
|
|
238
|
-
? Math.max(scanTarget, previousWindow.head) : scanTarget;
|
|
239
|
-
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
|
|
240
|
-
this.ensurePullOperationCurrent(ns);
|
|
241
|
-
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
1032
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
242
1033
|
throw new Error('pull invalidated');
|
|
1034
|
+
this.rememberCommittedSeqState(ns);
|
|
243
1035
|
pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
|
|
244
1036
|
trackerMutated = true;
|
|
245
1037
|
client._seqTracker.onPullResult(ns, messages, afterSeq, Number.isSafeInteger(result.contiguous_seq)
|
|
@@ -255,11 +1047,14 @@ export class MessageDeliveryEngine {
|
|
|
255
1047
|
this.ensurePullOperationCurrent(ns);
|
|
256
1048
|
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
257
1049
|
throw new Error('pull invalidated');
|
|
1050
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
1051
|
+
throw new Error('pull invalidated');
|
|
258
1052
|
}
|
|
259
1053
|
committed = true;
|
|
260
1054
|
}
|
|
261
1055
|
catch (exc) {
|
|
262
1056
|
if (trackerMutated && pageInlineGeneration === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
|
|
1057
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
263
1058
|
&& !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
264
1059
|
(client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
|
|
265
1060
|
.call(client._seqTracker, ns, pageTrackerSnapshot);
|
|
@@ -330,23 +1125,39 @@ export class MessageDeliveryEngine {
|
|
|
330
1125
|
this.volatileCursor = '';
|
|
331
1126
|
this.volatileSeenMessages.clear();
|
|
332
1127
|
this.volatileInFlightMessages.clear();
|
|
1128
|
+
for (const completion of this.volatileCompletions.values())
|
|
1129
|
+
completion.reject(new Error('delivery invalidated'));
|
|
1130
|
+
this.volatileCompletions.clear();
|
|
333
1131
|
}
|
|
334
1132
|
volatilePullCursor() {
|
|
335
1133
|
this.ensureVolatileContext();
|
|
336
1134
|
return this.volatileCursor;
|
|
337
1135
|
}
|
|
1136
|
+
peerVolatilePullCursor(sender) {
|
|
1137
|
+
this.ensurePeerContext();
|
|
1138
|
+
return this.peerVolatileCursors.get(sender) ?? '';
|
|
1139
|
+
}
|
|
1140
|
+
setPeerVolatilePullCursor(sender, cursor) {
|
|
1141
|
+
this.ensurePeerContext();
|
|
1142
|
+
if (cursor)
|
|
1143
|
+
this.peerVolatileCursors.set(sender, cursor);
|
|
1144
|
+
else
|
|
1145
|
+
this.peerVolatileCursors.delete(sender);
|
|
1146
|
+
}
|
|
338
1147
|
resetVolatilePullCursor() {
|
|
339
1148
|
this.ensureVolatileContext();
|
|
340
1149
|
this.volatileCursor = '';
|
|
341
1150
|
}
|
|
342
1151
|
volatileSeenKey(message) {
|
|
343
1152
|
const messageId = String(message.message_id ?? '').trim();
|
|
344
|
-
const sender = String(message.from_aid ?? message.from ?? '').trim();
|
|
1153
|
+
const sender = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim();
|
|
345
1154
|
const cursor = String(message.volatile_cursor ?? '').trim();
|
|
346
|
-
return
|
|
1155
|
+
return JSON.stringify([sender, messageId, cursor]);
|
|
347
1156
|
}
|
|
348
1157
|
isVolatileSeen(message) {
|
|
349
1158
|
this.ensureVolatileContext();
|
|
1159
|
+
if (this.currentApplicationPage()?.volatileSeen.has(this.volatileSeenKey(message)))
|
|
1160
|
+
return true;
|
|
350
1161
|
const now = Date.now();
|
|
351
1162
|
for (const [key, expiresAt] of this.volatileSeenMessages) {
|
|
352
1163
|
if (expiresAt <= now)
|
|
@@ -356,6 +1167,12 @@ export class MessageDeliveryEngine {
|
|
|
356
1167
|
}
|
|
357
1168
|
markVolatileSeen(message) {
|
|
358
1169
|
this.ensureVolatileContext();
|
|
1170
|
+
const page = this.currentApplicationPage();
|
|
1171
|
+
if (page) {
|
|
1172
|
+
page.volatileSeen.add(this.volatileSeenKey(message));
|
|
1173
|
+
page.commits.push(() => this.markVolatileSeen(message));
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
359
1176
|
const now = Date.now();
|
|
360
1177
|
const expiresAt = Math.max(now + 60_000, Number(message.expires_at ?? now + 1_800_000) || now + 1_800_000);
|
|
361
1178
|
this.volatileSeenMessages.set(this.volatileSeenKey(message), expiresAt);
|
|
@@ -373,11 +1190,38 @@ export class MessageDeliveryEngine {
|
|
|
373
1190
|
if (this.volatileInFlightMessages.has(key))
|
|
374
1191
|
return 'inflight';
|
|
375
1192
|
this.volatileInFlightMessages.add(key);
|
|
1193
|
+
let resolve, reject;
|
|
1194
|
+
const promise = new Promise((done, fail) => { resolve = done; reject = fail; });
|
|
1195
|
+
void promise.catch(() => { });
|
|
1196
|
+
const page = this.currentApplicationPage();
|
|
1197
|
+
const completion = { page, promise, resolve, reject };
|
|
1198
|
+
this.volatileCompletions.set(key, completion);
|
|
1199
|
+
if (page)
|
|
1200
|
+
void page.completion.then(resolve, reject).finally(() => {
|
|
1201
|
+
if (this.volatileCompletions.get(key) === completion) {
|
|
1202
|
+
this.volatileCompletions.delete(key);
|
|
1203
|
+
this.volatileInFlightMessages.delete(key);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
376
1206
|
return 'claimed';
|
|
377
1207
|
}
|
|
1208
|
+
async waitVolatileDelivery(message) {
|
|
1209
|
+
const completion = this.volatileCompletions.get(this.volatileSeenKey(message));
|
|
1210
|
+
if (!completion || completion.page === this.currentApplicationPage())
|
|
1211
|
+
return;
|
|
1212
|
+
if (this.runtime.client._dispatcher._isApplicationHandler?.())
|
|
1213
|
+
throw new Error('delivery cannot wait inside an application handler');
|
|
1214
|
+
await completion.promise;
|
|
1215
|
+
}
|
|
378
1216
|
finishVolatileDelivery(message) {
|
|
379
1217
|
this.ensureVolatileContext();
|
|
380
|
-
this.
|
|
1218
|
+
const key = this.volatileSeenKey(message);
|
|
1219
|
+
const completion = this.volatileCompletions.get(key);
|
|
1220
|
+
if (completion?.page)
|
|
1221
|
+
return;
|
|
1222
|
+
completion?.resolve();
|
|
1223
|
+
this.volatileCompletions.delete(key);
|
|
1224
|
+
this.volatileInFlightMessages.delete(key);
|
|
381
1225
|
}
|
|
382
1226
|
publicVolatileMessage(message) {
|
|
383
1227
|
const result = this.stripInternalSenderDeviceFields(message);
|
|
@@ -385,15 +1229,27 @@ export class MessageDeliveryEngine {
|
|
|
385
1229
|
return result;
|
|
386
1230
|
}
|
|
387
1231
|
async ackVolatileMessages(messages, v2, advanceCursor) {
|
|
1232
|
+
const page = this.currentApplicationPage();
|
|
1233
|
+
if (page) {
|
|
1234
|
+
page.afterDelivery.push(() => this.ackVolatileMessages(messages, v2, advanceCursor));
|
|
1235
|
+
return undefined;
|
|
1236
|
+
}
|
|
388
1237
|
const cursors = [...new Set(messages
|
|
389
1238
|
.map((message) => String(message.volatile_cursor ?? '').trim())
|
|
390
1239
|
.filter(Boolean))];
|
|
391
1240
|
if (cursors.length === 0)
|
|
392
1241
|
return undefined;
|
|
393
|
-
const
|
|
1242
|
+
const client = this.runtime.client;
|
|
1243
|
+
const sender = client._p2pPeerPullEnabled ? String(messages[0]?.sender_aid ?? messages[0]?.from_aid ?? messages[0]?.from ?? '') : '';
|
|
1244
|
+
const result = sender
|
|
1245
|
+
? await client._v2E2EE.ackPeerV2(sender, 0, cursors)
|
|
1246
|
+
: await client._callRawV2Rpc(v2 ? 'message.v2.ack' : 'message.ack', { volatile_cursors: cursors, _rpc_background: true });
|
|
394
1247
|
if (advanceCursor) {
|
|
395
1248
|
this.ensureVolatileContext();
|
|
396
|
-
|
|
1249
|
+
if (sender)
|
|
1250
|
+
this.setPeerVolatilePullCursor(sender, cursors[cursors.length - 1] ?? '');
|
|
1251
|
+
else
|
|
1252
|
+
this.volatileCursor = cursors[cursors.length - 1] ?? '';
|
|
397
1253
|
}
|
|
398
1254
|
return result;
|
|
399
1255
|
}
|
|
@@ -406,8 +1262,12 @@ export class MessageDeliveryEngine {
|
|
|
406
1262
|
processed.push(rawMessage);
|
|
407
1263
|
continue;
|
|
408
1264
|
}
|
|
409
|
-
if (claim === 'inflight')
|
|
1265
|
+
if (claim === 'inflight') {
|
|
1266
|
+
await this.waitVolatileDelivery(message);
|
|
1267
|
+
if (this.isVolatileSeen(message))
|
|
1268
|
+
processed.push(rawMessage);
|
|
410
1269
|
continue;
|
|
1270
|
+
}
|
|
411
1271
|
try {
|
|
412
1272
|
const appEvent = this.p2pAppEventForMessage(message);
|
|
413
1273
|
await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
@@ -424,6 +1284,7 @@ export class MessageDeliveryEngine {
|
|
|
424
1284
|
return processed.map((message) => this.publicVolatileMessage(message));
|
|
425
1285
|
}
|
|
426
1286
|
resetInlineAckState() {
|
|
1287
|
+
this.resetAutomaticPullBackoff();
|
|
427
1288
|
this.syncStopped('aborted');
|
|
428
1289
|
this.deliveryChangedGeneration += 1;
|
|
429
1290
|
this.pendingDeliveryChanges = null;
|
|
@@ -445,6 +1306,16 @@ export class MessageDeliveryEngine {
|
|
|
445
1306
|
this.onlineUnreadHintTasks = null;
|
|
446
1307
|
this.realtimeTailHeads = null;
|
|
447
1308
|
this.syncSessions = null;
|
|
1309
|
+
this.peerCatalogTask = null;
|
|
1310
|
+
this.peerCatalogDirty = false;
|
|
1311
|
+
this.peerRecoveryNeeds.clear();
|
|
1312
|
+
this.peerForwardNeeds.clear();
|
|
1313
|
+
this.peerPushVersions.clear();
|
|
1314
|
+
this.peerCatalogVersion = null;
|
|
1315
|
+
this.peerPullHints.clear();
|
|
1316
|
+
this.peerPullConfirmedThrough.clear();
|
|
1317
|
+
this.peerServerConfirmedAcks.clear();
|
|
1318
|
+
this.peerRecoveryTasks.clear();
|
|
448
1319
|
}
|
|
449
1320
|
deliveryChangedNamespace(event, payload) {
|
|
450
1321
|
if (event === 'group.message_created' && isJsonObject(payload)) {
|
|
@@ -716,7 +1587,8 @@ export class MessageDeliveryEngine {
|
|
|
716
1587
|
return this.pendingGroupPullUpper?.get(ns) ?? 0;
|
|
717
1588
|
}
|
|
718
1589
|
hasAnyPendingPull() {
|
|
719
|
-
return
|
|
1590
|
+
return this.peerCatalogDirty || this.peerRecoveryNeeds.size > 0 || this.peerRecoveryTasks.size > 0 || Boolean(this.peerCatalogTask)
|
|
1591
|
+
|| [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
|
|
720
1592
|
|| this.backgroundPullNeeds.size > 0
|
|
721
1593
|
|| Boolean(this.onlineUnreadHintTasks?.size)
|
|
722
1594
|
|| Boolean(this.realtimeSyncing?.size)
|
|
@@ -733,6 +1605,8 @@ export class MessageDeliveryEngine {
|
|
|
733
1605
|
this.schedulePendingGroupPullIfNeeded(ns, 'pull-gate-idle');
|
|
734
1606
|
}
|
|
735
1607
|
recordBackgroundPullNeed(ns, throughSeq = 0, probe = false) {
|
|
1608
|
+
if (this.automaticGroupPullStopped(ns))
|
|
1609
|
+
return;
|
|
736
1610
|
const client = this.runtime.client;
|
|
737
1611
|
if (client._sessionOptions?.background_sync === false || !client._aid
|
|
738
1612
|
|| !(ns === `p2p:${client._aid}` || ns.startsWith('group:') || ns.startsWith('group_event:')))
|
|
@@ -767,6 +1641,8 @@ export class MessageDeliveryEngine {
|
|
|
767
1641
|
finishBackgroundPull(ns, generation, afterSeq, failed) {
|
|
768
1642
|
if (generation !== this.captureInlineAckGeneration())
|
|
769
1643
|
return;
|
|
1644
|
+
if (this.automaticGroupPullStopped(ns))
|
|
1645
|
+
return;
|
|
770
1646
|
const need = this.backgroundPullNeeds.get(ns);
|
|
771
1647
|
if (need) {
|
|
772
1648
|
if (failed)
|
|
@@ -779,7 +1655,7 @@ export class MessageDeliveryEngine {
|
|
|
779
1655
|
this.requestBackgroundPullRecovery();
|
|
780
1656
|
}
|
|
781
1657
|
requestBackgroundPullRecovery() {
|
|
782
|
-
if (this.backgroundPullCheckQueued || this.backgroundPullNeeds.size === 0)
|
|
1658
|
+
if (this.backgroundPullCheckQueued || (this.backgroundPullNeeds.size === 0 && !this.peerCatalogDirty && this.peerRecoveryNeeds.size === 0))
|
|
783
1659
|
return;
|
|
784
1660
|
this.backgroundPullCheckQueued = true;
|
|
785
1661
|
const generation = this.captureInlineAckGeneration();
|
|
@@ -790,8 +1666,22 @@ export class MessageDeliveryEngine {
|
|
|
790
1666
|
const client = this.runtime.client;
|
|
791
1667
|
if (client._sessionOptions?.background_sync === false || client.state !== ConnectionState.READY || client._closing)
|
|
792
1668
|
return;
|
|
1669
|
+
if (client._p2pPeerPullEnabled) {
|
|
1670
|
+
if (this.peerCatalogDirty && !this.peerCatalogTask)
|
|
1671
|
+
this.refreshPendingPeers();
|
|
1672
|
+
for (const [sender, need] of this.peerRecoveryNeeds) {
|
|
1673
|
+
if (!this.peerRecoveryTasks.has(sender))
|
|
1674
|
+
this.schedulePeerRecovery(sender, need.head, need.volatile);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
793
1677
|
let retryAt = Infinity;
|
|
794
1678
|
for (const [ns, need] of this.backgroundPullNeeds) {
|
|
1679
|
+
if (this.automaticGroupPullStopped(ns)) {
|
|
1680
|
+
this.backgroundPullNeeds.delete(ns);
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
if (ns.startsWith('p2p:') && !this.hasP2pApplicationSubscription())
|
|
1684
|
+
continue;
|
|
795
1685
|
if (this.backgroundPullTasks.has(ns) || this.realtimeSyncing?.has(ns) || this.onlineUnreadHintTasks?.has(ns))
|
|
796
1686
|
continue;
|
|
797
1687
|
const eventScope = ns.startsWith('group_event:');
|
|
@@ -845,6 +1735,8 @@ export class MessageDeliveryEngine {
|
|
|
845
1735
|
}
|
|
846
1736
|
onForegroundPullsDrained() {
|
|
847
1737
|
const client = this.runtime.client;
|
|
1738
|
+
if (client._p2pPeerPullEnabled)
|
|
1739
|
+
return;
|
|
848
1740
|
if (!client._aid || client._sessionOptions?.background_sync === false
|
|
849
1741
|
|| client.state !== ConnectionState.READY || client._closing)
|
|
850
1742
|
return;
|
|
@@ -856,6 +1748,8 @@ export class MessageDeliveryEngine {
|
|
|
856
1748
|
client._safeAsync(this.fillP2pGap());
|
|
857
1749
|
}
|
|
858
1750
|
recordPendingGroupPull(ns, seq) {
|
|
1751
|
+
if (this.automaticGroupPullStopped(ns))
|
|
1752
|
+
return;
|
|
859
1753
|
if (!ns || !Number.isSafeInteger(seq) || seq <= 0)
|
|
860
1754
|
return;
|
|
861
1755
|
const pending = this.pendingGroupPullUpper ?? new Map();
|
|
@@ -863,6 +1757,8 @@ export class MessageDeliveryEngine {
|
|
|
863
1757
|
pending.set(ns, Math.max(pending.get(ns) ?? 0, seq));
|
|
864
1758
|
}
|
|
865
1759
|
schedulePendingGroupPullIfNeeded(ns, reason) {
|
|
1760
|
+
if (this.automaticGroupPullStopped(ns))
|
|
1761
|
+
return false;
|
|
866
1762
|
const pending = this.pendingGroupPullUpper;
|
|
867
1763
|
if (!pending)
|
|
868
1764
|
return false;
|
|
@@ -898,6 +1794,12 @@ export class MessageDeliveryEngine {
|
|
|
898
1794
|
}
|
|
899
1795
|
}
|
|
900
1796
|
markPublishedSeq(ns, seq) {
|
|
1797
|
+
const page = this.currentApplicationPage();
|
|
1798
|
+
if (page) {
|
|
1799
|
+
page.consumed.add(JSON.stringify(['published', ns, seq]));
|
|
1800
|
+
page.commits.push(() => this.markPublishedSeq(ns, seq));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
901
1803
|
const client = this.runtime.client;
|
|
902
1804
|
let pushed = client._pushedSeqs.get(ns);
|
|
903
1805
|
if (!pushed) {
|
|
@@ -910,6 +1812,18 @@ export class MessageDeliveryEngine {
|
|
|
910
1812
|
client._pushedSeqs.set(ns, new Set(keep));
|
|
911
1813
|
}
|
|
912
1814
|
}
|
|
1815
|
+
hasPublishedSeq(ns, seq) {
|
|
1816
|
+
return this.runtime.client._pushedSeqs.get(ns)?.has(seq) === true
|
|
1817
|
+
|| this.currentApplicationPage()?.consumed.has(JSON.stringify(['published', ns, seq])) === true;
|
|
1818
|
+
}
|
|
1819
|
+
consumeOrderedMessage(ns, seq, queue) {
|
|
1820
|
+
this.currentApplicationPage()?.consumed.add(JSON.stringify(['ordered', ns, seq]));
|
|
1821
|
+
this.completeApplicationDelivery(() => {
|
|
1822
|
+
queue?.delete(seq);
|
|
1823
|
+
if (queue?.size === 0)
|
|
1824
|
+
this.runtime.client._pendingOrderedMsgs.delete(ns);
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
913
1827
|
enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
|
|
914
1828
|
const client = this.runtime.client;
|
|
915
1829
|
let queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -960,21 +1874,38 @@ export class MessageDeliveryEngine {
|
|
|
960
1874
|
async publishOrderedGroupChanged(payload, source = 'ordered') {
|
|
961
1875
|
const client = this.runtime.client;
|
|
962
1876
|
const generation = this.captureInlineAckGeneration();
|
|
1877
|
+
const scope = this.applicationPage.getStore();
|
|
1878
|
+
const page = scope?.collecting ? scope : undefined;
|
|
1879
|
+
const owner = this.applicationClaims.get(this.applicationClaimKey('group.changed', payload, this.applicationNamespace('group.changed', payload), this.ensurePeerContext()));
|
|
1880
|
+
if (owner) {
|
|
1881
|
+
if (owner !== this.currentApplicationPage())
|
|
1882
|
+
await this.waitForApplicationOwner(owner);
|
|
1883
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
1884
|
+
throw new Error('pull invalidated');
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
963
1887
|
const published = client._publishAppEvent('group.changed', payload, source);
|
|
964
1888
|
if (isPromiseLike(published))
|
|
965
1889
|
await published;
|
|
966
1890
|
if (generation !== this.captureInlineAckGeneration())
|
|
967
1891
|
throw new Error('pull invalidated');
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1892
|
+
this.completeApplicationDelivery(() => {
|
|
1893
|
+
if (isJsonObject(payload)) {
|
|
1894
|
+
const eventPayload = payload;
|
|
1895
|
+
client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
|
|
1896
|
+
if (eventPayload.action === 'dissolved') {
|
|
1897
|
+
const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
|
|
1898
|
+
const groupId = String(rawGroupId).trim();
|
|
1899
|
+
if (groupId) {
|
|
1900
|
+
const cleanup = () => { client._cleanupDissolvedGroup?.(groupId); };
|
|
1901
|
+
if (page)
|
|
1902
|
+
page.cleanup.push(cleanup);
|
|
1903
|
+
else
|
|
1904
|
+
cleanup();
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
976
1907
|
}
|
|
977
|
-
}
|
|
1908
|
+
});
|
|
978
1909
|
}
|
|
979
1910
|
isInstanceScopedMessageEvent(event) {
|
|
980
1911
|
return event === 'message.received'
|
|
@@ -1078,6 +2009,7 @@ export class MessageDeliveryEngine {
|
|
|
1078
2009
|
setIfPresent('timestamp', firstValue(message.timestamp, message.created_at, message.t_server));
|
|
1079
2010
|
if ('encrypted' in message)
|
|
1080
2011
|
envelope.encrypted = Boolean(message.encrypted);
|
|
2012
|
+
envelope.need_render = message.need_render !== false;
|
|
1081
2013
|
const context = this.envelopeMetadata(message.context);
|
|
1082
2014
|
if (context)
|
|
1083
2015
|
envelope.context = context;
|
|
@@ -1145,6 +2077,7 @@ export class MessageDeliveryEngine {
|
|
|
1145
2077
|
setIfPresent('version', firstValue(body.version, params.version));
|
|
1146
2078
|
setIfPresent('timestamp', firstValue(params.timestamp, resultObj.timestamp, resultObj.created_at, resultObj.t_server, Date.now()));
|
|
1147
2079
|
envelope.encrypted = Boolean(encrypted);
|
|
2080
|
+
envelope.need_render = params.need_render !== false;
|
|
1148
2081
|
const context = this.envelopeMetadata(params.context);
|
|
1149
2082
|
if (context)
|
|
1150
2083
|
envelope.context = context;
|
|
@@ -1306,12 +2239,14 @@ export class MessageDeliveryEngine {
|
|
|
1306
2239
|
throw new Error('pull invalidated');
|
|
1307
2240
|
if (this.isPullScopedSource(source))
|
|
1308
2241
|
this.ensurePullOperationCurrent(`p2p:${client._aid ?? ''}`);
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
2242
|
+
this.completeApplicationDelivery(() => {
|
|
2243
|
+
seen.set(dedupKey, Date.now());
|
|
2244
|
+
if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
|
|
2245
|
+
const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
|
|
2246
|
+
for (const [oldKey] of drop)
|
|
2247
|
+
seen.delete(oldKey);
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
1315
2250
|
client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
|
|
1316
2251
|
return true;
|
|
1317
2252
|
}
|
|
@@ -1417,12 +2352,14 @@ export class MessageDeliveryEngine {
|
|
|
1417
2352
|
throw new Error('pull invalidated');
|
|
1418
2353
|
if (this.isPullScopedSource(source))
|
|
1419
2354
|
this.ensurePullOperationCurrent(`group:${groupId}`);
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2355
|
+
this.completeApplicationDelivery(() => {
|
|
2356
|
+
seen.set(dedupKey, Date.now());
|
|
2357
|
+
if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
|
|
2358
|
+
const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
|
|
2359
|
+
for (const [oldKey] of drop)
|
|
2360
|
+
seen.delete(oldKey);
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
1426
2363
|
client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
|
|
1427
2364
|
return true;
|
|
1428
2365
|
}
|
|
@@ -1471,6 +2408,14 @@ export class MessageDeliveryEngine {
|
|
|
1471
2408
|
}
|
|
1472
2409
|
publishAppEvent(event, payload, source = 'direct', deliveryContext) {
|
|
1473
2410
|
const client = this.runtime.client;
|
|
2411
|
+
const scope = this.applicationPage.getStore();
|
|
2412
|
+
const page = scope?.collecting ? scope : undefined;
|
|
2413
|
+
if (APPLICATION_DELIVERY_EVENTS.has(event) && typeof client._dispatcher._snapshotApplicationHandlers === 'function') {
|
|
2414
|
+
const namespace = this.applicationNamespace(event, payload);
|
|
2415
|
+
if (!page || (page.namespace !== namespace && !(page.namespace.startsWith('p2p:') && event.startsWith('message.')))) {
|
|
2416
|
+
return this.runApplicationPage(namespace, source, async () => { await this.publishAppEvent(event, payload, source, deliveryContext); });
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
1474
2419
|
if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
|
|
1475
2420
|
client._maybeAppendEchoTraceReceive(payload);
|
|
1476
2421
|
}
|
|
@@ -1491,22 +2436,33 @@ export class MessageDeliveryEngine {
|
|
|
1491
2436
|
}
|
|
1492
2437
|
const generation = this.deliveryChangedGeneration;
|
|
1493
2438
|
const normalized = this.normalizePublishedMessagePayload(event, payload, source);
|
|
2439
|
+
const note = () => {
|
|
2440
|
+
if (generation === this.deliveryChangedGeneration) {
|
|
2441
|
+
this.noteDeliveryChanged(event, payload, source, deliveryContext);
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
if (page && APPLICATION_DELIVERY_EVENTS.has(event)) {
|
|
2445
|
+
if (page.batching)
|
|
2446
|
+
return this.collectApplicationEvent(page, event, normalized, note);
|
|
2447
|
+
const owner = this.applicationClaims.get(this.applicationClaimKey(event, normalized, page.namespace, page.context));
|
|
2448
|
+
if (owner)
|
|
2449
|
+
return this.waitForApplicationOwner(owner).then(() => this.ensureApplicationPageCurrent(page));
|
|
2450
|
+
}
|
|
1494
2451
|
const dispatcher = client._dispatcher;
|
|
1495
2452
|
const result = ['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
|
|
1496
2453
|
&& typeof dispatcher._publishAndWait === 'function'
|
|
1497
|
-
? dispatcher._publishAndWait(event, normalized)
|
|
2454
|
+
? dispatcher._publishAndWait(event, normalized, undefined, page?.reservation, page ? () => this.isApplicationPageCurrent(page) : undefined)
|
|
1498
2455
|
: typeof dispatcher.enqueue === 'function'
|
|
1499
2456
|
? (dispatcher.enqueue(event, normalized), undefined)
|
|
1500
2457
|
: typeof dispatcher.publishSyncAware === 'function'
|
|
1501
2458
|
? dispatcher.publishSyncAware(event, normalized)
|
|
1502
2459
|
: dispatcher.publish?.(event, normalized);
|
|
1503
|
-
const note = () => {
|
|
1504
|
-
if (generation === this.deliveryChangedGeneration) {
|
|
1505
|
-
this.noteDeliveryChanged(event, payload, source, deliveryContext);
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
2460
|
if (isPromiseLike(result))
|
|
1509
|
-
return Promise.resolve(result).then(
|
|
2461
|
+
return Promise.resolve(result).then(() => {
|
|
2462
|
+
if (page && APPLICATION_DELIVERY_EVENTS.has(event))
|
|
2463
|
+
this.ensureApplicationPageCurrent(page);
|
|
2464
|
+
note();
|
|
2465
|
+
});
|
|
1510
2466
|
note();
|
|
1511
2467
|
return result;
|
|
1512
2468
|
}
|
|
@@ -1703,6 +2659,10 @@ export class MessageDeliveryEngine {
|
|
|
1703
2659
|
}
|
|
1704
2660
|
async onRawMessageReceived(data) {
|
|
1705
2661
|
const client = this.runtime.client;
|
|
2662
|
+
if (client._p2pPeerPullEnabled) {
|
|
2663
|
+
await this.onPeerPush(data);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
1706
2666
|
const tStart = Date.now();
|
|
1707
2667
|
if (isJsonObject(data)) {
|
|
1708
2668
|
client._logMessageDebug('server-push', '_raw.message.received', 'message.received', data);
|
|
@@ -1745,6 +2705,11 @@ export class MessageDeliveryEngine {
|
|
|
1745
2705
|
if (!this.messageTargetsCurrentInstance(msg))
|
|
1746
2706
|
return;
|
|
1747
2707
|
const seq = msg.seq;
|
|
2708
|
+
if (client._p2pPeerPullEnabled) {
|
|
2709
|
+
await this.onPeerPush(msg);
|
|
2710
|
+
await this.publishMessageRecallTombstone(seq, msg);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
1748
2713
|
if (seq !== undefined && seq !== null && client._aid) {
|
|
1749
2714
|
const ns = `p2p:${client._aid}`;
|
|
1750
2715
|
const seqNum = Number(seq);
|
|
@@ -1933,20 +2898,26 @@ export class MessageDeliveryEngine {
|
|
|
1933
2898
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1934
2899
|
client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
|
|
1935
2900
|
const operation = async () => {
|
|
1936
|
-
const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
|
|
2901
|
+
const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
|
|
1937
2902
|
this.prunePushedSeqs(ns);
|
|
1938
2903
|
return messages.length;
|
|
1939
2904
|
};
|
|
1940
2905
|
if (client._rpcPipeline) {
|
|
1941
2906
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1942
|
-
await client._rpcPipeline.
|
|
2907
|
+
await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, true));
|
|
1943
2908
|
}
|
|
1944
2909
|
else {
|
|
1945
|
-
await client._withBackgroundRpc(
|
|
2910
|
+
await this.runAutomaticPull(ns, operation, (attempt) => client._withBackgroundRpc(attempt));
|
|
1946
2911
|
}
|
|
1947
2912
|
}
|
|
1948
2913
|
async fillP2pGap() {
|
|
2914
|
+
if (!this.hasP2pApplicationSubscription())
|
|
2915
|
+
return;
|
|
1949
2916
|
const client = this.runtime.client;
|
|
2917
|
+
if (client._p2pPeerPullEnabled) {
|
|
2918
|
+
this.refreshPendingPeers();
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
1950
2921
|
if (client._sessionOptions?.background_sync === false)
|
|
1951
2922
|
return;
|
|
1952
2923
|
if (!client._aid)
|
|
@@ -1966,9 +2937,9 @@ export class MessageDeliveryEngine {
|
|
|
1966
2937
|
// Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
|
|
1967
2938
|
// 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
|
|
1968
2939
|
const operation = () => client._pullV2(afterSeq, 50, { gateLocked: true });
|
|
1969
|
-
const messages = client._rpcPipeline
|
|
1970
|
-
?
|
|
1971
|
-
:
|
|
2940
|
+
const messages = await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline
|
|
2941
|
+
? client._rpcPipeline.runPullSerialized(client._rpcPipeline.pullGateKeyForCall('message.pull', { after_seq: afterSeq, limit: 50 }), attempt, true)
|
|
2942
|
+
: client._withBackgroundRpc(attempt));
|
|
1972
2943
|
filled = messages.length;
|
|
1973
2944
|
this.prunePushedSeqs(ns);
|
|
1974
2945
|
client._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
|
|
@@ -1993,6 +2964,8 @@ export class MessageDeliveryEngine {
|
|
|
1993
2964
|
if (!groupId)
|
|
1994
2965
|
return;
|
|
1995
2966
|
const ns = `group:${groupId}`;
|
|
2967
|
+
if (this.automaticGroupPullStopped(ns))
|
|
2968
|
+
return;
|
|
1996
2969
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1997
2970
|
const generation = this.captureInlineAckGeneration();
|
|
1998
2971
|
const dedupKey = `group_msg:${groupId}:${afterSeq}`;
|
|
@@ -2004,10 +2977,10 @@ export class MessageDeliveryEngine {
|
|
|
2004
2977
|
let filled = 0;
|
|
2005
2978
|
let failed = false;
|
|
2006
2979
|
try {
|
|
2007
|
-
const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
|
|
2008
|
-
const messages = client._rpcPipeline
|
|
2009
|
-
?
|
|
2010
|
-
:
|
|
2980
|
+
const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
|
|
2981
|
+
const messages = await this.runAutomaticPull(ns, operation, (attempt) => client._rpcPipeline
|
|
2982
|
+
? client._rpcPipeline.runPullNamespaceSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), attempt, true)
|
|
2983
|
+
: client._withBackgroundRpc(attempt));
|
|
2011
2984
|
filled = messages.length;
|
|
2012
2985
|
this.prunePushedSeqs(ns);
|
|
2013
2986
|
client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
|
|
@@ -2063,6 +3036,8 @@ export class MessageDeliveryEngine {
|
|
|
2063
3036
|
if (!groupId)
|
|
2064
3037
|
return;
|
|
2065
3038
|
const ns = `group_event:${groupId}`;
|
|
3039
|
+
if (this.automaticGroupPullStopped(ns))
|
|
3040
|
+
return;
|
|
2066
3041
|
const generation = this.captureInlineAckGeneration();
|
|
2067
3042
|
const requestedAfterSeq = Number(options.afterSeq ?? 0);
|
|
2068
3043
|
const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
|
|
@@ -2106,10 +3081,10 @@ export class MessageDeliveryEngine {
|
|
|
2106
3081
|
let result;
|
|
2107
3082
|
if (client._rpcPipeline) {
|
|
2108
3083
|
const pullKey = client._rpcPipeline.pullGateKeyForCall('group.pull_events', request);
|
|
2109
|
-
result = await
|
|
3084
|
+
result = await this.runAutomaticPull(ns, () => client.call('group.pull_events', request), (attempt) => client._rpcPipeline.runPullSerialized(pullKey, attempt, true));
|
|
2110
3085
|
}
|
|
2111
3086
|
else {
|
|
2112
|
-
result = await client.call('group.pull_events', request);
|
|
3087
|
+
result = await this.runAutomaticPull(ns, () => client.call('group.pull_events', request));
|
|
2113
3088
|
}
|
|
2114
3089
|
if (!isJsonObject(result))
|
|
2115
3090
|
return;
|
|
@@ -2120,6 +3095,7 @@ export class MessageDeliveryEngine {
|
|
|
2120
3095
|
if (!Array.isArray(events))
|
|
2121
3096
|
return;
|
|
2122
3097
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
3098
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
2123
3099
|
let pageTrackerSnapshot = null;
|
|
2124
3100
|
let trackerMutated = false;
|
|
2125
3101
|
const eventObjects = events.filter((evt) => isJsonObject(evt));
|
|
@@ -2135,35 +3111,37 @@ export class MessageDeliveryEngine {
|
|
|
2135
3111
|
const eventSeqs = [];
|
|
2136
3112
|
let hasDissolvedEvent = false;
|
|
2137
3113
|
let publishedEventCount = 0;
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
3114
|
+
try {
|
|
3115
|
+
await this.runApplicationPage(ns, 'pull', async () => {
|
|
3116
|
+
for (const evt of eventObjects) {
|
|
3117
|
+
const eventSeq = Number(evt.event_seq ?? 0);
|
|
3118
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0)
|
|
3119
|
+
eventSeqs.push(eventSeq);
|
|
3120
|
+
evt._from_gap_fill = true;
|
|
3121
|
+
if (evt.action === 'dissolved')
|
|
3122
|
+
hasDissolvedEvent = true;
|
|
3123
|
+
const et = String(evt.event_type ?? '');
|
|
3124
|
+
if (et !== 'group.message_created') {
|
|
3125
|
+
const cs = evt.client_signature;
|
|
3126
|
+
if (cs && isJsonObject(cs)) {
|
|
3127
|
+
if (client._shouldSkipEventSignature(evt)) {
|
|
3128
|
+
delete evt.client_signature;
|
|
3129
|
+
}
|
|
3130
|
+
else {
|
|
3131
|
+
evt._verified = await client._verifyEventSignatureAsync(evt, cs);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
|
|
3135
|
+
this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
|
|
3136
|
+
publishedEventCount += 1;
|
|
3137
|
+
}
|
|
2154
3138
|
}
|
|
3139
|
+
if (et !== 'group.message_created')
|
|
3140
|
+
filled += 1;
|
|
2155
3141
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
if (et !== 'group.message_created')
|
|
2162
|
-
filled += 1;
|
|
2163
|
-
}
|
|
2164
|
-
try {
|
|
2165
|
-
const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
|
|
2166
|
-
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, scanTarget);
|
|
3142
|
+
const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
|
|
3143
|
+
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, scanTarget);
|
|
3144
|
+
});
|
|
2167
3145
|
if (generation !== this.captureInlineAckGeneration())
|
|
2168
3146
|
throw new Error('pull invalidated');
|
|
2169
3147
|
this.ensurePullOperationCurrent(ns);
|
|
@@ -2171,6 +3149,9 @@ export class MessageDeliveryEngine {
|
|
|
2171
3149
|
dissolved = true;
|
|
2172
3150
|
break;
|
|
2173
3151
|
}
|
|
3152
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
3153
|
+
throw new Error('pull invalidated');
|
|
3154
|
+
this.rememberCommittedSeqState(ns);
|
|
2174
3155
|
pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
|
|
2175
3156
|
trackerMutated = true;
|
|
2176
3157
|
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, Number.isSafeInteger(result.contiguous_seq)
|
|
@@ -2183,10 +3164,13 @@ export class MessageDeliveryEngine {
|
|
|
2183
3164
|
this.ensurePullOperationCurrent(ns);
|
|
2184
3165
|
if (generation !== this.captureInlineAckGeneration())
|
|
2185
3166
|
throw new Error('pull invalidated');
|
|
3167
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
3168
|
+
throw new Error('pull invalidated');
|
|
2186
3169
|
}
|
|
2187
3170
|
}
|
|
2188
3171
|
catch (exc) {
|
|
2189
3172
|
if (trackerMutated && generation === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
|
|
3173
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
2190
3174
|
&& typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
2191
3175
|
(client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
|
|
2192
3176
|
.call(client._seqTracker, ns, pageTrackerSnapshot);
|
|
@@ -2269,6 +3253,8 @@ export class MessageDeliveryEngine {
|
|
|
2269
3253
|
enqueueOnlineUnreadEventHint(data) {
|
|
2270
3254
|
const client = this.runtime.client;
|
|
2271
3255
|
const groupId = String(data.group_id ?? '').trim();
|
|
3256
|
+
if (this.automaticGroupPullStopped(`group_event:${groupId}`))
|
|
3257
|
+
return;
|
|
2272
3258
|
if (!groupId || client._sessionOptions?.background_sync === false)
|
|
2273
3259
|
return;
|
|
2274
3260
|
this.recordBackgroundPullNeed(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
|
|
@@ -2598,9 +3584,9 @@ export class MessageDeliveryEngine {
|
|
|
2598
3584
|
return { messages, raw_count: messages.length };
|
|
2599
3585
|
};
|
|
2600
3586
|
if (!client._rpcPipeline)
|
|
2601
|
-
return await invoke
|
|
3587
|
+
return await this.runAutomaticPull(ns, invoke);
|
|
2602
3588
|
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
|
|
2603
|
-
return await client._rpcPipeline.runPullSerialized(key,
|
|
3589
|
+
return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullSerialized(key, attempt, background));
|
|
2604
3590
|
};
|
|
2605
3591
|
if (order.length === 1 && order[0] === 'tail') {
|
|
2606
3592
|
result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
|
|
@@ -2665,20 +3651,20 @@ export class MessageDeliveryEngine {
|
|
|
2665
3651
|
window_mode: 'tail',
|
|
2666
3652
|
after_seq: after,
|
|
2667
3653
|
limit: pageLimit,
|
|
2668
|
-
}, true);
|
|
3654
|
+
}, true, true);
|
|
2669
3655
|
tailHead = this.syncState(ns).head;
|
|
2670
3656
|
return tailResult;
|
|
2671
3657
|
}
|
|
2672
|
-
const opts = { gateLocked: true };
|
|
3658
|
+
const opts = { gateLocked: true, background };
|
|
2673
3659
|
if (request.max_pages !== undefined)
|
|
2674
3660
|
opts.maxPages = request.max_pages;
|
|
2675
3661
|
const messages = await client._pullGroupV2(groupId, after, pageLimit, opts);
|
|
2676
3662
|
return { messages, raw_count: messages.length };
|
|
2677
3663
|
};
|
|
2678
3664
|
if (!client._rpcPipeline)
|
|
2679
|
-
return await invoke
|
|
3665
|
+
return await this.runAutomaticPull(ns, invoke);
|
|
2680
3666
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
|
|
2681
|
-
return await client._rpcPipeline.
|
|
3667
|
+
return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, background));
|
|
2682
3668
|
};
|
|
2683
3669
|
if (order.length === 1 && order[0] === 'tail') {
|
|
2684
3670
|
result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
|
|
@@ -2719,9 +3705,9 @@ export class MessageDeliveryEngine {
|
|
|
2719
3705
|
return { messages, raw_count: messages.length };
|
|
2720
3706
|
};
|
|
2721
3707
|
if (!client._rpcPipeline)
|
|
2722
|
-
return await invoke
|
|
3708
|
+
return await this.runAutomaticPull(ns, invoke);
|
|
2723
3709
|
const key = client._rpcPipeline.pullGateKeyForCall('message.pull', request);
|
|
2724
|
-
return await client._rpcPipeline.runPullSerialized(key,
|
|
3710
|
+
return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullSerialized(key, attempt, background));
|
|
2725
3711
|
}
|
|
2726
3712
|
async runGroupForwardRecovery(groupId, ns, pageLimit, background = false, throughSeq = 0) {
|
|
2727
3713
|
const client = this.runtime.client;
|
|
@@ -2729,13 +3715,13 @@ export class MessageDeliveryEngine {
|
|
|
2729
3715
|
const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
|
|
2730
3716
|
const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
|
|
2731
3717
|
const invoke = async () => {
|
|
2732
|
-
const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages });
|
|
3718
|
+
const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages, background });
|
|
2733
3719
|
return { messages, raw_count: messages.length };
|
|
2734
3720
|
};
|
|
2735
3721
|
if (!client._rpcPipeline)
|
|
2736
|
-
return await invoke
|
|
3722
|
+
return await this.runAutomaticPull(ns, invoke);
|
|
2737
3723
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
2738
|
-
return await client._rpcPipeline.
|
|
3724
|
+
return await this.runAutomaticPull(ns, invoke, (attempt) => client._rpcPipeline.runPullNamespaceSerialized(key, attempt, background));
|
|
2739
3725
|
}
|
|
2740
3726
|
syncState(ns) {
|
|
2741
3727
|
const tracker = this.runtime.client._seqTracker;
|
|
@@ -3076,6 +4062,7 @@ export class MessageDeliveryEngine {
|
|
|
3076
4062
|
return false;
|
|
3077
4063
|
const client = this.runtime.client;
|
|
3078
4064
|
const tracker = client._seqTracker;
|
|
4065
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
3079
4066
|
let snapshot = null;
|
|
3080
4067
|
let trackerMutated = false;
|
|
3081
4068
|
let delivered = false;
|
|
@@ -3092,6 +4079,9 @@ export class MessageDeliveryEngine {
|
|
|
3092
4079
|
delivered = true;
|
|
3093
4080
|
if (!this.inlineGenerationIsCurrent(generation))
|
|
3094
4081
|
return false;
|
|
4082
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
4083
|
+
return false;
|
|
4084
|
+
this.rememberCommittedSeqState(ns);
|
|
3095
4085
|
snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
|
|
3096
4086
|
trackerMutated = true;
|
|
3097
4087
|
const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
|
|
@@ -3110,13 +4100,14 @@ export class MessageDeliveryEngine {
|
|
|
3110
4100
|
if (!this.inlineGenerationIsCurrent(generation))
|
|
3111
4101
|
return false;
|
|
3112
4102
|
await client._commitSeqTrackerState(ns);
|
|
3113
|
-
if (!this.inlineGenerationIsCurrent(generation))
|
|
4103
|
+
if (!this.inlineGenerationIsCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
3114
4104
|
return false;
|
|
3115
4105
|
this.markPublishedSeq(ns, seq);
|
|
3116
4106
|
return true;
|
|
3117
4107
|
}
|
|
3118
4108
|
catch (exc) {
|
|
3119
4109
|
if (trackerMutated && this.inlineGenerationIsCurrent(generation)
|
|
4110
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
3120
4111
|
&& typeof tracker.restoreNamespaceSnapshot === 'function') {
|
|
3121
4112
|
(tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
|
|
3122
4113
|
}
|
|
@@ -3131,6 +4122,8 @@ export class MessageDeliveryEngine {
|
|
|
3131
4122
|
const tracker = client._seqTracker;
|
|
3132
4123
|
if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, seq))
|
|
3133
4124
|
return false;
|
|
4125
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
4126
|
+
this.rememberCommittedSeqState(ns);
|
|
3134
4127
|
const snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
|
|
3135
4128
|
try {
|
|
3136
4129
|
const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
|
|
@@ -3145,10 +4138,11 @@ export class MessageDeliveryEngine {
|
|
|
3145
4138
|
throw new Error('inline realtime terminal cursor did not collapse after consume');
|
|
3146
4139
|
}
|
|
3147
4140
|
await client._commitSeqTrackerState(ns);
|
|
3148
|
-
return this.inlineGenerationIsCurrent(generation);
|
|
4141
|
+
return this.inlineGenerationIsCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration);
|
|
3149
4142
|
}
|
|
3150
4143
|
catch (exc) {
|
|
3151
4144
|
if (this.inlineGenerationIsCurrent(generation)
|
|
4145
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
3152
4146
|
&& typeof tracker.restoreNamespaceSnapshot === 'function') {
|
|
3153
4147
|
(tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
|
|
3154
4148
|
}
|
|
@@ -3342,8 +4336,99 @@ export class MessageDeliveryEngine {
|
|
|
3342
4336
|
}
|
|
3343
4337
|
return 0;
|
|
3344
4338
|
}
|
|
4339
|
+
async onPeerPush(data, expectedVersion) {
|
|
4340
|
+
const client = this.runtime.client;
|
|
4341
|
+
if (!isJsonObject(data) || !client._aid || !this.messageTargetsCurrentInstance(data))
|
|
4342
|
+
return;
|
|
4343
|
+
const head = data;
|
|
4344
|
+
if (String(head.kind ?? '') === 'message.online_unread_hint' || !head.sender_aid) {
|
|
4345
|
+
this.refreshPendingPeers();
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
if (head.cursor_scope !== 'peer') {
|
|
4349
|
+
this.refreshPendingPeers();
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
const sender = String(head.sender_aid);
|
|
4353
|
+
this.ensurePeerContext();
|
|
4354
|
+
this.peerPushVersions.set(this.peerNamespace(sender), {});
|
|
4355
|
+
if (!hasNonEmptyInlineMessage(head.inline_message) && !this.isVolatileMessage(head) && !this.hasP2pApplicationSubscription())
|
|
4356
|
+
return;
|
|
4357
|
+
const generation = this.captureInlineAckGeneration();
|
|
4358
|
+
let ns;
|
|
4359
|
+
try {
|
|
4360
|
+
ns = await this.runAutomaticPull(`peer-init:${sender}`, () => this.initializePeer(sender));
|
|
4361
|
+
}
|
|
4362
|
+
catch (error) {
|
|
4363
|
+
if (generation === this.captureInlineAckGeneration())
|
|
4364
|
+
this.schedulePeerRecovery(sender, positiveSafeSequenceHint(head.seq) ?? 0, this.isVolatileMessage(head));
|
|
4365
|
+
throw error;
|
|
4366
|
+
}
|
|
4367
|
+
const seq = positiveSafeSequenceHint(head.seq);
|
|
4368
|
+
if (this.isVolatileMessage(head)) {
|
|
4369
|
+
await client._v2E2EE.publishV2VolatileMessages([head], 'push', false);
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
if (!seq) {
|
|
4373
|
+
if (client._sessionOptions?.background_sync === false && this.hasP2pApplicationSubscription()) {
|
|
4374
|
+
await this.runAutomaticPull(ns, () => client._v2E2EE.pullPeerV2(sender, { limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, budget: { remaining: 1 } }));
|
|
4375
|
+
}
|
|
4376
|
+
else
|
|
4377
|
+
this.refreshPendingPeers();
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
4381
|
+
const row = isJsonObject(head.inline_message) ? head.inline_message : null;
|
|
4382
|
+
const prev = nonNegativeSafeSequenceHint(head.prev_peer_seq);
|
|
4383
|
+
if (row && prev !== null && prev < seq && prev <= this.syncState(ns).ack
|
|
4384
|
+
&& (!expectedVersion || this.inlineRowKind(row) === expectedVersion) && this.inlineHeadMatches(head, row, seq)) {
|
|
4385
|
+
const consumed = await client._tryRunInlineRealtime(ns, seq, () => this.runPeerPageSerialized(ns, async () => {
|
|
4386
|
+
if (generation !== this.captureInlineAckGeneration() || prev > this.syncState(ns).ack)
|
|
4387
|
+
return false;
|
|
4388
|
+
const decoded = await this.decodeInlineP2P(row);
|
|
4389
|
+
if (!decoded || !this.decodedInlineMatches(head, decoded, seq))
|
|
4390
|
+
return false;
|
|
4391
|
+
decoded.sender_aid = sender;
|
|
4392
|
+
decoded.need_render = typeof row.need_render === 'boolean' ? row.need_render : decoded.need_render !== false;
|
|
4393
|
+
const event = this.p2pAppEventForMessage(decoded);
|
|
4394
|
+
await this.publishPulledMessage(event.event, ns, seq, event.payload, false, 'inline_push');
|
|
4395
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
4396
|
+
throw new Error('inline invalidated');
|
|
4397
|
+
const snapshot = client._seqTracker.snapshotNamespace(ns);
|
|
4398
|
+
try {
|
|
4399
|
+
client._seqTracker.onPullResult(ns, [{ seq }], this.syncState(ns).ack, seq);
|
|
4400
|
+
await this.commitSeqTrackerState(ns);
|
|
4401
|
+
}
|
|
4402
|
+
catch (error) {
|
|
4403
|
+
client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
|
|
4404
|
+
throw error;
|
|
4405
|
+
}
|
|
4406
|
+
await client._v2E2EE.ackPeerV2(sender, this.syncState(ns).ack);
|
|
4407
|
+
return true;
|
|
4408
|
+
})).catch((error) => {
|
|
4409
|
+
if (generation === this.captureInlineAckGeneration())
|
|
4410
|
+
this.schedulePeerRecovery(sender, seq);
|
|
4411
|
+
throw error;
|
|
4412
|
+
});
|
|
4413
|
+
if (consumed?.acquired && consumed.value)
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
if (seq <= this.syncState(ns).ack || !this.hasP2pApplicationSubscription())
|
|
4417
|
+
return;
|
|
4418
|
+
if (client._sessionOptions?.background_sync !== false) {
|
|
4419
|
+
this.schedulePeerRecovery(sender, seq);
|
|
4420
|
+
}
|
|
4421
|
+
else {
|
|
4422
|
+
await this.runAutomaticPull(ns, () => client._v2E2EE.pullPeerV2(sender, { limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, budget: { remaining: 1 },
|
|
4423
|
+
...(this.shouldTailPeer(ns, seq, _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize) ? { window_mode: 'tail', autoTail: { head: seq, forward: false } } : {}) }));
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
3345
4426
|
async onV2PushNotification(data, expectedVersion, inlineGeneration) {
|
|
3346
4427
|
const client = this.runtime.client;
|
|
4428
|
+
if (client._p2pPeerPullEnabled) {
|
|
4429
|
+
await this.onPeerPush(data, expectedVersion);
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
3347
4432
|
const syncGeneration = this.captureInlineAckGeneration();
|
|
3348
4433
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
3349
4434
|
return;
|
|
@@ -3399,6 +4484,8 @@ export class MessageDeliveryEngine {
|
|
|
3399
4484
|
this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
|
|
3400
4485
|
return;
|
|
3401
4486
|
}
|
|
4487
|
+
if (!this.hasP2pApplicationSubscription())
|
|
4488
|
+
return;
|
|
3402
4489
|
this.recordBackgroundPullNeed(ns, pushSeq);
|
|
3403
4490
|
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
3404
4491
|
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
@@ -3430,6 +4517,8 @@ export class MessageDeliveryEngine {
|
|
|
3430
4517
|
if (!groupId)
|
|
3431
4518
|
return;
|
|
3432
4519
|
const ns = `group:${groupId}`;
|
|
4520
|
+
if (this.automaticGroupPullStopped(ns))
|
|
4521
|
+
return;
|
|
3433
4522
|
const generation = this.captureInlineAckGeneration();
|
|
3434
4523
|
const afterSeq = this.syncState(ns).ack;
|
|
3435
4524
|
this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq));
|
|
@@ -3503,6 +4592,8 @@ export class MessageDeliveryEngine {
|
|
|
3503
4592
|
return;
|
|
3504
4593
|
}
|
|
3505
4594
|
const ns = `group:${groupId}`;
|
|
4595
|
+
if (this.automaticGroupPullStopped(ns))
|
|
4596
|
+
return;
|
|
3506
4597
|
let inlineConsumed = false;
|
|
3507
4598
|
const pendingInlineAck = this.pendingGroupInlineAcks?.get(ns);
|
|
3508
4599
|
const inlineBlockedByPendingAck = Boolean(pendingInlineAck && pendingInlineAck.seq !== seq);
|
|
@@ -3561,6 +4652,14 @@ export class MessageDeliveryEngine {
|
|
|
3561
4652
|
const client = this.runtime.client;
|
|
3562
4653
|
if (!client._aid)
|
|
3563
4654
|
return;
|
|
4655
|
+
this.ensurePeerContext();
|
|
4656
|
+
const rememberPeers = () => {
|
|
4657
|
+
const state = client._seqTracker.exportSyncState?.() ?? {};
|
|
4658
|
+
for (const [ns, triplet] of Object.entries(state)) {
|
|
4659
|
+
if (this.isReliableSeqNamespace(ns))
|
|
4660
|
+
this.committedSeqTrackerStates.set(ns, { ...triplet });
|
|
4661
|
+
}
|
|
4662
|
+
};
|
|
3564
4663
|
try {
|
|
3565
4664
|
const loadSync = client._tokenStore.loadAllSyncStates;
|
|
3566
4665
|
if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
|
|
@@ -3568,6 +4667,7 @@ export class MessageDeliveryEngine {
|
|
|
3568
4667
|
if (state && Object.keys(state).length > 0) {
|
|
3569
4668
|
state = this.migrateSeqStateGroupIds(state);
|
|
3570
4669
|
client._seqTracker.restoreSyncState(state);
|
|
4670
|
+
rememberPeers();
|
|
3571
4671
|
return;
|
|
3572
4672
|
}
|
|
3573
4673
|
}
|
|
@@ -3577,6 +4677,7 @@ export class MessageDeliveryEngine {
|
|
|
3577
4677
|
if (state && Object.keys(state).length > 0) {
|
|
3578
4678
|
state = this.migrateSeqStateGroupIds(state);
|
|
3579
4679
|
client._seqTracker.restoreState(state);
|
|
4680
|
+
rememberPeers();
|
|
3580
4681
|
return;
|
|
3581
4682
|
}
|
|
3582
4683
|
}
|
|
@@ -3590,6 +4691,7 @@ export class MessageDeliveryEngine {
|
|
|
3590
4691
|
client._seqTracker.restoreSyncState(state);
|
|
3591
4692
|
else
|
|
3592
4693
|
client._seqTracker.restoreState(state);
|
|
4694
|
+
rememberPeers();
|
|
3593
4695
|
}
|
|
3594
4696
|
}
|
|
3595
4697
|
}
|
|
@@ -3610,6 +4712,10 @@ export class MessageDeliveryEngine {
|
|
|
3610
4712
|
}
|
|
3611
4713
|
enqueueOnlineUnreadP2pHint(data) {
|
|
3612
4714
|
const client = this.runtime.client;
|
|
4715
|
+
if (client._p2pPeerPullEnabled) {
|
|
4716
|
+
this.refreshPendingPeers();
|
|
4717
|
+
return;
|
|
4718
|
+
}
|
|
3613
4719
|
if (!client._aid || !client._v2Session)
|
|
3614
4720
|
return;
|
|
3615
4721
|
const ns = `p2p:${client._aid}`;
|
|
@@ -3632,6 +4738,16 @@ export class MessageDeliveryEngine {
|
|
|
3632
4738
|
const state = typeof client._seqTracker.exportSyncState === 'function'
|
|
3633
4739
|
? client._seqTracker.exportSyncState()
|
|
3634
4740
|
: Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
|
|
4741
|
+
this.ensurePeerContext();
|
|
4742
|
+
for (const ns of Object.keys(state)) {
|
|
4743
|
+
if (!this.isReliableSeqNamespace(ns))
|
|
4744
|
+
continue;
|
|
4745
|
+
const committed = this.committedSeqTrackerStates.get(ns);
|
|
4746
|
+
if (committed)
|
|
4747
|
+
state[ns] = { ...committed };
|
|
4748
|
+
else if (ns.startsWith('p2p.peer.v1/'))
|
|
4749
|
+
delete state[ns];
|
|
4750
|
+
}
|
|
3635
4751
|
const pending = this.seqTrackerPending();
|
|
3636
4752
|
if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
|
|
3637
4753
|
const liveNamespaces = new Set(Object.keys(state));
|
|
@@ -3659,9 +4775,12 @@ export class MessageDeliveryEngine {
|
|
|
3659
4775
|
if (forceSeq !== undefined) {
|
|
3660
4776
|
client._seqTracker.forceContiguousSeq(ns, seq);
|
|
3661
4777
|
}
|
|
3662
|
-
const state =
|
|
3663
|
-
|
|
3664
|
-
|
|
4778
|
+
const state = this.committedSeqTrackerStates.get(ns) ?? (ns.startsWith('p2p.peer.v1/') ? undefined
|
|
4779
|
+
: typeof client._seqTracker.getSyncState === 'function'
|
|
4780
|
+
? client._seqTracker.getSyncState(ns)
|
|
4781
|
+
: { ack: seq, tail: seq, head: seq });
|
|
4782
|
+
if (!state)
|
|
4783
|
+
return;
|
|
3665
4784
|
this.mergeSeqTrackerPending(context, { [ns]: state });
|
|
3666
4785
|
this.scheduleSeqTrackerFlush();
|
|
3667
4786
|
}
|
|
@@ -3676,12 +4795,16 @@ export class MessageDeliveryEngine {
|
|
|
3676
4795
|
if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
|
|
3677
4796
|
return;
|
|
3678
4797
|
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
4798
|
+
const tracker = client._seqTracker;
|
|
4799
|
+
const namespaceGeneration = this.seqTrackerNamespaceGenerations.get(ns) ?? 0;
|
|
3679
4800
|
const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
|
|
3680
4801
|
try {
|
|
3681
4802
|
await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
|
|
3682
4803
|
}
|
|
3683
4804
|
catch (exc) {
|
|
3684
|
-
this.
|
|
4805
|
+
if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration) {
|
|
4806
|
+
this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
|
|
4807
|
+
}
|
|
3685
4808
|
throw exc;
|
|
3686
4809
|
}
|
|
3687
4810
|
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
@@ -3788,7 +4911,37 @@ export class MessageDeliveryEngine {
|
|
|
3788
4911
|
}
|
|
3789
4912
|
}
|
|
3790
4913
|
async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
3791
|
-
|
|
4914
|
+
const context = JSON.stringify([aid, deviceId, slotId]);
|
|
4915
|
+
const tracker = this.runtime.client._seqTracker;
|
|
4916
|
+
const namespaceGenerations = new Map(Object.keys(state).map((ns) => [ns, this.seqTrackerNamespaceGenerations.get(ns) ?? 0]));
|
|
4917
|
+
await this.enqueueSeqTrackerWrite(async () => {
|
|
4918
|
+
const current = context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker;
|
|
4919
|
+
if (throwOnError && !current)
|
|
4920
|
+
throw new Error('seq tracker commit invalidated');
|
|
4921
|
+
if (current)
|
|
4922
|
+
this.ensurePeerContext();
|
|
4923
|
+
const writing = { ...state };
|
|
4924
|
+
if (!throwOnError && current) {
|
|
4925
|
+
for (const ns of Object.keys(writing)) {
|
|
4926
|
+
if (!this.isReliableSeqNamespace(ns))
|
|
4927
|
+
continue;
|
|
4928
|
+
const committed = this.committedSeqTrackerStates.get(ns);
|
|
4929
|
+
if (committed)
|
|
4930
|
+
writing[ns] = { ...committed };
|
|
4931
|
+
else if (ns.startsWith('p2p.peer.v1/'))
|
|
4932
|
+
delete writing[ns];
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
await this.writeSeqTrackerStateNow(aid, deviceId, slotId, writing, throwOnError);
|
|
4936
|
+
if (throwOnError && context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker) {
|
|
4937
|
+
for (const [ns, triplet] of Object.entries(writing)) {
|
|
4938
|
+
if (this.isReliableSeqNamespace(ns)
|
|
4939
|
+
&& (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGenerations.get(ns)) {
|
|
4940
|
+
this.committedSeqTrackerStates.set(ns, { ...triplet });
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
});
|
|
3792
4945
|
}
|
|
3793
4946
|
enqueueSeqTrackerWrite(operation) {
|
|
3794
4947
|
const run = this.seqTrackerWriteChain.then(operation, operation);
|
|
@@ -3841,10 +4994,17 @@ export class MessageDeliveryEngine {
|
|
|
3841
4994
|
const client = this.runtime.client;
|
|
3842
4995
|
if (!client._aid || !ns)
|
|
3843
4996
|
return;
|
|
4997
|
+
this.ensurePeerContext();
|
|
3844
4998
|
const aid = client._aid;
|
|
3845
4999
|
const deviceId = client._deviceId;
|
|
3846
5000
|
const slotId = client._slotId;
|
|
3847
5001
|
const seq = client._seqTracker.getContiguousSeq(ns);
|
|
5002
|
+
if (seq <= 0) {
|
|
5003
|
+
this.seqTrackerNamespaceGenerations.set(ns, (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) + 1);
|
|
5004
|
+
this.committedSeqTrackerStates.delete(ns);
|
|
5005
|
+
this.initializedPeers.delete(ns);
|
|
5006
|
+
this.peerServerConfirmedAcks.delete(ns);
|
|
5007
|
+
}
|
|
3848
5008
|
this.dropSeqTrackerPending(ns);
|
|
3849
5009
|
const saveSeq = client._tokenStore.saveSeq;
|
|
3850
5010
|
if (seq > 0 && typeof saveSeq === 'function') {
|
|
@@ -3902,7 +5062,9 @@ export class MessageDeliveryEngine {
|
|
|
3902
5062
|
if (next < 0)
|
|
3903
5063
|
next = 0;
|
|
3904
5064
|
if (ns) {
|
|
3905
|
-
|
|
5065
|
+
this.ensurePeerContext();
|
|
5066
|
+
const localAck = client._seqTracker.getContiguousSeq(ns);
|
|
5067
|
+
const committedAck = Math.min(localAck, this.committedSeqTrackerStates.get(ns)?.ack ?? localAck);
|
|
3906
5068
|
if (next > committedAck) {
|
|
3907
5069
|
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
|
|
3908
5070
|
next = committedAck;
|
|
@@ -3959,6 +5121,8 @@ export class MessageDeliveryEngine {
|
|
|
3959
5121
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
3960
5122
|
if (!queue || queue.size === 0)
|
|
3961
5123
|
return;
|
|
5124
|
+
const page = this.currentApplicationPage();
|
|
5125
|
+
let deliveredThrough = client._seqTracker.getContiguousSeq(ns);
|
|
3962
5126
|
let delivered = false;
|
|
3963
5127
|
const rootTrigger = this.deliveryChangedTrigger(source);
|
|
3964
5128
|
const drainContexts = new Map();
|
|
@@ -3966,16 +5130,17 @@ export class MessageDeliveryEngine {
|
|
|
3966
5130
|
while (true) {
|
|
3967
5131
|
if (pullScoped)
|
|
3968
5132
|
this.ensurePullOperationCurrent(ns);
|
|
3969
|
-
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
5133
|
+
const contig = page ? Math.max(deliveredThrough, client._seqTracker.getContiguousSeq(ns)) : client._seqTracker.getContiguousSeq(ns);
|
|
3970
5134
|
const ready = [...queue.keys()]
|
|
3971
|
-
.filter((seq) =>
|
|
5135
|
+
.filter((seq) => !page?.consumed.has(JSON.stringify(['ordered', ns, seq]))
|
|
5136
|
+
&& seq <= Math.max(contig, throughSeq ?? contig) && (beforeSeq === undefined || seq < beforeSeq))
|
|
3972
5137
|
.sort((a, b) => a - b);
|
|
3973
5138
|
let seq = ready[0];
|
|
3974
5139
|
if (seq === undefined) {
|
|
3975
5140
|
const nextSeq = Math.max(contig, throughSeq ?? contig) + 1;
|
|
3976
5141
|
if (beforeSeq !== undefined && nextSeq >= beforeSeq)
|
|
3977
5142
|
break;
|
|
3978
|
-
if (!queue.has(nextSeq))
|
|
5143
|
+
if (!queue.has(nextSeq) || page?.consumed.has(JSON.stringify(['ordered', ns, nextSeq])))
|
|
3979
5144
|
break;
|
|
3980
5145
|
seq = nextSeq;
|
|
3981
5146
|
}
|
|
@@ -3984,10 +5149,11 @@ export class MessageDeliveryEngine {
|
|
|
3984
5149
|
const item = queue.get(seq);
|
|
3985
5150
|
if (!item)
|
|
3986
5151
|
continue;
|
|
3987
|
-
if (
|
|
3988
|
-
|
|
5152
|
+
if (this.hasPublishedSeq(ns, seq)) {
|
|
5153
|
+
this.consumeOrderedMessage(ns, seq, queue);
|
|
3989
5154
|
client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
|
|
3990
|
-
client._markOrderedSeqDelivered(ns, seq);
|
|
5155
|
+
this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
|
|
5156
|
+
deliveredThrough = Math.max(deliveredThrough, seq);
|
|
3991
5157
|
continue;
|
|
3992
5158
|
}
|
|
3993
5159
|
const itemSource = item.source ?? source;
|
|
@@ -4007,12 +5173,13 @@ export class MessageDeliveryEngine {
|
|
|
4007
5173
|
throw new Error('pull invalidated');
|
|
4008
5174
|
if (pullScoped)
|
|
4009
5175
|
this.ensurePullOperationCurrent(ns);
|
|
4010
|
-
|
|
5176
|
+
this.consumeOrderedMessage(ns, seq, queue);
|
|
4011
5177
|
if (item.event === 'group.changed' && isJsonObject(item.payload)
|
|
4012
5178
|
&& item.payload.action === 'dissolved')
|
|
4013
5179
|
break;
|
|
4014
5180
|
this.markPublishedSeq(ns, seq);
|
|
4015
|
-
client._markOrderedSeqDelivered(ns, seq);
|
|
5181
|
+
this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
|
|
5182
|
+
deliveredThrough = Math.max(deliveredThrough, seq);
|
|
4016
5183
|
if (throughSeq !== undefined)
|
|
4017
5184
|
throughSeq = Math.max(throughSeq, seq);
|
|
4018
5185
|
delivered = true;
|
|
@@ -4122,6 +5289,87 @@ export class MessageDeliveryEngine {
|
|
|
4122
5289
|
return true;
|
|
4123
5290
|
}
|
|
4124
5291
|
async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull') {
|
|
5292
|
+
const client = this.runtime.client;
|
|
5293
|
+
const seqNum = Number(seq);
|
|
5294
|
+
if (!client._p2pPeerPullEnabled || event !== 'message.received' || source === 'history'
|
|
5295
|
+
|| !Number.isSafeInteger(seqNum) || seqNum <= 0 || !payload || typeof payload !== 'object'
|
|
5296
|
+
|| payload.storage === 'volatile') {
|
|
5297
|
+
return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
|
|
5298
|
+
}
|
|
5299
|
+
const context = this.ensurePeerContext();
|
|
5300
|
+
const generation = this.captureInlineAckGeneration();
|
|
5301
|
+
const ensureCurrent = () => {
|
|
5302
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
5303
|
+
throw new Error('delivery invalidated');
|
|
5304
|
+
if (this.isPullScopedSource(source))
|
|
5305
|
+
this.ensurePullOperationCurrent(ns);
|
|
5306
|
+
};
|
|
5307
|
+
const value = payload;
|
|
5308
|
+
const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? this.peerSender(ns));
|
|
5309
|
+
if (!sender)
|
|
5310
|
+
return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
|
|
5311
|
+
const messageId = String(value.message_id ?? '');
|
|
5312
|
+
const key = JSON.stringify([context, event, seqNum]);
|
|
5313
|
+
const page = this.currentApplicationPage();
|
|
5314
|
+
const bindingMatches = (receipt) => receipt.sender_aid === sender
|
|
5315
|
+
&& (!receipt.message_id || !messageId || receipt.message_id === messageId);
|
|
5316
|
+
const local = page?.bindings.get(key);
|
|
5317
|
+
if (local) {
|
|
5318
|
+
if (!bindingMatches(local))
|
|
5319
|
+
throw new Error('P2P delivery receipt binding conflict');
|
|
5320
|
+
if (messageId && !local.message_id)
|
|
5321
|
+
local.message_id = messageId;
|
|
5322
|
+
return false;
|
|
5323
|
+
}
|
|
5324
|
+
const pending = this.deliveryInflight.get(key);
|
|
5325
|
+
if (pending) {
|
|
5326
|
+
if (client._dispatcher._isApplicationHandler?.())
|
|
5327
|
+
throw new Error('delivery cannot wait inside an application handler');
|
|
5328
|
+
await pending;
|
|
5329
|
+
ensureCurrent();
|
|
5330
|
+
const settled = this.settledDeliveries.get(key);
|
|
5331
|
+
if (settled && !bindingMatches(settled))
|
|
5332
|
+
throw new Error('P2P delivery receipt binding conflict');
|
|
5333
|
+
if (!messageId || settled?.message_id)
|
|
5334
|
+
return false;
|
|
5335
|
+
}
|
|
5336
|
+
const operation = Promise.resolve().then(async () => {
|
|
5337
|
+
ensureCurrent();
|
|
5338
|
+
let settled = this.settledDeliveries.get(key);
|
|
5339
|
+
if (settled && !bindingMatches(settled))
|
|
5340
|
+
throw new Error('P2P delivery receipt binding conflict');
|
|
5341
|
+
if (settled && messageId && !settled.message_id) {
|
|
5342
|
+
settled = { ...settled, message_id: messageId };
|
|
5343
|
+
}
|
|
5344
|
+
let published = false;
|
|
5345
|
+
if (!settled) {
|
|
5346
|
+
if (!this.isSelfSentP2PMessage(payload)) {
|
|
5347
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
|
|
5348
|
+
published = true;
|
|
5349
|
+
}
|
|
5350
|
+
ensureCurrent();
|
|
5351
|
+
settled = { sender_aid: sender, ...(messageId ? { message_id: messageId } : {}) };
|
|
5352
|
+
}
|
|
5353
|
+
ensureCurrent();
|
|
5354
|
+
const binding = settled;
|
|
5355
|
+
page?.bindings.set(key, binding);
|
|
5356
|
+
this.completeApplicationDelivery(() => {
|
|
5357
|
+
this.settledDeliveries.set(key, binding);
|
|
5358
|
+
while (this.settledDeliveries.size > PUSHED_SEQS_LIMIT) {
|
|
5359
|
+
const first = this.settledDeliveries.keys().next().value;
|
|
5360
|
+
this.settledDeliveries.delete(first);
|
|
5361
|
+
}
|
|
5362
|
+
});
|
|
5363
|
+
this.markPublishedSeq(ns, seqNum);
|
|
5364
|
+
return published;
|
|
5365
|
+
});
|
|
5366
|
+
const completion = page ? operation.then(async (result) => { await page.completion; return result; }) : operation;
|
|
5367
|
+
this.deliveryInflight.set(key, completion);
|
|
5368
|
+
void completion.finally(() => { if (this.deliveryInflight.get(key) === completion)
|
|
5369
|
+
this.deliveryInflight.delete(key); }).catch(() => { });
|
|
5370
|
+
return await operation;
|
|
5371
|
+
}
|
|
5372
|
+
async publishPulledMessageInternal(event, ns, seq, payload, persist = true, source = 'pull') {
|
|
4125
5373
|
const client = this.runtime.client;
|
|
4126
5374
|
const generation = this.captureInlineAckGeneration();
|
|
4127
5375
|
const pullScoped = this.isPullScopedSource(source);
|
|
@@ -4144,9 +5392,9 @@ export class MessageDeliveryEngine {
|
|
|
4144
5392
|
return true;
|
|
4145
5393
|
}
|
|
4146
5394
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
4147
|
-
if (
|
|
5395
|
+
if (this.hasPublishedSeq(ns, seqNum)) {
|
|
4148
5396
|
client._clientLog.debug(`publish pulled skipped duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
4149
|
-
|
|
5397
|
+
this.consumeOrderedMessage(ns, seqNum, queue);
|
|
4150
5398
|
if (queue && queue.size === 0)
|
|
4151
5399
|
client._pendingOrderedMsgs.delete(ns);
|
|
4152
5400
|
return false;
|
|
@@ -4165,7 +5413,7 @@ export class MessageDeliveryEngine {
|
|
|
4165
5413
|
throw new Error('pull invalidated');
|
|
4166
5414
|
if (pullScoped)
|
|
4167
5415
|
this.ensurePullOperationCurrent(ns);
|
|
4168
|
-
|
|
5416
|
+
this.consumeOrderedMessage(ns, seqNum, queue);
|
|
4169
5417
|
if (queue && queue.size === 0)
|
|
4170
5418
|
client._pendingOrderedMsgs.delete(ns);
|
|
4171
5419
|
this.markPublishedSeq(ns, seqNum);
|
|
@@ -4182,7 +5430,7 @@ export class MessageDeliveryEngine {
|
|
|
4182
5430
|
throw new Error('pull invalidated');
|
|
4183
5431
|
if (pullScoped)
|
|
4184
5432
|
this.ensurePullOperationCurrent(ns);
|
|
4185
|
-
|
|
5433
|
+
this.consumeOrderedMessage(ns, seqNum, queue);
|
|
4186
5434
|
if (queue && queue.size === 0)
|
|
4187
5435
|
client._pendingOrderedMsgs.delete(ns);
|
|
4188
5436
|
this.markPublishedSeq(ns, seqNum);
|