@agentunion/fastaun 0.5.19 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +13 -1
- package/dist/agent-md-schema.js +63 -33
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +30 -3
- package/dist/agent-md.js +293 -152
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +42 -0
- package/dist/aid-store.js +224 -44
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +18 -13
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +78 -2
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +44 -1
- package/dist/cert-utils.js +157 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +68 -1
- package/dist/client/delivery.js +1196 -135
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +5 -1
- package/dist/client/rpc-pipeline.js +240 -111
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +12 -2
- package/dist/client/v2-e2ee.js +842 -379
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +26 -2
- package/dist/client.js +420 -75
- 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 +23 -4
- package/dist/events.js +171 -15
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +43 -1
- package/dist/facades.js +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +7 -2
- package/dist/keystore/aid-db.js +194 -43
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +15 -1
- package/dist/keystore/local-token-store.d.ts +10 -2
- package/dist/keystore/local-token-store.js +81 -16
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.js +829 -22
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +40 -9
- package/dist/ownership-v21.js +174 -79
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +22 -0
- package/dist/ownership-workflow.js +861 -66
- 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/tools/cross-sdk-agent.d.ts +67 -3
- package/dist/tools/cross-sdk-agent.js +575 -42
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.js +108 -44
- 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,4 +1,6 @@
|
|
|
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';
|
|
3
5
|
import { ConnectionState, isJsonObject } from '../types.js';
|
|
4
6
|
import { validateGroupAIDFormat } from '../validators.js';
|
|
@@ -9,16 +11,18 @@ const MESSAGE_RECALL_SEEN_LIMIT = 10_000;
|
|
|
9
11
|
const GROUP_RECALL_SEEN_LIMIT = 10_000;
|
|
10
12
|
const VOLATILE_SEEN_LIMIT = 10_000;
|
|
11
13
|
const SEQ_TRACKER_PERSIST_FLUSH_DELAY_MS = 200;
|
|
14
|
+
const BACKGROUND_PULL_RETRY_DELAY_MS = 250;
|
|
12
15
|
const DELIVERY_CHANGED_MESSAGE_EVENTS = new Set([
|
|
13
16
|
'message.received',
|
|
14
17
|
'group.message_created',
|
|
15
18
|
]);
|
|
19
|
+
const APPLICATION_DELIVERY_EVENTS = new Set(['message.received', 'message.recalled', 'group.message_created', 'group.message_recalled', 'group.changed']);
|
|
16
20
|
const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
|
|
17
21
|
const APP_MESSAGE_ENVELOPE_KEYS = [
|
|
18
22
|
'module_id', 'message_type', 'type', 'kind', 'version',
|
|
19
23
|
'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
|
|
20
24
|
'timestamp', 'created_at', 'encrypted',
|
|
21
|
-
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
|
|
25
|
+
'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode', 'need_render',
|
|
22
26
|
];
|
|
23
27
|
const RECALL_PAYLOAD_KEYS = [
|
|
24
28
|
'message_ids', 'target_message_seqs',
|
|
@@ -80,6 +84,11 @@ export class MessageDeliveryEngine {
|
|
|
80
84
|
pendingGroupPullUpper = null;
|
|
81
85
|
onlineUnreadHintTargets = null;
|
|
82
86
|
onlineUnreadHintTasks = null;
|
|
87
|
+
backgroundPullNeeds = new Map();
|
|
88
|
+
backgroundPullTasks = new Set();
|
|
89
|
+
backgroundPullCheckQueued = false;
|
|
90
|
+
backgroundPullTimer = null;
|
|
91
|
+
backgroundPullTimerAt = 0;
|
|
83
92
|
pendingP2PInlineAcks = null;
|
|
84
93
|
pendingGroupInlineAcks = null;
|
|
85
94
|
pendingGroupInlineAckNamespaces = null;
|
|
@@ -94,9 +103,488 @@ export class MessageDeliveryEngine {
|
|
|
94
103
|
volatileInFlightMessages = new Set();
|
|
95
104
|
// 同一实例的 tracker 写入必须串行,避免旧的延迟 flush 覆盖新的 Forward 提交。
|
|
96
105
|
seqTrackerWriteChain = Promise.resolve();
|
|
106
|
+
peerCatalogTask = null;
|
|
107
|
+
peerCatalogDirty = false;
|
|
108
|
+
peerCatalogQueries = new Map();
|
|
109
|
+
peerContext = '';
|
|
110
|
+
peerTracker = null;
|
|
111
|
+
peerContextGeneration = 0;
|
|
112
|
+
peerBootstrap = null;
|
|
113
|
+
initializedPeers = new Set();
|
|
114
|
+
committedSeqTrackerStates = new Map();
|
|
115
|
+
seqTrackerNamespaceGenerations = new Map();
|
|
116
|
+
peerRecoveryTasks = new Map();
|
|
117
|
+
peerRecoveryNeeds = new Map();
|
|
118
|
+
peerPullHints = new Map();
|
|
119
|
+
peerPullConfirmedThrough = new Map();
|
|
120
|
+
peerVolatileCursors = new Map();
|
|
121
|
+
settledDeliveries = new Map();
|
|
122
|
+
deliveryInflight = new Map();
|
|
123
|
+
applicationPage = new AsyncLocalStorage();
|
|
124
|
+
applicationClaims = new Map();
|
|
125
|
+
volatileCompletions = new Map();
|
|
126
|
+
peerPageChains = new Map();
|
|
97
127
|
constructor(runtime) {
|
|
98
128
|
this.runtime = runtime;
|
|
99
129
|
}
|
|
130
|
+
currentApplicationPage() {
|
|
131
|
+
const page = this.applicationPage.getStore();
|
|
132
|
+
return page?.collecting && page.batching ? page : undefined;
|
|
133
|
+
}
|
|
134
|
+
isApplicationPageCurrent(page) {
|
|
135
|
+
return page.context === this.ensurePeerContext() && page.generation === this.captureInlineAckGeneration()
|
|
136
|
+
&& this.isSeqNamespaceGenerationCurrent(page.namespace, page.namespaceGeneration)
|
|
137
|
+
&& (!this.isPullScopedSource(page.source) || this.isPullOperationCurrent(page.namespace));
|
|
138
|
+
}
|
|
139
|
+
ensureApplicationPageCurrent(page) {
|
|
140
|
+
if (!this.isApplicationPageCurrent(page))
|
|
141
|
+
throw new Error(this.isPullScopedSource(page.source) ? 'pull invalidated' : 'delivery invalidated');
|
|
142
|
+
}
|
|
143
|
+
snapshotApplicationHandlers() {
|
|
144
|
+
return this.runtime.client._dispatcher._snapshotApplicationHandlers?.(['message.batch', 'group.message_batch', 'group.event_batch']);
|
|
145
|
+
}
|
|
146
|
+
async runApplicationPage(namespace, source, operation, reservation, snapshot) {
|
|
147
|
+
const dispatcher = this.runtime.client._dispatcher;
|
|
148
|
+
if (typeof dispatcher._snapshotApplicationHandlers !== 'function')
|
|
149
|
+
return await operation();
|
|
150
|
+
const previous = this.applicationPage.getStore();
|
|
151
|
+
if (previous?.collecting && previous.namespace === namespace)
|
|
152
|
+
return await operation();
|
|
153
|
+
let resolve, reject;
|
|
154
|
+
const completion = new Promise((done, fail) => { resolve = done; reject = fail; });
|
|
155
|
+
void completion.catch(() => { });
|
|
156
|
+
const handlers = snapshot ?? this.snapshotApplicationHandlers(), batchEvent = namespace.startsWith('group_event:') ? 'group.event_batch'
|
|
157
|
+
: namespace.startsWith('group:') ? 'group.message_batch' : 'message.batch';
|
|
158
|
+
const page = { namespace, source, context: this.ensurePeerContext(), generation: this.captureInlineAckGeneration(),
|
|
159
|
+
namespaceGeneration: this.captureSeqNamespaceGeneration(namespace),
|
|
160
|
+
collecting: true, batching: (handlers.get(batchEvent)?.length ?? 0) > 0, handlers, reservation,
|
|
161
|
+
items: [], commits: [], afterDelivery: [], cleanup: [], claims: new Set(), bindings: new Map(), volatileSeen: new Set(), consumed: new Set(),
|
|
162
|
+
completion, resolve, reject };
|
|
163
|
+
return await this.applicationPage.run(page, async () => {
|
|
164
|
+
try {
|
|
165
|
+
const result = await operation();
|
|
166
|
+
page.collecting = false;
|
|
167
|
+
this.ensureApplicationPageCurrent(page);
|
|
168
|
+
if (page.items.length > 0) {
|
|
169
|
+
const batchHandlers = page.handlers.get(batchEvent) ?? [];
|
|
170
|
+
await dispatcher._publishAndWait(batchEvent, { namespace, items: page.items, source }, batchHandlers, reservation, () => this.isApplicationPageCurrent(page));
|
|
171
|
+
}
|
|
172
|
+
this.ensureApplicationPageCurrent(page);
|
|
173
|
+
for (const commit of page.commits)
|
|
174
|
+
commit();
|
|
175
|
+
for (const action of page.afterDelivery) {
|
|
176
|
+
await action();
|
|
177
|
+
this.ensureApplicationPageCurrent(page);
|
|
178
|
+
}
|
|
179
|
+
for (const cleanup of page.cleanup)
|
|
180
|
+
cleanup();
|
|
181
|
+
page.resolve();
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
page.reject(error);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
page.collecting = false;
|
|
190
|
+
for (const key of page.claims)
|
|
191
|
+
if (this.applicationClaims.get(key) === page)
|
|
192
|
+
this.applicationClaims.delete(key);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
completeApplicationDelivery(operation) {
|
|
197
|
+
const page = this.currentApplicationPage();
|
|
198
|
+
if (page)
|
|
199
|
+
page.commits.push(operation);
|
|
200
|
+
else
|
|
201
|
+
operation();
|
|
202
|
+
}
|
|
203
|
+
applicationNamespace(event, payload) {
|
|
204
|
+
const value = payload && typeof payload === 'object' ? payload : {};
|
|
205
|
+
if (event === 'group.changed')
|
|
206
|
+
return `group_event:${String(value.group_id ?? value.group_aid ?? '')}`;
|
|
207
|
+
if (event.startsWith('group.'))
|
|
208
|
+
return `group:${String(value.group_id ?? value.group_aid ?? '')}`;
|
|
209
|
+
const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? '');
|
|
210
|
+
return this.runtime.client._p2pPeerPullEnabled && sender ? this.peerNamespace(sender) : `p2p:${this.runtime.client._aid ?? ''}`;
|
|
211
|
+
}
|
|
212
|
+
applicationClaimKey(event, payload, namespace, context) {
|
|
213
|
+
const value = payload && typeof payload === 'object' ? payload : {};
|
|
214
|
+
const seq = Number(event === 'group.changed' ? value.event_seq : value.seq);
|
|
215
|
+
const identity = event === 'message.recalled' ? this.messageRecallDedupKey(value)
|
|
216
|
+
: event === 'group.message_recalled' ? this.groupRecallDedupKey(String(value.group_id ?? ''), value)
|
|
217
|
+
: Number.isSafeInteger(seq) && seq > 0 ? seq : value.volatile_cursor ?? value.message_id;
|
|
218
|
+
return identity === undefined ? '' : JSON.stringify([context, event, event.startsWith('message.') ? 'p2p' : namespace,
|
|
219
|
+
value.storage === 'volatile' ? String(value.sender_aid ?? value.from_aid ?? value.from ?? '') : '', identity]);
|
|
220
|
+
}
|
|
221
|
+
async waitForApplicationOwner(owner) {
|
|
222
|
+
if (this.runtime.client._dispatcher._isApplicationHandler?.())
|
|
223
|
+
throw new Error('delivery cannot wait inside an application handler');
|
|
224
|
+
await owner.completion;
|
|
225
|
+
}
|
|
226
|
+
async collectApplicationEvent(page, event, payload, completed) {
|
|
227
|
+
this.ensureApplicationPageCurrent(page);
|
|
228
|
+
const key = this.applicationClaimKey(event, payload, page.namespace, page.context);
|
|
229
|
+
if (key) {
|
|
230
|
+
if (page.claims.has(key))
|
|
231
|
+
return;
|
|
232
|
+
const owner = this.applicationClaims.get(key);
|
|
233
|
+
if (owner) {
|
|
234
|
+
await this.waitForApplicationOwner(owner);
|
|
235
|
+
this.ensureApplicationPageCurrent(page);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.applicationClaims.set(key, page);
|
|
239
|
+
page.claims.add(key);
|
|
240
|
+
}
|
|
241
|
+
page.items.push({ event, payload });
|
|
242
|
+
page.commits.push(completed);
|
|
243
|
+
}
|
|
244
|
+
peerNamespace(senderAid) {
|
|
245
|
+
if (!senderAid.trim())
|
|
246
|
+
throw new Error('sender_aid is required');
|
|
247
|
+
return `p2p.peer.v1/${Buffer.from(senderAid, 'utf8').toString('base64url')}`;
|
|
248
|
+
}
|
|
249
|
+
async runPeerPageSerialized(namespace, operation) {
|
|
250
|
+
const previous = this.peerPageChains.get(namespace) ?? Promise.resolve();
|
|
251
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
252
|
+
this.peerPageChains.set(namespace, current);
|
|
253
|
+
try {
|
|
254
|
+
return await current;
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
if (this.peerPageChains.get(namespace) === current)
|
|
258
|
+
this.peerPageChains.delete(namespace);
|
|
259
|
+
if (this.peerCatalogDirty && this.peerPageChains.size === 0 && this.peerRecoveryTasks.size === 0)
|
|
260
|
+
this.refreshPendingPeers();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
hasPeerPageActivity(namespace) {
|
|
264
|
+
return this.peerPageChains.has(namespace);
|
|
265
|
+
}
|
|
266
|
+
peerAckChanged() {
|
|
267
|
+
this.requestBackgroundPullRecovery();
|
|
268
|
+
}
|
|
269
|
+
peerPullPriority(namespace) {
|
|
270
|
+
this.ensurePeerContext();
|
|
271
|
+
return this.peerPullHints.get(namespace) ?? Math.max(this.peerPullConfirmedThrough.get(namespace) ?? 0, this.syncState(namespace).ack);
|
|
272
|
+
}
|
|
273
|
+
consumePeerPullHint(namespace, coveredThroughSeq, nextPendingSeq) {
|
|
274
|
+
const hint = this.peerPullPriority(namespace);
|
|
275
|
+
if (coveredThroughSeq < (this.peerPullConfirmedThrough.get(namespace) ?? 0))
|
|
276
|
+
return;
|
|
277
|
+
this.peerPullConfirmedThrough.set(namespace, coveredThroughSeq);
|
|
278
|
+
if (hint !== null && hint <= coveredThroughSeq)
|
|
279
|
+
this.peerPullHints.delete(namespace);
|
|
280
|
+
if (nextPendingSeq !== undefined && nextPendingSeq > coveredThroughSeq) {
|
|
281
|
+
this.peerPullHints.set(namespace, Math.max(this.peerPullHints.get(namespace) ?? 0, nextPendingSeq));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
peerSender(namespace) {
|
|
285
|
+
return namespace.startsWith('p2p.peer.v1/') ? Buffer.from(namespace.slice(12), 'base64url').toString('utf8') : '';
|
|
286
|
+
}
|
|
287
|
+
ensurePeerContext() {
|
|
288
|
+
const client = this.runtime.client;
|
|
289
|
+
const context = JSON.stringify([client._aid ?? '', client._deviceId ?? '', slotIsolationKey(client._slotId)]);
|
|
290
|
+
if (context !== this.peerContext || client._seqTracker !== this.peerTracker) {
|
|
291
|
+
this.peerContext = context;
|
|
292
|
+
this.peerTracker = client._seqTracker;
|
|
293
|
+
this.peerContextGeneration += 1;
|
|
294
|
+
this.peerBootstrap = null;
|
|
295
|
+
this.initializedPeers.clear();
|
|
296
|
+
this.committedSeqTrackerStates.clear();
|
|
297
|
+
this.seqTrackerNamespaceGenerations.clear();
|
|
298
|
+
this.peerVolatileCursors.clear();
|
|
299
|
+
this.peerRecoveryNeeds.clear();
|
|
300
|
+
this.peerPullHints.clear();
|
|
301
|
+
this.peerPullConfirmedThrough.clear();
|
|
302
|
+
this.peerRecoveryTasks.clear();
|
|
303
|
+
this.settledDeliveries.clear();
|
|
304
|
+
this.deliveryInflight.clear();
|
|
305
|
+
this.peerCatalogTask = null;
|
|
306
|
+
this.peerCatalogDirty = false;
|
|
307
|
+
this.peerCatalogQueries.clear();
|
|
308
|
+
}
|
|
309
|
+
return `${context}/${this.peerContextGeneration}`;
|
|
310
|
+
}
|
|
311
|
+
peerOwnType() {
|
|
312
|
+
const client = this.runtime.client;
|
|
313
|
+
try {
|
|
314
|
+
const current = client._currentAid;
|
|
315
|
+
if (!current?.isCertValid() || current.aid.toLowerCase() !== String(client._aid ?? '').toLowerCase())
|
|
316
|
+
return '';
|
|
317
|
+
const content = client._agentMdManager?.readContent?.(client._aid);
|
|
318
|
+
if (typeof content !== 'string' || !content)
|
|
319
|
+
return '';
|
|
320
|
+
const verified = current.verifyAgentMd(content);
|
|
321
|
+
if (!verified.ok || verified.data.status !== 'verified')
|
|
322
|
+
return '';
|
|
323
|
+
const type = parseFileFrontmatter(verified.data.payload).type;
|
|
324
|
+
return typeof type === 'string' ? type.trim() : '';
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return '';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async initializePeer(senderAid, initialAfterSeq) {
|
|
331
|
+
const client = this.runtime.client;
|
|
332
|
+
const context = this.ensurePeerContext();
|
|
333
|
+
const ns = this.peerNamespace(senderAid);
|
|
334
|
+
if (this.initializedPeers.has(ns)) {
|
|
335
|
+
if (client._seqTracker.snapshotNamespace(ns) !== null)
|
|
336
|
+
return ns;
|
|
337
|
+
this.initializedPeers.delete(ns);
|
|
338
|
+
this.committedSeqTrackerStates.delete(ns);
|
|
339
|
+
}
|
|
340
|
+
const hadState = client._seqTracker.snapshotNamespace(ns) !== null;
|
|
341
|
+
if (this.peerBootstrap === null)
|
|
342
|
+
await this.peers({}, true);
|
|
343
|
+
if (context !== this.ensurePeerContext())
|
|
344
|
+
throw new Error('peer initialization invalidated');
|
|
345
|
+
if (this.initializedPeers.has(ns))
|
|
346
|
+
return ns;
|
|
347
|
+
if (!hadState) {
|
|
348
|
+
if (initialAfterSeq === undefined) {
|
|
349
|
+
let pageToken;
|
|
350
|
+
do {
|
|
351
|
+
const catalog = await this.peers({ filter: 'all', ...(pageToken ? { page_token: pageToken } : {}) }, true);
|
|
352
|
+
if (context !== this.ensurePeerContext())
|
|
353
|
+
throw new Error('peer initialization invalidated');
|
|
354
|
+
if (this.initializedPeers.has(ns))
|
|
355
|
+
return ns;
|
|
356
|
+
pageToken = catalog.has_more === true ? String(catalog.next_page_token ?? '') : undefined;
|
|
357
|
+
if (catalog.has_more === true && !pageToken)
|
|
358
|
+
throw new Error('all peer catalog is incomplete');
|
|
359
|
+
} while (pageToken);
|
|
360
|
+
}
|
|
361
|
+
const initial = initialAfterSeq ?? this.peerBootstrap ?? 0;
|
|
362
|
+
if (!Number.isSafeInteger(initial) || initial < 0)
|
|
363
|
+
throw new Error('invalid peer initialization baseline');
|
|
364
|
+
client._seqTracker.forceContiguousSeq(ns, initial);
|
|
365
|
+
}
|
|
366
|
+
const state = { ...client._seqTracker.getSyncState(ns) };
|
|
367
|
+
await this.writeSeqTrackerState(client._aid, client._deviceId, client._slotId, { [ns]: state }, true);
|
|
368
|
+
if (context !== this.ensurePeerContext())
|
|
369
|
+
throw new Error('peer initialization invalidated');
|
|
370
|
+
this.committedSeqTrackerStates.set(ns, state);
|
|
371
|
+
this.initializedPeers.add(ns);
|
|
372
|
+
return ns;
|
|
373
|
+
}
|
|
374
|
+
peerCommittedAck(namespace) {
|
|
375
|
+
this.ensurePeerContext();
|
|
376
|
+
return Math.min(this.committedSeqTrackerStates.get(namespace)?.ack ?? 0, this.syncState(namespace).ack);
|
|
377
|
+
}
|
|
378
|
+
rememberCommittedSeqState(namespace) {
|
|
379
|
+
this.ensurePeerContext();
|
|
380
|
+
if (this.isReliableSeqNamespace(namespace) && !this.committedSeqTrackerStates.has(namespace)) {
|
|
381
|
+
this.committedSeqTrackerStates.set(namespace, { ...this.syncState(namespace) });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
isReliableSeqNamespace(namespace) {
|
|
385
|
+
return namespace.startsWith('p2p.peer.v1/') || namespace.startsWith('group:') || namespace.startsWith('group_event:');
|
|
386
|
+
}
|
|
387
|
+
captureSeqNamespaceGeneration(namespace) {
|
|
388
|
+
return JSON.stringify([this.ensurePeerContext(), this.seqTrackerNamespaceGenerations.get(namespace) ?? 0]);
|
|
389
|
+
}
|
|
390
|
+
isSeqNamespaceGenerationCurrent(namespace, generation) {
|
|
391
|
+
return this.captureSeqNamespaceGeneration(namespace) === generation;
|
|
392
|
+
}
|
|
393
|
+
async peers(params = {}, background = false) {
|
|
394
|
+
const context = this.ensurePeerContext();
|
|
395
|
+
const key = JSON.stringify([context, params.filter ?? 'pending', params.limit ?? null, params.page_token ?? null]);
|
|
396
|
+
const existing = this.peerCatalogQueries.get(key);
|
|
397
|
+
if (existing) {
|
|
398
|
+
existing.application ||= !background;
|
|
399
|
+
return await existing.promise;
|
|
400
|
+
}
|
|
401
|
+
const query = { application: !background, promise: Promise.resolve({}) };
|
|
402
|
+
query.promise = this.queryPeers(params, () => !query.application);
|
|
403
|
+
this.peerCatalogQueries.set(key, query);
|
|
404
|
+
try {
|
|
405
|
+
return await query.promise;
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
if (this.peerCatalogQueries.get(key) === query)
|
|
409
|
+
this.peerCatalogQueries.delete(key);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async queryPeers(params, isBackground) {
|
|
413
|
+
const client = this.runtime.client;
|
|
414
|
+
const context = this.ensurePeerContext();
|
|
415
|
+
const generation = this.captureInlineAckGeneration();
|
|
416
|
+
const aid = client._aid;
|
|
417
|
+
const deviceId = client._deviceId;
|
|
418
|
+
const slotId = client._slotId;
|
|
419
|
+
const filter = String(params.filter ?? 'pending');
|
|
420
|
+
if (filter !== 'pending' && filter !== 'all')
|
|
421
|
+
throw new Error('invalid peers filter');
|
|
422
|
+
if (filter === 'pending' && ('limit' in params || 'page_token' in params))
|
|
423
|
+
throw new Error('pending peers cannot be paginated');
|
|
424
|
+
const raw = await client._rpcPipeline.rawCall('message.peers', {
|
|
425
|
+
...params, filter, device_id: deviceId, slot_id: slotId,
|
|
426
|
+
}, { background: isBackground() });
|
|
427
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
428
|
+
throw new Error('peer catalog invalidated');
|
|
429
|
+
if (!raw || raw.cursor_scope !== 'peer' || raw.catalog_ready !== true || !Array.isArray(raw.peers))
|
|
430
|
+
throw new Error('peer catalog is not ready');
|
|
431
|
+
if (filter === 'pending' && (raw.has_more === true || raw.next_page_token))
|
|
432
|
+
throw new Error('pending peer catalog is incomplete');
|
|
433
|
+
const baseline = nonNegativeSafeSequenceHint(raw.bootstrap_floor_seq);
|
|
434
|
+
if (baseline === null)
|
|
435
|
+
throw new Error('invalid peer bootstrap_floor_seq');
|
|
436
|
+
if (this.peerBootstrap !== null && this.peerBootstrap !== baseline)
|
|
437
|
+
throw new Error('peer bootstrap changed');
|
|
438
|
+
if (this.peerBootstrap === null) {
|
|
439
|
+
const store = client._tokenStore;
|
|
440
|
+
const existing = await store.loadInstanceState?.(aid, deviceId, slotId);
|
|
441
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
442
|
+
throw new Error('peer catalog invalidated');
|
|
443
|
+
const saved = existing?.p2p_peer_bootstrap_floor_seq;
|
|
444
|
+
if (saved !== undefined && saved !== baseline)
|
|
445
|
+
throw new Error('persisted peer bootstrap changed');
|
|
446
|
+
if (typeof store.updateInstanceState !== 'function')
|
|
447
|
+
throw new Error('peer bootstrap persistence is unavailable');
|
|
448
|
+
await store.updateInstanceState(aid, deviceId, slotId, (state) => ({ ...state, p2p_peer_bootstrap_floor_seq: baseline }));
|
|
449
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
450
|
+
throw new Error('peer catalog invalidated');
|
|
451
|
+
this.peerBootstrap = baseline;
|
|
452
|
+
}
|
|
453
|
+
const rows = [];
|
|
454
|
+
for (const entry of raw.peers) {
|
|
455
|
+
if (!entry || typeof entry.sender_aid !== 'string' || !entry.sender_aid.trim())
|
|
456
|
+
throw new Error('peer catalog sender is invalid');
|
|
457
|
+
await this.initializePeer(entry.sender_aid, nonNegativeSafeSequenceHint(entry.initial_after_seq) ?? baseline);
|
|
458
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
459
|
+
throw new Error('peer catalog invalidated');
|
|
460
|
+
rows.push({ ...entry });
|
|
461
|
+
}
|
|
462
|
+
for (const row of rows) {
|
|
463
|
+
const ns = this.peerNamespace(String(row.sender_aid));
|
|
464
|
+
const hint = nonNegativeSafeSequenceHint(row.next_pending_seq);
|
|
465
|
+
const confirmed = Math.max(this.peerPullConfirmedThrough.get(ns) ?? 0, nonNegativeSafeSequenceHint(row.server_ack_seq) ?? nonNegativeSafeSequenceHint(row.initial_after_seq) ?? 0);
|
|
466
|
+
this.peerPullConfirmedThrough.set(ns, confirmed);
|
|
467
|
+
const current = this.peerPullHints.get(ns);
|
|
468
|
+
if (current !== undefined && current <= confirmed)
|
|
469
|
+
this.peerPullHints.delete(ns);
|
|
470
|
+
if (hint !== null && hint >= confirmed)
|
|
471
|
+
this.peerPullHints.set(ns, Math.max(this.peerPullHints.get(ns) ?? 0, hint));
|
|
472
|
+
}
|
|
473
|
+
const snapshot = {
|
|
474
|
+
...raw, filter, peers: rows, aid, device_id: deviceId, slot_id: slotId,
|
|
475
|
+
trigger: isBackground() ? 'background' : 'application', peer_count: rows.length,
|
|
476
|
+
};
|
|
477
|
+
if (!isBackground() || rows.length > 0)
|
|
478
|
+
client._dispatcher.enqueue('message.peers_snapshot', snapshot, () => context === this.ensurePeerContext() && generation === this.captureInlineAckGeneration());
|
|
479
|
+
return snapshot;
|
|
480
|
+
}
|
|
481
|
+
async pullPeers(params = {}, background = false) {
|
|
482
|
+
const client = this.runtime.client;
|
|
483
|
+
const sender = String(params.sender_aid ?? '').trim();
|
|
484
|
+
if (!sender && ('after_seq' in params || 'after_volatile_cursor' in params || 'window_mode' in params || params.force === true))
|
|
485
|
+
throw new Error('explicit peer is required for after_seq, after_volatile_cursor, window_mode or force');
|
|
486
|
+
const snapshot = sender ? null : await this.peers({}, background);
|
|
487
|
+
const rows = sender ? [{ sender_aid: sender }] : snapshot.peers;
|
|
488
|
+
const budget = { remaining: Math.max(1, Math.trunc(Number(params.max_pages ?? 100) || 100)) };
|
|
489
|
+
const results = await Promise.all(rows.map(async (row) => {
|
|
490
|
+
const peer = String(row.sender_aid);
|
|
491
|
+
const ns = await this.initializePeer(peer);
|
|
492
|
+
try {
|
|
493
|
+
const state = this.syncState(ns);
|
|
494
|
+
if (!params.window_mode && params.force !== true && this.peerOwnType() === 'human' && state.head <= state.ack && budget.remaining > 0) {
|
|
495
|
+
const tail = await client._v2E2EE.pullPeerV2(peer, { ...params, window_mode: 'tail', budget, background });
|
|
496
|
+
if (!budget.remaining) {
|
|
497
|
+
if (row.volatile_has_more)
|
|
498
|
+
this.schedulePeerRecovery(peer, Number(tail.head_seq ?? 0), true);
|
|
499
|
+
return { ...tail, complete: tail.complete && !row.volatile_has_more };
|
|
500
|
+
}
|
|
501
|
+
const forward = await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background });
|
|
502
|
+
const messages = [...new Map([...tail.messages, ...forward.messages].map((message) => [message.seq ?? message.volatile_cursor, message])).values()];
|
|
503
|
+
return { ...forward, messages };
|
|
504
|
+
}
|
|
505
|
+
return await client._v2E2EE.pullPeerV2(peer, { ...params, budget, background });
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
this.schedulePeerRecovery(peer, Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
|
|
509
|
+
return { sender_aid: peer, messages: [], complete: false, ack_seq: this.syncState(ns).ack, error: error instanceof Error ? error.message : String(error) };
|
|
510
|
+
}
|
|
511
|
+
}));
|
|
512
|
+
return { cursor_scope: 'peer', peers: results, messages: results.flatMap((row) => row.messages), partial: results.some((row) => !row.complete) };
|
|
513
|
+
}
|
|
514
|
+
refreshPendingPeers() {
|
|
515
|
+
const client = this.runtime.client;
|
|
516
|
+
if (client._sessionOptions?.background_sync === false || !client._aid || client._closing || client.state !== ConnectionState.READY)
|
|
517
|
+
return;
|
|
518
|
+
this.ensurePeerContext();
|
|
519
|
+
this.peerCatalogDirty = true;
|
|
520
|
+
if (this.peerCatalogTask)
|
|
521
|
+
return;
|
|
522
|
+
const generation = this.captureInlineAckGeneration();
|
|
523
|
+
const task = (async () => {
|
|
524
|
+
while (this.peerCatalogDirty && generation === this.captureInlineAckGeneration()) {
|
|
525
|
+
this.peerCatalogDirty = false;
|
|
526
|
+
const result = await this.peers({}, true);
|
|
527
|
+
const rows = [...result.peers];
|
|
528
|
+
const hint = (row) => this.peerPullPriority(this.peerNamespace(String(row.sender_aid)));
|
|
529
|
+
rows.sort((left, right) => hint(left) - hint(right));
|
|
530
|
+
for (const row of rows)
|
|
531
|
+
this.schedulePeerRecovery(String(row.sender_aid), Number(row.head_seq ?? 0), Boolean(row.volatile_has_more));
|
|
532
|
+
}
|
|
533
|
+
})();
|
|
534
|
+
this.peerCatalogTask = task.then(() => ({}), () => ({}));
|
|
535
|
+
client._safeAsync(task.catch(() => {
|
|
536
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
537
|
+
return;
|
|
538
|
+
const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
|
|
539
|
+
this.refreshPendingPeers(); }, BACKGROUND_PULL_RETRY_DELAY_MS);
|
|
540
|
+
timer.unref?.();
|
|
541
|
+
}).finally(() => { if (generation === this.captureInlineAckGeneration())
|
|
542
|
+
this.peerCatalogTask = null; }));
|
|
543
|
+
}
|
|
544
|
+
schedulePeerRecovery(sender, head = 0, volatile = false) {
|
|
545
|
+
const client = this.runtime.client;
|
|
546
|
+
if (client._sessionOptions?.background_sync === false || !sender || client._closing)
|
|
547
|
+
return;
|
|
548
|
+
this.ensurePeerContext();
|
|
549
|
+
const need = this.peerRecoveryNeeds.get(sender) ?? { head: 0, volatile: false, pending: false };
|
|
550
|
+
need.head = Math.max(need.head, head);
|
|
551
|
+
need.volatile ||= volatile;
|
|
552
|
+
need.pending = true;
|
|
553
|
+
this.peerRecoveryNeeds.set(sender, need);
|
|
554
|
+
if (this.peerRecoveryTasks.has(sender))
|
|
555
|
+
return;
|
|
556
|
+
const generation = this.captureInlineAckGeneration();
|
|
557
|
+
const task = (async () => {
|
|
558
|
+
await this.initializePeer(sender);
|
|
559
|
+
const state = this.syncState(this.peerNamespace(sender));
|
|
560
|
+
if (this.peerOwnType() === 'human' && need.head > state.head && state.ack >= state.tail - 1) {
|
|
561
|
+
await client._v2E2EE.pullPeerV2(sender, { window_mode: 'tail', background: true, retainMessages: false, budget: { remaining: 1 } });
|
|
562
|
+
}
|
|
563
|
+
while (generation === this.captureInlineAckGeneration() && this.peerRecoveryNeeds.has(sender)) {
|
|
564
|
+
need.pending = false;
|
|
565
|
+
const result = await client._v2E2EE.pullPeerV2(sender, { background: true, retainMessages: false, budget: { remaining: 100 } });
|
|
566
|
+
if (result.complete && result.ack_seq >= need.head && !need.pending) {
|
|
567
|
+
this.peerRecoveryNeeds.delete(sender);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
await new Promise((resolve) => setTimeout(resolve, BACKGROUND_PULL_RETRY_DELAY_MS));
|
|
571
|
+
}
|
|
572
|
+
})();
|
|
573
|
+
this.peerRecoveryTasks.set(sender, task);
|
|
574
|
+
client._safeAsync(task.catch(() => {
|
|
575
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
576
|
+
return;
|
|
577
|
+
const timer = setTimeout(() => { if (generation === this.captureInlineAckGeneration())
|
|
578
|
+
this.schedulePeerRecovery(sender, need.head, need.volatile); }, BACKGROUND_PULL_RETRY_DELAY_MS);
|
|
579
|
+
timer.unref?.();
|
|
580
|
+
}).finally(() => {
|
|
581
|
+
if (generation === this.captureInlineAckGeneration()) {
|
|
582
|
+
this.peerRecoveryTasks.delete(sender);
|
|
583
|
+
if (this.peerCatalogDirty && this.peerRecoveryTasks.size === 0)
|
|
584
|
+
this.refreshPendingPeers();
|
|
585
|
+
}
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
100
588
|
isPullOperationCurrent(namespace = '') {
|
|
101
589
|
const pipeline = this.runtime.client._rpcPipeline;
|
|
102
590
|
return typeof pipeline?.isPullNamespaceCurrent === 'function'
|
|
@@ -164,15 +652,13 @@ export class MessageDeliveryEngine {
|
|
|
164
652
|
const client = this.runtime.client;
|
|
165
653
|
const response = result;
|
|
166
654
|
const volatileMessages = method === 'message.pull' ? this.volatileMessages(response.messages) : [];
|
|
167
|
-
if (volatileMessages.length > 0) {
|
|
168
|
-
await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
|
|
169
|
-
}
|
|
170
655
|
this.ensurePullOperationCurrent(ns);
|
|
171
656
|
this.syncStarted(ns);
|
|
172
657
|
const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
|
|
173
658
|
const coordinator = this.forwardCoordinator();
|
|
174
659
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
175
660
|
const pageInlineGeneration = this.captureInlineAckGeneration();
|
|
661
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
176
662
|
const deferredServerCursor = Number(coordinator.pendingForwardCursor(ns, pageContigBefore) ?? 0);
|
|
177
663
|
const deferredForwardAck = Number(coordinator.pendingForwardAck(ns, pageContigBefore) ?? 0);
|
|
178
664
|
let pageTrackerSnapshot = null;
|
|
@@ -196,44 +682,51 @@ export class MessageDeliveryEngine {
|
|
|
196
682
|
let publishedCount = 0;
|
|
197
683
|
let committed = false;
|
|
198
684
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const appEvent = this.p2pAppEventForMessage(rawMessage);
|
|
204
|
-
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
205
|
-
publishedCount += 1;
|
|
206
|
-
}
|
|
685
|
+
await this.runApplicationPage(ns, 'pull', async () => {
|
|
686
|
+
if (volatileMessages.length > 0)
|
|
687
|
+
await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
|
|
688
|
+
for (const rawMessage of messages) {
|
|
207
689
|
this.ensurePullOperationCurrent(ns);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
690
|
+
const seq = positiveSafeSequenceHint(rawMessage.seq);
|
|
691
|
+
if (method === 'message.pull') {
|
|
692
|
+
const appEvent = this.p2pAppEventForMessage(rawMessage);
|
|
693
|
+
if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
|
|
694
|
+
publishedCount += 1;
|
|
695
|
+
}
|
|
213
696
|
this.ensurePullOperationCurrent(ns);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const message = normalizeGroupMentionMode(rawMessage);
|
|
700
|
+
if (this.recallEventFromGroupMessage(message)) {
|
|
701
|
+
if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
|
|
702
|
+
this.ensurePullOperationCurrent(ns);
|
|
703
|
+
this.markPublishedSeq(ns, seq);
|
|
704
|
+
publishedCount += 1;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else if (this.isSelfSentGroupMessage(message)) {
|
|
214
708
|
this.markPublishedSeq(ns, seq);
|
|
709
|
+
}
|
|
710
|
+
else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
|
|
711
|
+
this.ensurePullOperationCurrent(ns);
|
|
215
712
|
publishedCount += 1;
|
|
216
713
|
}
|
|
217
714
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
715
|
+
this.ensurePullOperationCurrent(ns);
|
|
716
|
+
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
717
|
+
throw new Error('pull invalidated');
|
|
718
|
+
const previousWindow = client._seqTracker.getSyncState(ns);
|
|
719
|
+
const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
|
|
720
|
+
const deliveryTarget = scanTarget >= previousWindow.tail - 1
|
|
721
|
+
? Math.max(scanTarget, previousWindow.head) : scanTarget;
|
|
722
|
+
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
|
|
723
|
+
});
|
|
226
724
|
this.ensurePullOperationCurrent(ns);
|
|
227
725
|
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
228
726
|
throw new Error('pull invalidated');
|
|
229
|
-
|
|
230
|
-
const scanTarget = Math.max(pageContigBefore, ...messages.map((message) => positiveSafeSequenceHint(message.seq)), positiveSafeSequenceHint(response.contiguous_seq), retentionFloor, visibilityFloor, deferredServerCursor);
|
|
231
|
-
const deliveryTarget = scanTarget >= previousWindow.tail - 1
|
|
232
|
-
? Math.max(scanTarget, previousWindow.head) : scanTarget;
|
|
233
|
-
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, deliveryTarget);
|
|
234
|
-
this.ensurePullOperationCurrent(ns);
|
|
235
|
-
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
727
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
236
728
|
throw new Error('pull invalidated');
|
|
729
|
+
this.rememberCommittedSeqState(ns);
|
|
237
730
|
pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
|
|
238
731
|
trackerMutated = true;
|
|
239
732
|
client._seqTracker.onPullResult(ns, messages, afterSeq, Number.isSafeInteger(result.contiguous_seq)
|
|
@@ -249,11 +742,14 @@ export class MessageDeliveryEngine {
|
|
|
249
742
|
this.ensurePullOperationCurrent(ns);
|
|
250
743
|
if (pageInlineGeneration !== this.captureInlineAckGeneration())
|
|
251
744
|
throw new Error('pull invalidated');
|
|
745
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
746
|
+
throw new Error('pull invalidated');
|
|
252
747
|
}
|
|
253
748
|
committed = true;
|
|
254
749
|
}
|
|
255
750
|
catch (exc) {
|
|
256
751
|
if (trackerMutated && pageInlineGeneration === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
|
|
752
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
257
753
|
&& !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
258
754
|
(client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
|
|
259
755
|
.call(client._seqTracker, ns, pageTrackerSnapshot);
|
|
@@ -324,23 +820,39 @@ export class MessageDeliveryEngine {
|
|
|
324
820
|
this.volatileCursor = '';
|
|
325
821
|
this.volatileSeenMessages.clear();
|
|
326
822
|
this.volatileInFlightMessages.clear();
|
|
823
|
+
for (const completion of this.volatileCompletions.values())
|
|
824
|
+
completion.reject(new Error('delivery invalidated'));
|
|
825
|
+
this.volatileCompletions.clear();
|
|
327
826
|
}
|
|
328
827
|
volatilePullCursor() {
|
|
329
828
|
this.ensureVolatileContext();
|
|
330
829
|
return this.volatileCursor;
|
|
331
830
|
}
|
|
831
|
+
peerVolatilePullCursor(sender) {
|
|
832
|
+
this.ensurePeerContext();
|
|
833
|
+
return this.peerVolatileCursors.get(sender) ?? '';
|
|
834
|
+
}
|
|
835
|
+
setPeerVolatilePullCursor(sender, cursor) {
|
|
836
|
+
this.ensurePeerContext();
|
|
837
|
+
if (cursor)
|
|
838
|
+
this.peerVolatileCursors.set(sender, cursor);
|
|
839
|
+
else
|
|
840
|
+
this.peerVolatileCursors.delete(sender);
|
|
841
|
+
}
|
|
332
842
|
resetVolatilePullCursor() {
|
|
333
843
|
this.ensureVolatileContext();
|
|
334
844
|
this.volatileCursor = '';
|
|
335
845
|
}
|
|
336
846
|
volatileSeenKey(message) {
|
|
337
847
|
const messageId = String(message.message_id ?? '').trim();
|
|
338
|
-
const sender = String(message.from_aid ?? message.from ?? '').trim();
|
|
848
|
+
const sender = String(message.sender_aid ?? message.from_aid ?? message.from ?? '').trim();
|
|
339
849
|
const cursor = String(message.volatile_cursor ?? '').trim();
|
|
340
|
-
return
|
|
850
|
+
return JSON.stringify([sender, messageId, cursor]);
|
|
341
851
|
}
|
|
342
852
|
isVolatileSeen(message) {
|
|
343
853
|
this.ensureVolatileContext();
|
|
854
|
+
if (this.currentApplicationPage()?.volatileSeen.has(this.volatileSeenKey(message)))
|
|
855
|
+
return true;
|
|
344
856
|
const now = Date.now();
|
|
345
857
|
for (const [key, expiresAt] of this.volatileSeenMessages) {
|
|
346
858
|
if (expiresAt <= now)
|
|
@@ -350,6 +862,12 @@ export class MessageDeliveryEngine {
|
|
|
350
862
|
}
|
|
351
863
|
markVolatileSeen(message) {
|
|
352
864
|
this.ensureVolatileContext();
|
|
865
|
+
const page = this.currentApplicationPage();
|
|
866
|
+
if (page) {
|
|
867
|
+
page.volatileSeen.add(this.volatileSeenKey(message));
|
|
868
|
+
page.commits.push(() => this.markVolatileSeen(message));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
353
871
|
const now = Date.now();
|
|
354
872
|
const expiresAt = Math.max(now + 60_000, Number(message.expires_at ?? now + 1_800_000) || now + 1_800_000);
|
|
355
873
|
this.volatileSeenMessages.set(this.volatileSeenKey(message), expiresAt);
|
|
@@ -367,11 +885,38 @@ export class MessageDeliveryEngine {
|
|
|
367
885
|
if (this.volatileInFlightMessages.has(key))
|
|
368
886
|
return 'inflight';
|
|
369
887
|
this.volatileInFlightMessages.add(key);
|
|
888
|
+
let resolve, reject;
|
|
889
|
+
const promise = new Promise((done, fail) => { resolve = done; reject = fail; });
|
|
890
|
+
void promise.catch(() => { });
|
|
891
|
+
const page = this.currentApplicationPage();
|
|
892
|
+
const completion = { page, promise, resolve, reject };
|
|
893
|
+
this.volatileCompletions.set(key, completion);
|
|
894
|
+
if (page)
|
|
895
|
+
void page.completion.then(resolve, reject).finally(() => {
|
|
896
|
+
if (this.volatileCompletions.get(key) === completion) {
|
|
897
|
+
this.volatileCompletions.delete(key);
|
|
898
|
+
this.volatileInFlightMessages.delete(key);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
370
901
|
return 'claimed';
|
|
371
902
|
}
|
|
903
|
+
async waitVolatileDelivery(message) {
|
|
904
|
+
const completion = this.volatileCompletions.get(this.volatileSeenKey(message));
|
|
905
|
+
if (!completion || completion.page === this.currentApplicationPage())
|
|
906
|
+
return;
|
|
907
|
+
if (this.runtime.client._dispatcher._isApplicationHandler?.())
|
|
908
|
+
throw new Error('delivery cannot wait inside an application handler');
|
|
909
|
+
await completion.promise;
|
|
910
|
+
}
|
|
372
911
|
finishVolatileDelivery(message) {
|
|
373
912
|
this.ensureVolatileContext();
|
|
374
|
-
this.
|
|
913
|
+
const key = this.volatileSeenKey(message);
|
|
914
|
+
const completion = this.volatileCompletions.get(key);
|
|
915
|
+
if (completion?.page)
|
|
916
|
+
return;
|
|
917
|
+
completion?.resolve();
|
|
918
|
+
this.volatileCompletions.delete(key);
|
|
919
|
+
this.volatileInFlightMessages.delete(key);
|
|
375
920
|
}
|
|
376
921
|
publicVolatileMessage(message) {
|
|
377
922
|
const result = this.stripInternalSenderDeviceFields(message);
|
|
@@ -379,15 +924,27 @@ export class MessageDeliveryEngine {
|
|
|
379
924
|
return result;
|
|
380
925
|
}
|
|
381
926
|
async ackVolatileMessages(messages, v2, advanceCursor) {
|
|
927
|
+
const page = this.currentApplicationPage();
|
|
928
|
+
if (page) {
|
|
929
|
+
page.afterDelivery.push(() => this.ackVolatileMessages(messages, v2, advanceCursor));
|
|
930
|
+
return undefined;
|
|
931
|
+
}
|
|
382
932
|
const cursors = [...new Set(messages
|
|
383
933
|
.map((message) => String(message.volatile_cursor ?? '').trim())
|
|
384
934
|
.filter(Boolean))];
|
|
385
935
|
if (cursors.length === 0)
|
|
386
936
|
return undefined;
|
|
387
|
-
const
|
|
937
|
+
const client = this.runtime.client;
|
|
938
|
+
const sender = client._p2pPeerPullEnabled ? String(messages[0]?.sender_aid ?? messages[0]?.from_aid ?? messages[0]?.from ?? '') : '';
|
|
939
|
+
const result = sender
|
|
940
|
+
? await client._v2E2EE.ackPeerV2(sender, 0, cursors)
|
|
941
|
+
: await client._callRawV2Rpc(v2 ? 'message.v2.ack' : 'message.ack', { volatile_cursors: cursors, _rpc_background: true });
|
|
388
942
|
if (advanceCursor) {
|
|
389
943
|
this.ensureVolatileContext();
|
|
390
|
-
|
|
944
|
+
if (sender)
|
|
945
|
+
this.setPeerVolatilePullCursor(sender, cursors[cursors.length - 1] ?? '');
|
|
946
|
+
else
|
|
947
|
+
this.volatileCursor = cursors[cursors.length - 1] ?? '';
|
|
391
948
|
}
|
|
392
949
|
return result;
|
|
393
950
|
}
|
|
@@ -400,8 +957,12 @@ export class MessageDeliveryEngine {
|
|
|
400
957
|
processed.push(rawMessage);
|
|
401
958
|
continue;
|
|
402
959
|
}
|
|
403
|
-
if (claim === 'inflight')
|
|
960
|
+
if (claim === 'inflight') {
|
|
961
|
+
await this.waitVolatileDelivery(message);
|
|
962
|
+
if (this.isVolatileSeen(message))
|
|
963
|
+
processed.push(rawMessage);
|
|
404
964
|
continue;
|
|
965
|
+
}
|
|
405
966
|
try {
|
|
406
967
|
const appEvent = this.p2pAppEventForMessage(message);
|
|
407
968
|
await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source);
|
|
@@ -422,6 +983,13 @@ export class MessageDeliveryEngine {
|
|
|
422
983
|
this.deliveryChangedGeneration += 1;
|
|
423
984
|
this.pendingDeliveryChanges = null;
|
|
424
985
|
this.inlineAckGeneration += 1;
|
|
986
|
+
if (this.backgroundPullTimer)
|
|
987
|
+
clearTimeout(this.backgroundPullTimer);
|
|
988
|
+
this.backgroundPullTimer = null;
|
|
989
|
+
this.backgroundPullTimerAt = 0;
|
|
990
|
+
this.backgroundPullNeeds.clear();
|
|
991
|
+
this.backgroundPullTasks.clear();
|
|
992
|
+
this.backgroundPullCheckQueued = false;
|
|
425
993
|
void this.runtime.client._rpcPipeline?.invalidatePulls?.();
|
|
426
994
|
this.pendingP2PInlineAcks = null;
|
|
427
995
|
this.pendingGroupInlineAcks = null;
|
|
@@ -432,6 +1000,12 @@ export class MessageDeliveryEngine {
|
|
|
432
1000
|
this.onlineUnreadHintTasks = null;
|
|
433
1001
|
this.realtimeTailHeads = null;
|
|
434
1002
|
this.syncSessions = null;
|
|
1003
|
+
this.peerCatalogTask = null;
|
|
1004
|
+
this.peerCatalogDirty = false;
|
|
1005
|
+
this.peerRecoveryNeeds.clear();
|
|
1006
|
+
this.peerPullHints.clear();
|
|
1007
|
+
this.peerPullConfirmedThrough.clear();
|
|
1008
|
+
this.peerRecoveryTasks.clear();
|
|
435
1009
|
}
|
|
436
1010
|
deliveryChangedNamespace(event, payload) {
|
|
437
1011
|
if (event === 'group.message_created' && isJsonObject(payload)) {
|
|
@@ -693,6 +1267,7 @@ export class MessageDeliveryEngine {
|
|
|
693
1267
|
}
|
|
694
1268
|
endRealtimeSync(ns) {
|
|
695
1269
|
this.realtimeSyncing?.delete(ns);
|
|
1270
|
+
this.requestBackgroundPullRecovery();
|
|
696
1271
|
this.runtime.client._rpcPipeline?.requestPullDrainedCheck?.();
|
|
697
1272
|
}
|
|
698
1273
|
hasPendingPull(ns) {
|
|
@@ -702,7 +1277,9 @@ export class MessageDeliveryEngine {
|
|
|
702
1277
|
return this.pendingGroupPullUpper?.get(ns) ?? 0;
|
|
703
1278
|
}
|
|
704
1279
|
hasAnyPendingPull() {
|
|
705
|
-
return
|
|
1280
|
+
return this.peerCatalogDirty || this.peerRecoveryNeeds.size > 0 || this.peerRecoveryTasks.size > 0 || Boolean(this.peerCatalogTask)
|
|
1281
|
+
|| [...(this.pendingGroupPullUpper?.values() ?? [])].some((seq) => seq > 0)
|
|
1282
|
+
|| this.backgroundPullNeeds.size > 0
|
|
706
1283
|
|| Boolean(this.onlineUnreadHintTasks?.size)
|
|
707
1284
|
|| Boolean(this.realtimeSyncing?.size)
|
|
708
1285
|
|| Boolean(this.runtime.client._v2E2EE?.hasPendingSenderIKWork?.());
|
|
@@ -710,14 +1287,136 @@ export class MessageDeliveryEngine {
|
|
|
710
1287
|
onPullGateIdle(ns) {
|
|
711
1288
|
if (!ns)
|
|
712
1289
|
return;
|
|
1290
|
+
this.requestBackgroundPullRecovery();
|
|
713
1291
|
if (ns.startsWith('p2p:')) {
|
|
714
1292
|
this.runtime.client._schedulePendingP2pPullIfNeeded?.(ns, 'pull-gate-idle');
|
|
715
1293
|
return;
|
|
716
1294
|
}
|
|
717
1295
|
this.schedulePendingGroupPullIfNeeded(ns, 'pull-gate-idle');
|
|
718
1296
|
}
|
|
1297
|
+
recordBackgroundPullNeed(ns, throughSeq = 0, probe = false) {
|
|
1298
|
+
const client = this.runtime.client;
|
|
1299
|
+
if (client._sessionOptions?.background_sync === false || !client._aid
|
|
1300
|
+
|| !(ns === `p2p:${client._aid}` || ns.startsWith('group:') || ns.startsWith('group_event:')))
|
|
1301
|
+
return;
|
|
1302
|
+
const need = this.backgroundPullNeeds.get(ns) ?? {
|
|
1303
|
+
throughSeq: 0, hasMore: false, probe, lastAfter: -1, retryAt: 0,
|
|
1304
|
+
};
|
|
1305
|
+
need.throughSeq = Math.max(need.throughSeq, throughSeq);
|
|
1306
|
+
need.probe ||= probe;
|
|
1307
|
+
this.backgroundPullNeeds.set(ns, need);
|
|
1308
|
+
}
|
|
1309
|
+
observeBackgroundPullResponse(ns, afterSeq, result, limit = 50) {
|
|
1310
|
+
if (!isJsonObject(result))
|
|
1311
|
+
return;
|
|
1312
|
+
const response = result;
|
|
1313
|
+
const rows = ns.startsWith('group_event:') ? response.events : response.messages;
|
|
1314
|
+
if (!Array.isArray(rows))
|
|
1315
|
+
return;
|
|
1316
|
+
const cursor = isJsonObject(response.cursor)
|
|
1317
|
+
? response.cursor : null;
|
|
1318
|
+
const throughSeq = Math.max(positiveSafeSequenceHint(cursor?.latest_seq), positiveSafeSequenceHint(response.latest_seq), positiveSafeSequenceHint(response.latest_message_seq), positiveSafeSequenceHint(response.latest_event_seq), ...rows.map((row) => row && typeof row === 'object' && row.storage !== 'volatile'
|
|
1319
|
+
? positiveSafeSequenceHint(row.seq ?? row.event_seq) : 0));
|
|
1320
|
+
this.recordBackgroundPullNeed(ns, throughSeq);
|
|
1321
|
+
const need = this.backgroundPullNeeds.get(ns);
|
|
1322
|
+
if (!need || afterSeq < need.lastAfter)
|
|
1323
|
+
return;
|
|
1324
|
+
need.lastAfter = afterSeq;
|
|
1325
|
+
need.hasMore = response.has_more === true || positiveSafeSequenceHint(response.remaining) > 0
|
|
1326
|
+
|| (!('has_more' in response) && limit > 0 && rows.length >= limit);
|
|
1327
|
+
need.probe = false;
|
|
1328
|
+
}
|
|
1329
|
+
finishBackgroundPull(ns, generation, afterSeq, failed) {
|
|
1330
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
1331
|
+
return;
|
|
1332
|
+
const need = this.backgroundPullNeeds.get(ns);
|
|
1333
|
+
if (need) {
|
|
1334
|
+
if (failed)
|
|
1335
|
+
need.probe = true;
|
|
1336
|
+
else
|
|
1337
|
+
need.probe = false;
|
|
1338
|
+
need.retryAt = failed || this.syncState(ns).ack <= afterSeq
|
|
1339
|
+
? Date.now() + BACKGROUND_PULL_RETRY_DELAY_MS : 0;
|
|
1340
|
+
}
|
|
1341
|
+
this.requestBackgroundPullRecovery();
|
|
1342
|
+
}
|
|
1343
|
+
requestBackgroundPullRecovery() {
|
|
1344
|
+
if (this.backgroundPullCheckQueued || (this.backgroundPullNeeds.size === 0 && !this.peerCatalogDirty && this.peerRecoveryNeeds.size === 0))
|
|
1345
|
+
return;
|
|
1346
|
+
this.backgroundPullCheckQueued = true;
|
|
1347
|
+
const generation = this.captureInlineAckGeneration();
|
|
1348
|
+
queueMicrotask(() => {
|
|
1349
|
+
this.backgroundPullCheckQueued = false;
|
|
1350
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
1351
|
+
return;
|
|
1352
|
+
const client = this.runtime.client;
|
|
1353
|
+
if (client._sessionOptions?.background_sync === false || client.state !== ConnectionState.READY || client._closing)
|
|
1354
|
+
return;
|
|
1355
|
+
if (client._p2pPeerPullEnabled) {
|
|
1356
|
+
if (this.peerCatalogDirty && !this.peerCatalogTask)
|
|
1357
|
+
this.refreshPendingPeers();
|
|
1358
|
+
for (const [sender, need] of this.peerRecoveryNeeds) {
|
|
1359
|
+
if (!this.peerRecoveryTasks.has(sender))
|
|
1360
|
+
this.schedulePeerRecovery(sender, need.head, need.volatile);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
let retryAt = Infinity;
|
|
1364
|
+
for (const [ns, need] of this.backgroundPullNeeds) {
|
|
1365
|
+
if (this.backgroundPullTasks.has(ns) || this.realtimeSyncing?.has(ns) || this.onlineUnreadHintTasks?.has(ns))
|
|
1366
|
+
continue;
|
|
1367
|
+
const eventScope = ns.startsWith('group_event:');
|
|
1368
|
+
const filling = [...(client._gapFillDone?.keys?.() ?? [])].some((key) => eventScope
|
|
1369
|
+
? String(key) === `group_event_pull:${ns}`
|
|
1370
|
+
: ns.startsWith('p2p:') ? String(key).startsWith('p2p:') : String(key).startsWith(`group_msg:${ns.slice(6)}:`));
|
|
1371
|
+
if (filling || (!eventScope && client._rpcPipeline?.hasPullActivity?.(ns, false))
|
|
1372
|
+
|| (eventScope && ['pulling', 'processing', 'pulled'].includes(client._rpcPipeline?.getPullLifecycle?.(ns))))
|
|
1373
|
+
continue;
|
|
1374
|
+
if (this.syncState(ns).ack >= need.throughSeq && !need.hasMore && !need.probe) {
|
|
1375
|
+
this.backgroundPullNeeds.delete(ns);
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (need.retryAt > Date.now()) {
|
|
1379
|
+
retryAt = Math.min(retryAt, need.retryAt);
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
this.backgroundPullTasks.add(ns);
|
|
1383
|
+
const task = Promise.resolve().then(() => {
|
|
1384
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
1385
|
+
return;
|
|
1386
|
+
return eventScope ? this.fillGroupEventGap(ns.slice('group_event:'.length))
|
|
1387
|
+
: ns.startsWith('group:') ? this.fillGroupGap(ns.slice(6)) : this.fillP2pGap();
|
|
1388
|
+
}).finally(() => {
|
|
1389
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
1390
|
+
return;
|
|
1391
|
+
this.backgroundPullTasks.delete(ns);
|
|
1392
|
+
this.requestBackgroundPullRecovery();
|
|
1393
|
+
});
|
|
1394
|
+
client._safeAsync(task);
|
|
1395
|
+
}
|
|
1396
|
+
if (this.backgroundPullNeeds.size === 0) {
|
|
1397
|
+
if (this.backgroundPullTimer)
|
|
1398
|
+
clearTimeout(this.backgroundPullTimer);
|
|
1399
|
+
this.backgroundPullTimer = null;
|
|
1400
|
+
this.backgroundPullTimerAt = 0;
|
|
1401
|
+
client._rpcPipeline?.requestPullDrainedCheck?.();
|
|
1402
|
+
}
|
|
1403
|
+
if (Number.isFinite(retryAt) && (!this.backgroundPullTimer || retryAt < this.backgroundPullTimerAt)) {
|
|
1404
|
+
if (this.backgroundPullTimer)
|
|
1405
|
+
clearTimeout(this.backgroundPullTimer);
|
|
1406
|
+
this.backgroundPullTimerAt = retryAt;
|
|
1407
|
+
this.backgroundPullTimer = setTimeout(() => {
|
|
1408
|
+
this.backgroundPullTimer = null;
|
|
1409
|
+
this.backgroundPullTimerAt = 0;
|
|
1410
|
+
this.requestBackgroundPullRecovery();
|
|
1411
|
+
}, Math.max(0, retryAt - Date.now()));
|
|
1412
|
+
this.backgroundPullTimer.unref?.();
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
719
1416
|
onForegroundPullsDrained() {
|
|
720
1417
|
const client = this.runtime.client;
|
|
1418
|
+
if (client._p2pPeerPullEnabled)
|
|
1419
|
+
return;
|
|
721
1420
|
if (!client._aid || client._sessionOptions?.background_sync === false
|
|
722
1421
|
|| client.state !== ConnectionState.READY || client._closing)
|
|
723
1422
|
return;
|
|
@@ -771,6 +1470,12 @@ export class MessageDeliveryEngine {
|
|
|
771
1470
|
}
|
|
772
1471
|
}
|
|
773
1472
|
markPublishedSeq(ns, seq) {
|
|
1473
|
+
const page = this.currentApplicationPage();
|
|
1474
|
+
if (page) {
|
|
1475
|
+
page.consumed.add(JSON.stringify(['published', ns, seq]));
|
|
1476
|
+
page.commits.push(() => this.markPublishedSeq(ns, seq));
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
774
1479
|
const client = this.runtime.client;
|
|
775
1480
|
let pushed = client._pushedSeqs.get(ns);
|
|
776
1481
|
if (!pushed) {
|
|
@@ -783,6 +1488,18 @@ export class MessageDeliveryEngine {
|
|
|
783
1488
|
client._pushedSeqs.set(ns, new Set(keep));
|
|
784
1489
|
}
|
|
785
1490
|
}
|
|
1491
|
+
hasPublishedSeq(ns, seq) {
|
|
1492
|
+
return this.runtime.client._pushedSeqs.get(ns)?.has(seq) === true
|
|
1493
|
+
|| this.currentApplicationPage()?.consumed.has(JSON.stringify(['published', ns, seq])) === true;
|
|
1494
|
+
}
|
|
1495
|
+
consumeOrderedMessage(ns, seq, queue) {
|
|
1496
|
+
this.currentApplicationPage()?.consumed.add(JSON.stringify(['ordered', ns, seq]));
|
|
1497
|
+
this.completeApplicationDelivery(() => {
|
|
1498
|
+
queue?.delete(seq);
|
|
1499
|
+
if (queue?.size === 0)
|
|
1500
|
+
this.runtime.client._pendingOrderedMsgs.delete(ns);
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
786
1503
|
enqueueOrderedMessage(ns, event, seq, payload, source = 'push') {
|
|
787
1504
|
const client = this.runtime.client;
|
|
788
1505
|
let queue = client._pendingOrderedMsgs.get(ns);
|
|
@@ -833,21 +1550,38 @@ export class MessageDeliveryEngine {
|
|
|
833
1550
|
async publishOrderedGroupChanged(payload, source = 'ordered') {
|
|
834
1551
|
const client = this.runtime.client;
|
|
835
1552
|
const generation = this.captureInlineAckGeneration();
|
|
1553
|
+
const scope = this.applicationPage.getStore();
|
|
1554
|
+
const page = scope?.collecting ? scope : undefined;
|
|
1555
|
+
const owner = this.applicationClaims.get(this.applicationClaimKey('group.changed', payload, this.applicationNamespace('group.changed', payload), this.ensurePeerContext()));
|
|
1556
|
+
if (owner) {
|
|
1557
|
+
if (owner !== this.currentApplicationPage())
|
|
1558
|
+
await this.waitForApplicationOwner(owner);
|
|
1559
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
1560
|
+
throw new Error('pull invalidated');
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
836
1563
|
const published = client._publishAppEvent('group.changed', payload, source);
|
|
837
1564
|
if (isPromiseLike(published))
|
|
838
1565
|
await published;
|
|
839
1566
|
if (generation !== this.captureInlineAckGeneration())
|
|
840
1567
|
throw new Error('pull invalidated');
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1568
|
+
this.completeApplicationDelivery(() => {
|
|
1569
|
+
if (isJsonObject(payload)) {
|
|
1570
|
+
const eventPayload = payload;
|
|
1571
|
+
client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
|
|
1572
|
+
if (eventPayload.action === 'dissolved') {
|
|
1573
|
+
const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
|
|
1574
|
+
const groupId = String(rawGroupId).trim();
|
|
1575
|
+
if (groupId) {
|
|
1576
|
+
const cleanup = () => { client._cleanupDissolvedGroup?.(groupId); };
|
|
1577
|
+
if (page)
|
|
1578
|
+
page.cleanup.push(cleanup);
|
|
1579
|
+
else
|
|
1580
|
+
cleanup();
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
849
1583
|
}
|
|
850
|
-
}
|
|
1584
|
+
});
|
|
851
1585
|
}
|
|
852
1586
|
isInstanceScopedMessageEvent(event) {
|
|
853
1587
|
return event === 'message.received'
|
|
@@ -951,6 +1685,7 @@ export class MessageDeliveryEngine {
|
|
|
951
1685
|
setIfPresent('timestamp', firstValue(message.timestamp, message.created_at, message.t_server));
|
|
952
1686
|
if ('encrypted' in message)
|
|
953
1687
|
envelope.encrypted = Boolean(message.encrypted);
|
|
1688
|
+
envelope.need_render = message.need_render !== false;
|
|
954
1689
|
const context = this.envelopeMetadata(message.context);
|
|
955
1690
|
if (context)
|
|
956
1691
|
envelope.context = context;
|
|
@@ -1018,6 +1753,7 @@ export class MessageDeliveryEngine {
|
|
|
1018
1753
|
setIfPresent('version', firstValue(body.version, params.version));
|
|
1019
1754
|
setIfPresent('timestamp', firstValue(params.timestamp, resultObj.timestamp, resultObj.created_at, resultObj.t_server, Date.now()));
|
|
1020
1755
|
envelope.encrypted = Boolean(encrypted);
|
|
1756
|
+
envelope.need_render = params.need_render !== false;
|
|
1021
1757
|
const context = this.envelopeMetadata(params.context);
|
|
1022
1758
|
if (context)
|
|
1023
1759
|
envelope.context = context;
|
|
@@ -1179,12 +1915,14 @@ export class MessageDeliveryEngine {
|
|
|
1179
1915
|
throw new Error('pull invalidated');
|
|
1180
1916
|
if (this.isPullScopedSource(source))
|
|
1181
1917
|
this.ensurePullOperationCurrent(`p2p:${client._aid ?? ''}`);
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1918
|
+
this.completeApplicationDelivery(() => {
|
|
1919
|
+
seen.set(dedupKey, Date.now());
|
|
1920
|
+
if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
|
|
1921
|
+
const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
|
|
1922
|
+
for (const [oldKey] of drop)
|
|
1923
|
+
seen.delete(oldKey);
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1188
1926
|
client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
|
|
1189
1927
|
return true;
|
|
1190
1928
|
}
|
|
@@ -1290,12 +2028,14 @@ export class MessageDeliveryEngine {
|
|
|
1290
2028
|
throw new Error('pull invalidated');
|
|
1291
2029
|
if (this.isPullScopedSource(source))
|
|
1292
2030
|
this.ensurePullOperationCurrent(`group:${groupId}`);
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
2031
|
+
this.completeApplicationDelivery(() => {
|
|
2032
|
+
seen.set(dedupKey, Date.now());
|
|
2033
|
+
if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
|
|
2034
|
+
const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
|
|
2035
|
+
for (const [oldKey] of drop)
|
|
2036
|
+
seen.delete(oldKey);
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
1299
2039
|
client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
|
|
1300
2040
|
return true;
|
|
1301
2041
|
}
|
|
@@ -1340,9 +2080,18 @@ export class MessageDeliveryEngine {
|
|
|
1340
2080
|
if (seqNum > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
|
|
1341
2081
|
this.fillGroupGap(groupId).catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
|
|
1342
2082
|
}
|
|
2083
|
+
await this.publishGroupRecallTombstone(groupId, seq, wrapped);
|
|
1343
2084
|
}
|
|
1344
2085
|
publishAppEvent(event, payload, source = 'direct', deliveryContext) {
|
|
1345
2086
|
const client = this.runtime.client;
|
|
2087
|
+
const scope = this.applicationPage.getStore();
|
|
2088
|
+
const page = scope?.collecting ? scope : undefined;
|
|
2089
|
+
if (APPLICATION_DELIVERY_EVENTS.has(event) && typeof client._dispatcher._snapshotApplicationHandlers === 'function') {
|
|
2090
|
+
const namespace = this.applicationNamespace(event, payload);
|
|
2091
|
+
if (!page || (page.namespace !== namespace && !(page.namespace.startsWith('p2p:') && event.startsWith('message.')))) {
|
|
2092
|
+
return this.runApplicationPage(namespace, source, async () => { await this.publishAppEvent(event, payload, source, deliveryContext); });
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
1346
2095
|
if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
|
|
1347
2096
|
client._maybeAppendEchoTraceReceive(payload);
|
|
1348
2097
|
}
|
|
@@ -1363,22 +2112,33 @@ export class MessageDeliveryEngine {
|
|
|
1363
2112
|
}
|
|
1364
2113
|
const generation = this.deliveryChangedGeneration;
|
|
1365
2114
|
const normalized = this.normalizePublishedMessagePayload(event, payload, source);
|
|
2115
|
+
const note = () => {
|
|
2116
|
+
if (generation === this.deliveryChangedGeneration) {
|
|
2117
|
+
this.noteDeliveryChanged(event, payload, source, deliveryContext);
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
if (page && APPLICATION_DELIVERY_EVENTS.has(event)) {
|
|
2121
|
+
if (page.batching)
|
|
2122
|
+
return this.collectApplicationEvent(page, event, normalized, note);
|
|
2123
|
+
const owner = this.applicationClaims.get(this.applicationClaimKey(event, normalized, page.namespace, page.context));
|
|
2124
|
+
if (owner)
|
|
2125
|
+
return this.waitForApplicationOwner(owner).then(() => this.ensureApplicationPageCurrent(page));
|
|
2126
|
+
}
|
|
1366
2127
|
const dispatcher = client._dispatcher;
|
|
1367
2128
|
const result = ['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
|
|
1368
2129
|
&& typeof dispatcher._publishAndWait === 'function'
|
|
1369
|
-
? dispatcher._publishAndWait(event, normalized)
|
|
2130
|
+
? dispatcher._publishAndWait(event, normalized, undefined, page?.reservation, page ? () => this.isApplicationPageCurrent(page) : undefined)
|
|
1370
2131
|
: typeof dispatcher.enqueue === 'function'
|
|
1371
2132
|
? (dispatcher.enqueue(event, normalized), undefined)
|
|
1372
2133
|
: typeof dispatcher.publishSyncAware === 'function'
|
|
1373
2134
|
? dispatcher.publishSyncAware(event, normalized)
|
|
1374
2135
|
: dispatcher.publish?.(event, normalized);
|
|
1375
|
-
const note = () => {
|
|
1376
|
-
if (generation === this.deliveryChangedGeneration) {
|
|
1377
|
-
this.noteDeliveryChanged(event, payload, source, deliveryContext);
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
1380
2136
|
if (isPromiseLike(result))
|
|
1381
|
-
return Promise.resolve(result).then(
|
|
2137
|
+
return Promise.resolve(result).then(() => {
|
|
2138
|
+
if (page && APPLICATION_DELIVERY_EVENTS.has(event))
|
|
2139
|
+
this.ensureApplicationPageCurrent(page);
|
|
2140
|
+
note();
|
|
2141
|
+
});
|
|
1382
2142
|
note();
|
|
1383
2143
|
return result;
|
|
1384
2144
|
}
|
|
@@ -1575,6 +2335,10 @@ export class MessageDeliveryEngine {
|
|
|
1575
2335
|
}
|
|
1576
2336
|
async onRawMessageReceived(data) {
|
|
1577
2337
|
const client = this.runtime.client;
|
|
2338
|
+
if (client._p2pPeerPullEnabled) {
|
|
2339
|
+
await this.onPeerPush(data);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
1578
2342
|
const tStart = Date.now();
|
|
1579
2343
|
if (isJsonObject(data)) {
|
|
1580
2344
|
client._logMessageDebug('server-push', '_raw.message.received', 'message.received', data);
|
|
@@ -1617,6 +2381,11 @@ export class MessageDeliveryEngine {
|
|
|
1617
2381
|
if (!this.messageTargetsCurrentInstance(msg))
|
|
1618
2382
|
return;
|
|
1619
2383
|
const seq = msg.seq;
|
|
2384
|
+
if (client._p2pPeerPullEnabled) {
|
|
2385
|
+
await this.onPeerPush(msg);
|
|
2386
|
+
await this.publishMessageRecallTombstone(seq, msg);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
1620
2389
|
if (seq !== undefined && seq !== null && client._aid) {
|
|
1621
2390
|
const ns = `p2p:${client._aid}`;
|
|
1622
2391
|
const seqNum = Number(seq);
|
|
@@ -1625,7 +2394,6 @@ export class MessageDeliveryEngine {
|
|
|
1625
2394
|
if (Number(seq) > client._seqTracker.getContiguousSeq(ns) && client._sessionOptions?.background_sync !== false) {
|
|
1626
2395
|
this.fillP2pGap().catch((exc) => client._clientLog.warn(`background recall gap fill failed: ${formatDeliveryError(exc)}`));
|
|
1627
2396
|
}
|
|
1628
|
-
return;
|
|
1629
2397
|
}
|
|
1630
2398
|
await this.publishMessageRecallTombstone(seq, msg);
|
|
1631
2399
|
}
|
|
@@ -1806,13 +2574,13 @@ export class MessageDeliveryEngine {
|
|
|
1806
2574
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
1807
2575
|
client._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
|
|
1808
2576
|
const operation = async () => {
|
|
1809
|
-
const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
|
|
2577
|
+
const messages = await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
|
|
1810
2578
|
this.prunePushedSeqs(ns);
|
|
1811
2579
|
return messages.length;
|
|
1812
2580
|
};
|
|
1813
2581
|
if (client._rpcPipeline) {
|
|
1814
2582
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
|
|
1815
|
-
await client._rpcPipeline.
|
|
2583
|
+
await client._rpcPipeline.runPullNamespaceSerialized(key, operation, true);
|
|
1816
2584
|
}
|
|
1817
2585
|
else {
|
|
1818
2586
|
await client._withBackgroundRpc(operation);
|
|
@@ -1820,18 +2588,25 @@ export class MessageDeliveryEngine {
|
|
|
1820
2588
|
}
|
|
1821
2589
|
async fillP2pGap() {
|
|
1822
2590
|
const client = this.runtime.client;
|
|
2591
|
+
if (client._p2pPeerPullEnabled) {
|
|
2592
|
+
this.refreshPendingPeers();
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
1823
2595
|
if (client._sessionOptions?.background_sync === false)
|
|
1824
2596
|
return;
|
|
1825
2597
|
if (!client._aid)
|
|
1826
2598
|
return;
|
|
1827
2599
|
const ns = `p2p:${client._aid}`;
|
|
1828
2600
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
2601
|
+
const generation = this.captureInlineAckGeneration();
|
|
1829
2602
|
const dedupKey = `p2p:${afterSeq}`;
|
|
1830
2603
|
if (client._gapFillDone.has(dedupKey))
|
|
1831
2604
|
return;
|
|
1832
2605
|
client._gapFillDone.set(dedupKey, Date.now());
|
|
2606
|
+
this.recordBackgroundPullNeed(ns, 0, true);
|
|
1833
2607
|
client._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
|
|
1834
2608
|
let filled = 0;
|
|
2609
|
+
let failed = false;
|
|
1835
2610
|
try {
|
|
1836
2611
|
// Gap Fill 本身就是 Forward Pull,必须沿用协调器的完整事务:
|
|
1837
2612
|
// 原始页提交成功后由协调器负责 Piggyback/终端 ACK。
|
|
@@ -1844,11 +2619,15 @@ export class MessageDeliveryEngine {
|
|
|
1844
2619
|
client._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
|
|
1845
2620
|
}
|
|
1846
2621
|
catch (exc) {
|
|
2622
|
+
failed = true;
|
|
1847
2623
|
client._clientLog.warn(`P2P message gap fill failed: ${formatDeliveryError(exc)}`);
|
|
1848
2624
|
}
|
|
1849
2625
|
finally {
|
|
2626
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
2627
|
+
return;
|
|
1850
2628
|
client._gapFillDone.delete(dedupKey);
|
|
1851
2629
|
client._schedulePendingP2pPullIfNeeded?.(ns, 'gap-fill-complete');
|
|
2630
|
+
this.finishBackgroundPull(ns, generation, afterSeq, failed);
|
|
1852
2631
|
}
|
|
1853
2632
|
}
|
|
1854
2633
|
async fillGroupGap(groupId) {
|
|
@@ -1860,30 +2639,34 @@ export class MessageDeliveryEngine {
|
|
|
1860
2639
|
return;
|
|
1861
2640
|
const ns = `group:${groupId}`;
|
|
1862
2641
|
const afterSeq = client._seqTracker.getContiguousSeq(ns);
|
|
2642
|
+
const generation = this.captureInlineAckGeneration();
|
|
1863
2643
|
const dedupKey = `group_msg:${groupId}:${afterSeq}`;
|
|
1864
2644
|
if (client._gapFillDone.has(dedupKey))
|
|
1865
2645
|
return;
|
|
1866
2646
|
client._gapFillDone.set(dedupKey, Date.now());
|
|
2647
|
+
this.recordBackgroundPullNeed(ns, 0, true);
|
|
1867
2648
|
client._clientLog.debug(`group message gap fill start: group=${groupId}, after_seq=${afterSeq}`);
|
|
1868
2649
|
let filled = 0;
|
|
2650
|
+
let failed = false;
|
|
1869
2651
|
try {
|
|
1870
|
-
const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
|
|
2652
|
+
const operation = () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
|
|
1871
2653
|
const messages = client._rpcPipeline
|
|
1872
|
-
? await client._rpcPipeline.
|
|
2654
|
+
? await client._rpcPipeline.runPullNamespaceSerialized(client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 }), operation, true)
|
|
1873
2655
|
: await client._withBackgroundRpc(operation);
|
|
1874
2656
|
filled = messages.length;
|
|
1875
2657
|
this.prunePushedSeqs(ns);
|
|
1876
2658
|
client._clientLog.debug(`group message gap fill done: group=${groupId}, after_seq=${afterSeq}, filled=${filled}`);
|
|
1877
2659
|
}
|
|
1878
2660
|
catch (exc) {
|
|
2661
|
+
failed = true;
|
|
1879
2662
|
client._clientLog.warn(`group message gap fill failed: ${formatDeliveryError(exc)}`);
|
|
1880
2663
|
}
|
|
1881
2664
|
finally {
|
|
2665
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
2666
|
+
return;
|
|
1882
2667
|
client._gapFillDone.delete(dedupKey);
|
|
1883
|
-
if (filled > 0 && client._seqTracker.getContiguousSeq(ns) > afterSeq) {
|
|
1884
|
-
void this.fillGroupGap(groupId);
|
|
1885
|
-
}
|
|
1886
2668
|
this.schedulePendingGroupPullIfNeeded(ns, 'gap-fill-complete');
|
|
2669
|
+
this.finishBackgroundPull(ns, generation, afterSeq, failed);
|
|
1887
2670
|
}
|
|
1888
2671
|
}
|
|
1889
2672
|
scheduleRealtimeWindowForward(ns, groupId = '') {
|
|
@@ -1944,6 +2727,7 @@ export class MessageDeliveryEngine {
|
|
|
1944
2727
|
}
|
|
1945
2728
|
}
|
|
1946
2729
|
client._gapFillDone.set(dedupKey, Date.now());
|
|
2730
|
+
this.recordBackgroundPullNeed(ns, client._seqTracker.getMaxSeenSeq(ns), true);
|
|
1947
2731
|
this.syncStarted(ns);
|
|
1948
2732
|
let filled = 0;
|
|
1949
2733
|
let continuationAfterSeq = 0;
|
|
@@ -1974,10 +2758,14 @@ export class MessageDeliveryEngine {
|
|
|
1974
2758
|
}
|
|
1975
2759
|
if (!isJsonObject(result))
|
|
1976
2760
|
return;
|
|
2761
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
2762
|
+
return;
|
|
2763
|
+
this.observeBackgroundPullResponse(ns, nextAfterSeq, result);
|
|
1977
2764
|
const events = result.events;
|
|
1978
2765
|
if (!Array.isArray(events))
|
|
1979
2766
|
return;
|
|
1980
2767
|
const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
|
|
2768
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
1981
2769
|
let pageTrackerSnapshot = null;
|
|
1982
2770
|
let trackerMutated = false;
|
|
1983
2771
|
const eventObjects = events.filter((evt) => isJsonObject(evt));
|
|
@@ -1993,35 +2781,37 @@ export class MessageDeliveryEngine {
|
|
|
1993
2781
|
const eventSeqs = [];
|
|
1994
2782
|
let hasDissolvedEvent = false;
|
|
1995
2783
|
let publishedEventCount = 0;
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2784
|
+
try {
|
|
2785
|
+
await this.runApplicationPage(ns, 'pull', async () => {
|
|
2786
|
+
for (const evt of eventObjects) {
|
|
2787
|
+
const eventSeq = Number(evt.event_seq ?? 0);
|
|
2788
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0)
|
|
2789
|
+
eventSeqs.push(eventSeq);
|
|
2790
|
+
evt._from_gap_fill = true;
|
|
2791
|
+
if (evt.action === 'dissolved')
|
|
2792
|
+
hasDissolvedEvent = true;
|
|
2793
|
+
const et = String(evt.event_type ?? '');
|
|
2794
|
+
if (et !== 'group.message_created') {
|
|
2795
|
+
const cs = evt.client_signature;
|
|
2796
|
+
if (cs && isJsonObject(cs)) {
|
|
2797
|
+
if (client._shouldSkipEventSignature(evt)) {
|
|
2798
|
+
delete evt.client_signature;
|
|
2799
|
+
}
|
|
2800
|
+
else {
|
|
2801
|
+
evt._verified = await client._verifyEventSignatureAsync(evt, cs);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
|
|
2805
|
+
this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
|
|
2806
|
+
publishedEventCount += 1;
|
|
2807
|
+
}
|
|
2012
2808
|
}
|
|
2809
|
+
if (et !== 'group.message_created')
|
|
2810
|
+
filled += 1;
|
|
2013
2811
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
if (et !== 'group.message_created')
|
|
2020
|
-
filled += 1;
|
|
2021
|
-
}
|
|
2022
|
-
try {
|
|
2023
|
-
const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
|
|
2024
|
-
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, scanTarget);
|
|
2812
|
+
const scanTarget = Math.max(pageContigBefore, effectiveFloor, positiveSafeSequenceHint(result.contiguous_seq), ...eventSeqs);
|
|
2813
|
+
await this.drainOrderedMessages(ns, undefined, false, false, 'pull', undefined, scanTarget);
|
|
2814
|
+
});
|
|
2025
2815
|
if (generation !== this.captureInlineAckGeneration())
|
|
2026
2816
|
throw new Error('pull invalidated');
|
|
2027
2817
|
this.ensurePullOperationCurrent(ns);
|
|
@@ -2029,6 +2819,9 @@ export class MessageDeliveryEngine {
|
|
|
2029
2819
|
dissolved = true;
|
|
2030
2820
|
break;
|
|
2031
2821
|
}
|
|
2822
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
2823
|
+
throw new Error('pull invalidated');
|
|
2824
|
+
this.rememberCommittedSeqState(ns);
|
|
2032
2825
|
pageTrackerSnapshot = client._seqTracker.snapshotNamespace?.(ns) ?? null;
|
|
2033
2826
|
trackerMutated = true;
|
|
2034
2827
|
client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, Number.isSafeInteger(result.contiguous_seq)
|
|
@@ -2041,10 +2834,13 @@ export class MessageDeliveryEngine {
|
|
|
2041
2834
|
this.ensurePullOperationCurrent(ns);
|
|
2042
2835
|
if (generation !== this.captureInlineAckGeneration())
|
|
2043
2836
|
throw new Error('pull invalidated');
|
|
2837
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
2838
|
+
throw new Error('pull invalidated');
|
|
2044
2839
|
}
|
|
2045
2840
|
}
|
|
2046
2841
|
catch (exc) {
|
|
2047
2842
|
if (trackerMutated && generation === this.captureInlineAckGeneration() && this.isPullOperationCurrent(ns)
|
|
2843
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
2048
2844
|
&& typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
|
|
2049
2845
|
(client._seqTracker._restoreNamespaceSnapshotPreservingPush ?? client._seqTracker.restoreNamespaceSnapshot)
|
|
2050
2846
|
.call(client._seqTracker, ns, pageTrackerSnapshot);
|
|
@@ -2085,6 +2881,7 @@ export class MessageDeliveryEngine {
|
|
|
2085
2881
|
const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
|
|
2086
2882
|
const pageAdvanced = eventObjects.length > 0 && nextAfter > nextAfterSeq;
|
|
2087
2883
|
const needsMore = result.has_more === true
|
|
2884
|
+
|| positiveSafeSequenceHint(result.remaining) > 0
|
|
2088
2885
|
|| client._seqTracker.getMaxSeenSeq(ns) > nextAfter;
|
|
2089
2886
|
if (singlePage && pageAdvanced && needsMore) {
|
|
2090
2887
|
continuationAfterSeq = nextAfter;
|
|
@@ -2103,19 +2900,22 @@ export class MessageDeliveryEngine {
|
|
|
2103
2900
|
client._clientLog.warn(`group event gap fill failed: ${formatDeliveryError(exc)}`);
|
|
2104
2901
|
}
|
|
2105
2902
|
finally {
|
|
2106
|
-
|
|
2903
|
+
if (generation === this.captureInlineAckGeneration())
|
|
2904
|
+
client._gapFillDone.delete(dedupKey);
|
|
2107
2905
|
if (legacyGateToken !== null && typeof client._releasePullGate === 'function') {
|
|
2108
2906
|
client._releasePullGate(ns, legacyGateToken);
|
|
2109
2907
|
}
|
|
2908
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
2909
|
+
return;
|
|
2910
|
+
if (dissolved)
|
|
2911
|
+
this.backgroundPullNeeds.delete(ns);
|
|
2110
2912
|
if (!dissolved && singlePage && continuationAfterSeq > afterSeq) {
|
|
2111
2913
|
this.enqueueOnlineUnreadEventHint({
|
|
2112
2914
|
group_id: groupId,
|
|
2113
2915
|
_online_hint_after_event_seq: continuationAfterSeq,
|
|
2114
2916
|
});
|
|
2115
2917
|
}
|
|
2116
|
-
|
|
2117
|
-
void this.fillGroupEventGap(groupId);
|
|
2118
|
-
}
|
|
2918
|
+
this.finishBackgroundPull(ns, generation, afterSeq, failed);
|
|
2119
2919
|
if (!client._rpcPipeline)
|
|
2120
2920
|
this.syncStopped(failed ? 'aborted' : 'pull_drained');
|
|
2121
2921
|
}
|
|
@@ -2125,6 +2925,7 @@ export class MessageDeliveryEngine {
|
|
|
2125
2925
|
const groupId = String(data.group_id ?? '').trim();
|
|
2126
2926
|
if (!groupId || client._sessionOptions?.background_sync === false)
|
|
2127
2927
|
return;
|
|
2928
|
+
this.recordBackgroundPullNeed(`group_event:${groupId}`, positiveSafeSequenceHint(data.event_seq), true);
|
|
2128
2929
|
const rawAfter = Number(data._online_hint_after_event_seq ?? 0);
|
|
2129
2930
|
const afterSeq = Number.isSafeInteger(rawAfter) && rawAfter > 0 ? rawAfter : 0;
|
|
2130
2931
|
client._safeAsync(this.fillGroupEventGap(groupId, {
|
|
@@ -2518,11 +3319,11 @@ export class MessageDeliveryEngine {
|
|
|
2518
3319
|
window_mode: 'tail',
|
|
2519
3320
|
after_seq: after,
|
|
2520
3321
|
limit: pageLimit,
|
|
2521
|
-
}, true);
|
|
3322
|
+
}, true, true);
|
|
2522
3323
|
tailHead = this.syncState(ns).head;
|
|
2523
3324
|
return tailResult;
|
|
2524
3325
|
}
|
|
2525
|
-
const opts = { gateLocked: true };
|
|
3326
|
+
const opts = { gateLocked: true, background };
|
|
2526
3327
|
if (request.max_pages !== undefined)
|
|
2527
3328
|
opts.maxPages = request.max_pages;
|
|
2528
3329
|
const messages = await client._pullGroupV2(groupId, after, pageLimit, opts);
|
|
@@ -2531,7 +3332,7 @@ export class MessageDeliveryEngine {
|
|
|
2531
3332
|
if (!client._rpcPipeline)
|
|
2532
3333
|
return await invoke();
|
|
2533
3334
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, ...request });
|
|
2534
|
-
return await client._rpcPipeline.
|
|
3335
|
+
return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
|
|
2535
3336
|
};
|
|
2536
3337
|
if (order.length === 1 && order[0] === 'tail') {
|
|
2537
3338
|
result = await run({ window_mode: 'tail', after_seq: this.syncState(ns).ack, limit: pageLimit }, false);
|
|
@@ -2582,13 +3383,13 @@ export class MessageDeliveryEngine {
|
|
|
2582
3383
|
const maxPages = Math.max(1, Math.ceil(Math.max(0, throughSeq - ack) / pageLimit));
|
|
2583
3384
|
const request = { group_id: groupId, after_seq: ack, limit: pageLimit, max_pages: maxPages };
|
|
2584
3385
|
const invoke = async () => {
|
|
2585
|
-
const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages });
|
|
3386
|
+
const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages, background });
|
|
2586
3387
|
return { messages, raw_count: messages.length };
|
|
2587
3388
|
};
|
|
2588
3389
|
if (!client._rpcPipeline)
|
|
2589
3390
|
return await invoke();
|
|
2590
3391
|
const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
|
|
2591
|
-
return await client._rpcPipeline.
|
|
3392
|
+
return await client._rpcPipeline.runPullNamespaceSerialized(key, invoke, background);
|
|
2592
3393
|
}
|
|
2593
3394
|
syncState(ns) {
|
|
2594
3395
|
const tracker = this.runtime.client._seqTracker;
|
|
@@ -2929,6 +3730,7 @@ export class MessageDeliveryEngine {
|
|
|
2929
3730
|
return false;
|
|
2930
3731
|
const client = this.runtime.client;
|
|
2931
3732
|
const tracker = client._seqTracker;
|
|
3733
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
2932
3734
|
let snapshot = null;
|
|
2933
3735
|
let trackerMutated = false;
|
|
2934
3736
|
let delivered = false;
|
|
@@ -2945,6 +3747,9 @@ export class MessageDeliveryEngine {
|
|
|
2945
3747
|
delivered = true;
|
|
2946
3748
|
if (!this.inlineGenerationIsCurrent(generation))
|
|
2947
3749
|
return false;
|
|
3750
|
+
if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
3751
|
+
return false;
|
|
3752
|
+
this.rememberCommittedSeqState(ns);
|
|
2948
3753
|
snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
|
|
2949
3754
|
trackerMutated = true;
|
|
2950
3755
|
const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
|
|
@@ -2963,13 +3768,14 @@ export class MessageDeliveryEngine {
|
|
|
2963
3768
|
if (!this.inlineGenerationIsCurrent(generation))
|
|
2964
3769
|
return false;
|
|
2965
3770
|
await client._commitSeqTrackerState(ns);
|
|
2966
|
-
if (!this.inlineGenerationIsCurrent(generation))
|
|
3771
|
+
if (!this.inlineGenerationIsCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
|
|
2967
3772
|
return false;
|
|
2968
3773
|
this.markPublishedSeq(ns, seq);
|
|
2969
3774
|
return true;
|
|
2970
3775
|
}
|
|
2971
3776
|
catch (exc) {
|
|
2972
3777
|
if (trackerMutated && this.inlineGenerationIsCurrent(generation)
|
|
3778
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
2973
3779
|
&& typeof tracker.restoreNamespaceSnapshot === 'function') {
|
|
2974
3780
|
(tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
|
|
2975
3781
|
}
|
|
@@ -2984,6 +3790,8 @@ export class MessageDeliveryEngine {
|
|
|
2984
3790
|
const tracker = client._seqTracker;
|
|
2985
3791
|
if (!this.inlineGenerationIsCurrent(generation) || !this.inlineStateIsCaughtUp(ns, seq))
|
|
2986
3792
|
return false;
|
|
3793
|
+
const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
|
|
3794
|
+
this.rememberCommittedSeqState(ns);
|
|
2987
3795
|
const snapshot = typeof tracker.snapshotNamespace === 'function' ? tracker.snapshotNamespace(ns) : null;
|
|
2988
3796
|
try {
|
|
2989
3797
|
const needsPull = Boolean(tracker.onMessageSeq(ns, seq));
|
|
@@ -2998,10 +3806,11 @@ export class MessageDeliveryEngine {
|
|
|
2998
3806
|
throw new Error('inline realtime terminal cursor did not collapse after consume');
|
|
2999
3807
|
}
|
|
3000
3808
|
await client._commitSeqTrackerState(ns);
|
|
3001
|
-
return this.inlineGenerationIsCurrent(generation);
|
|
3809
|
+
return this.inlineGenerationIsCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration);
|
|
3002
3810
|
}
|
|
3003
3811
|
catch (exc) {
|
|
3004
3812
|
if (this.inlineGenerationIsCurrent(generation)
|
|
3813
|
+
&& this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
|
|
3005
3814
|
&& typeof tracker.restoreNamespaceSnapshot === 'function') {
|
|
3006
3815
|
(tracker._restoreNamespaceSnapshotPreservingPush ?? tracker.restoreNamespaceSnapshot).call(tracker, ns, snapshot);
|
|
3007
3816
|
}
|
|
@@ -3195,8 +4004,84 @@ export class MessageDeliveryEngine {
|
|
|
3195
4004
|
}
|
|
3196
4005
|
return 0;
|
|
3197
4006
|
}
|
|
4007
|
+
async onPeerPush(data, expectedVersion) {
|
|
4008
|
+
const client = this.runtime.client;
|
|
4009
|
+
if (!isJsonObject(data) || !client._aid || !this.messageTargetsCurrentInstance(data))
|
|
4010
|
+
return;
|
|
4011
|
+
const head = data;
|
|
4012
|
+
if (String(head.kind ?? '') === 'message.online_unread_hint' || !head.sender_aid) {
|
|
4013
|
+
this.refreshPendingPeers();
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
if (head.cursor_scope !== 'peer') {
|
|
4017
|
+
this.refreshPendingPeers();
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
const sender = String(head.sender_aid);
|
|
4021
|
+
const ns = await this.initializePeer(sender);
|
|
4022
|
+
const generation = this.captureInlineAckGeneration();
|
|
4023
|
+
const seq = positiveSafeSequenceHint(head.seq);
|
|
4024
|
+
if (this.isVolatileMessage(head)) {
|
|
4025
|
+
await client._v2E2EE.publishV2VolatileMessages([head], 'push', false);
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
if (!seq) {
|
|
4029
|
+
this.refreshPendingPeers();
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
client._seqTracker.updateMaxSeen(ns, seq);
|
|
4033
|
+
const row = isJsonObject(head.inline_message) ? head.inline_message : null;
|
|
4034
|
+
const prev = nonNegativeSafeSequenceHint(head.prev_peer_seq);
|
|
4035
|
+
if (row && prev !== null && prev < seq && prev <= this.syncState(ns).ack
|
|
4036
|
+
&& (!expectedVersion || this.inlineRowKind(row) === expectedVersion) && this.inlineHeadMatches(head, row, seq)) {
|
|
4037
|
+
const consumed = await client._tryRunInlineRealtime(ns, seq, () => this.runPeerPageSerialized(ns, async () => {
|
|
4038
|
+
if (generation !== this.captureInlineAckGeneration() || prev > this.syncState(ns).ack)
|
|
4039
|
+
return false;
|
|
4040
|
+
const decoded = await this.decodeInlineP2P(row);
|
|
4041
|
+
if (!decoded || !this.decodedInlineMatches(head, decoded, seq))
|
|
4042
|
+
return false;
|
|
4043
|
+
decoded.sender_aid = sender;
|
|
4044
|
+
decoded.need_render = typeof row.need_render === 'boolean' ? row.need_render : decoded.need_render !== false;
|
|
4045
|
+
const event = this.p2pAppEventForMessage(decoded);
|
|
4046
|
+
await this.publishPulledMessage(event.event, ns, seq, event.payload, false, 'inline_push');
|
|
4047
|
+
if (generation !== this.captureInlineAckGeneration())
|
|
4048
|
+
throw new Error('inline invalidated');
|
|
4049
|
+
const snapshot = client._seqTracker.snapshotNamespace(ns);
|
|
4050
|
+
try {
|
|
4051
|
+
client._seqTracker.onPullResult(ns, [{ seq }], this.syncState(ns).ack, seq);
|
|
4052
|
+
await this.commitSeqTrackerState(ns);
|
|
4053
|
+
}
|
|
4054
|
+
catch (error) {
|
|
4055
|
+
client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
|
|
4056
|
+
throw error;
|
|
4057
|
+
}
|
|
4058
|
+
await client._v2E2EE.ackPeerV2(sender, this.syncState(ns).ack);
|
|
4059
|
+
return true;
|
|
4060
|
+
})).catch((error) => {
|
|
4061
|
+
if (generation === this.captureInlineAckGeneration())
|
|
4062
|
+
this.schedulePeerRecovery(sender, seq);
|
|
4063
|
+
throw error;
|
|
4064
|
+
});
|
|
4065
|
+
if (consumed?.acquired && consumed.value)
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
if (seq <= this.syncState(ns).ack)
|
|
4069
|
+
return;
|
|
4070
|
+
if (client._sessionOptions?.background_sync !== false) {
|
|
4071
|
+
this.schedulePeerRecovery(sender, seq);
|
|
4072
|
+
}
|
|
4073
|
+
else {
|
|
4074
|
+
await client._v2E2EE.pullPeerV2(sender, { limit: _INTERNAL_MESSAGE_FLOW_LIMITS.pullPageSize, budget: { remaining: 1 },
|
|
4075
|
+
...(this.peerOwnType() === 'human' ? { window_mode: 'tail' } : {}) });
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
3198
4078
|
async onV2PushNotification(data, expectedVersion, inlineGeneration) {
|
|
3199
4079
|
const client = this.runtime.client;
|
|
4080
|
+
if (client._p2pPeerPullEnabled) {
|
|
4081
|
+
await this.onPeerPush(data, expectedVersion);
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
const syncGeneration = this.captureInlineAckGeneration();
|
|
3200
4085
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
3201
4086
|
return;
|
|
3202
4087
|
const pushData = isJsonObject(data) ? data : null;
|
|
@@ -3251,6 +4136,7 @@ export class MessageDeliveryEngine {
|
|
|
3251
4136
|
this.retryPendingP2PInlineAck(ns, pushSeq, 'covered inline push');
|
|
3252
4137
|
return;
|
|
3253
4138
|
}
|
|
4139
|
+
this.recordBackgroundPullNeed(ns, pushSeq);
|
|
3254
4140
|
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
3255
4141
|
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
3256
4142
|
client._recordPendingP2pPull(ns, pushSeq);
|
|
@@ -3267,7 +4153,9 @@ export class MessageDeliveryEngine {
|
|
|
3267
4153
|
client._clientLog.warn(`V2 P2P push auto-sync failed: ${formatDeliveryError(exc)}`);
|
|
3268
4154
|
}
|
|
3269
4155
|
finally {
|
|
3270
|
-
if (
|
|
4156
|
+
if (syncGeneration === this.captureInlineAckGeneration()
|
|
4157
|
+
&& (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration))) {
|
|
4158
|
+
this.finishBackgroundPull(ns, syncGeneration, this.syncState(ns).ack, false);
|
|
3271
4159
|
this.endRealtimeSync(ns);
|
|
3272
4160
|
client._schedulePendingP2pPullIfNeeded?.(ns, 'push-auto-pull-complete');
|
|
3273
4161
|
}
|
|
@@ -3279,6 +4167,9 @@ export class MessageDeliveryEngine {
|
|
|
3279
4167
|
if (!groupId)
|
|
3280
4168
|
return;
|
|
3281
4169
|
const ns = `group:${groupId}`;
|
|
4170
|
+
const generation = this.captureInlineAckGeneration();
|
|
4171
|
+
const afterSeq = this.syncState(ns).ack;
|
|
4172
|
+
this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq));
|
|
3282
4173
|
const targets = this.onlineUnreadHintTargets ?? new Map();
|
|
3283
4174
|
const tasks = this.onlineUnreadHintTasks ?? new Map();
|
|
3284
4175
|
this.onlineUnreadHintTargets = targets;
|
|
@@ -3286,10 +4177,13 @@ export class MessageDeliveryEngine {
|
|
|
3286
4177
|
targets.set(ns, Math.max(targets.get(ns) ?? 0, positiveSafeSequenceHint(data.seq)));
|
|
3287
4178
|
if (tasks.has(ns))
|
|
3288
4179
|
return;
|
|
4180
|
+
let attemptAfterSeq = afterSeq;
|
|
3289
4181
|
const task = Promise.resolve().then(async () => {
|
|
4182
|
+
let failed = false;
|
|
3290
4183
|
try {
|
|
3291
4184
|
while (true) {
|
|
3292
4185
|
const ackBefore = this.syncState(ns).ack;
|
|
4186
|
+
attemptAfterSeq = ackBefore;
|
|
3293
4187
|
await this.runGroupForwardRecovery(groupId, ns, 50, true, targets.get(ns) ?? 0);
|
|
3294
4188
|
const ackAfter = this.syncState(ns).ack;
|
|
3295
4189
|
if (ackAfter >= (targets.get(ns) ?? 0) || ackAfter <= ackBefore)
|
|
@@ -3297,12 +4191,14 @@ export class MessageDeliveryEngine {
|
|
|
3297
4191
|
}
|
|
3298
4192
|
}
|
|
3299
4193
|
catch (exc) {
|
|
4194
|
+
failed = true;
|
|
3300
4195
|
client._clientLog.debug(`online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
3301
4196
|
}
|
|
3302
4197
|
finally {
|
|
3303
4198
|
targets.delete(ns);
|
|
3304
4199
|
if (tasks.get(ns) === task)
|
|
3305
4200
|
tasks.delete(ns);
|
|
4201
|
+
this.finishBackgroundPull(ns, generation, attemptAfterSeq, failed);
|
|
3306
4202
|
client._rpcPipeline?.requestPullDrainedCheck?.();
|
|
3307
4203
|
}
|
|
3308
4204
|
});
|
|
@@ -3311,6 +4207,7 @@ export class MessageDeliveryEngine {
|
|
|
3311
4207
|
}
|
|
3312
4208
|
async onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
|
|
3313
4209
|
const client = this.runtime.client;
|
|
4210
|
+
const syncGeneration = this.captureInlineAckGeneration();
|
|
3314
4211
|
if (inlineGeneration !== undefined && !this.inlineGenerationIsCurrent(inlineGeneration))
|
|
3315
4212
|
return;
|
|
3316
4213
|
if (!isJsonObject(data)) {
|
|
@@ -3373,6 +4270,7 @@ export class MessageDeliveryEngine {
|
|
|
3373
4270
|
}
|
|
3374
4271
|
return;
|
|
3375
4272
|
}
|
|
4273
|
+
this.recordBackgroundPullNeed(ns, seq);
|
|
3376
4274
|
const pullActive = client._rpcPipeline?.hasPullActivity?.(ns) === true;
|
|
3377
4275
|
if (pullActive || !this.beginRealtimeSync(ns)) {
|
|
3378
4276
|
this.recordPendingGroupPull(ns, seq);
|
|
@@ -3388,7 +4286,9 @@ export class MessageDeliveryEngine {
|
|
|
3388
4286
|
return;
|
|
3389
4287
|
client._clientLog.warn(`V2 group push auto-sync failed: group=${groupId} err=${formatDeliveryError(exc)}`);
|
|
3390
4288
|
}).finally(() => {
|
|
3391
|
-
if (
|
|
4289
|
+
if (syncGeneration === this.captureInlineAckGeneration()
|
|
4290
|
+
&& (inlineGeneration === undefined || this.inlineGenerationIsCurrent(inlineGeneration))) {
|
|
4291
|
+
this.finishBackgroundPull(ns, syncGeneration, this.syncState(ns).ack, false);
|
|
3392
4292
|
this.endRealtimeSync(ns);
|
|
3393
4293
|
this.schedulePendingGroupPullIfNeeded(ns, 'push-auto-pull-complete');
|
|
3394
4294
|
}
|
|
@@ -3398,6 +4298,14 @@ export class MessageDeliveryEngine {
|
|
|
3398
4298
|
const client = this.runtime.client;
|
|
3399
4299
|
if (!client._aid)
|
|
3400
4300
|
return;
|
|
4301
|
+
this.ensurePeerContext();
|
|
4302
|
+
const rememberPeers = () => {
|
|
4303
|
+
const state = client._seqTracker.exportSyncState?.() ?? {};
|
|
4304
|
+
for (const [ns, triplet] of Object.entries(state)) {
|
|
4305
|
+
if (this.isReliableSeqNamespace(ns))
|
|
4306
|
+
this.committedSeqTrackerStates.set(ns, { ...triplet });
|
|
4307
|
+
}
|
|
4308
|
+
};
|
|
3401
4309
|
try {
|
|
3402
4310
|
const loadSync = client._tokenStore.loadAllSyncStates;
|
|
3403
4311
|
if (typeof loadSync === 'function' && typeof client._seqTracker.restoreSyncState === 'function') {
|
|
@@ -3405,6 +4313,7 @@ export class MessageDeliveryEngine {
|
|
|
3405
4313
|
if (state && Object.keys(state).length > 0) {
|
|
3406
4314
|
state = this.migrateSeqStateGroupIds(state);
|
|
3407
4315
|
client._seqTracker.restoreSyncState(state);
|
|
4316
|
+
rememberPeers();
|
|
3408
4317
|
return;
|
|
3409
4318
|
}
|
|
3410
4319
|
}
|
|
@@ -3414,6 +4323,7 @@ export class MessageDeliveryEngine {
|
|
|
3414
4323
|
if (state && Object.keys(state).length > 0) {
|
|
3415
4324
|
state = this.migrateSeqStateGroupIds(state);
|
|
3416
4325
|
client._seqTracker.restoreState(state);
|
|
4326
|
+
rememberPeers();
|
|
3417
4327
|
return;
|
|
3418
4328
|
}
|
|
3419
4329
|
}
|
|
@@ -3427,6 +4337,7 @@ export class MessageDeliveryEngine {
|
|
|
3427
4337
|
client._seqTracker.restoreSyncState(state);
|
|
3428
4338
|
else
|
|
3429
4339
|
client._seqTracker.restoreState(state);
|
|
4340
|
+
rememberPeers();
|
|
3430
4341
|
}
|
|
3431
4342
|
}
|
|
3432
4343
|
}
|
|
@@ -3447,12 +4358,21 @@ export class MessageDeliveryEngine {
|
|
|
3447
4358
|
}
|
|
3448
4359
|
enqueueOnlineUnreadP2pHint(data) {
|
|
3449
4360
|
const client = this.runtime.client;
|
|
4361
|
+
if (client._p2pPeerPullEnabled) {
|
|
4362
|
+
this.refreshPendingPeers();
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
3450
4365
|
if (!client._aid || !client._v2Session)
|
|
3451
4366
|
return;
|
|
3452
4367
|
const ns = `p2p:${client._aid}`;
|
|
4368
|
+
const generation = this.captureInlineAckGeneration();
|
|
4369
|
+
const afterSeq = this.syncState(ns).ack;
|
|
4370
|
+
this.recordBackgroundPullNeed(ns, positiveSafeSequenceHint(data.seq), true);
|
|
4371
|
+
let failed = false;
|
|
3453
4372
|
client._safeAsync(this.runP2PForwardRecovery(ns, 50, true).catch((exc) => {
|
|
4373
|
+
failed = true;
|
|
3454
4374
|
client._clientLog.debug(`P2P online unread hint background Forward failed: ns=${ns} err=${formatDeliveryError(exc)}`);
|
|
3455
|
-
}));
|
|
4375
|
+
}).finally(() => this.finishBackgroundPull(ns, generation, afterSeq, failed)));
|
|
3456
4376
|
}
|
|
3457
4377
|
async saveSeqTrackerState() {
|
|
3458
4378
|
const client = this.runtime.client;
|
|
@@ -3464,6 +4384,16 @@ export class MessageDeliveryEngine {
|
|
|
3464
4384
|
const state = typeof client._seqTracker.exportSyncState === 'function'
|
|
3465
4385
|
? client._seqTracker.exportSyncState()
|
|
3466
4386
|
: Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
|
|
4387
|
+
this.ensurePeerContext();
|
|
4388
|
+
for (const ns of Object.keys(state)) {
|
|
4389
|
+
if (!this.isReliableSeqNamespace(ns))
|
|
4390
|
+
continue;
|
|
4391
|
+
const committed = this.committedSeqTrackerStates.get(ns);
|
|
4392
|
+
if (committed)
|
|
4393
|
+
state[ns] = { ...committed };
|
|
4394
|
+
else if (ns.startsWith('p2p.peer.v1/'))
|
|
4395
|
+
delete state[ns];
|
|
4396
|
+
}
|
|
3467
4397
|
const pending = this.seqTrackerPending();
|
|
3468
4398
|
if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
|
|
3469
4399
|
const liveNamespaces = new Set(Object.keys(state));
|
|
@@ -3491,9 +4421,12 @@ export class MessageDeliveryEngine {
|
|
|
3491
4421
|
if (forceSeq !== undefined) {
|
|
3492
4422
|
client._seqTracker.forceContiguousSeq(ns, seq);
|
|
3493
4423
|
}
|
|
3494
|
-
const state =
|
|
3495
|
-
|
|
3496
|
-
|
|
4424
|
+
const state = this.committedSeqTrackerStates.get(ns) ?? (ns.startsWith('p2p.peer.v1/') ? undefined
|
|
4425
|
+
: typeof client._seqTracker.getSyncState === 'function'
|
|
4426
|
+
? client._seqTracker.getSyncState(ns)
|
|
4427
|
+
: { ack: seq, tail: seq, head: seq });
|
|
4428
|
+
if (!state)
|
|
4429
|
+
return;
|
|
3497
4430
|
this.mergeSeqTrackerPending(context, { [ns]: state });
|
|
3498
4431
|
this.scheduleSeqTrackerFlush();
|
|
3499
4432
|
}
|
|
@@ -3508,12 +4441,16 @@ export class MessageDeliveryEngine {
|
|
|
3508
4441
|
if (![state.ack, state.tail, state.head].some((value) => Number(value) > 0))
|
|
3509
4442
|
return;
|
|
3510
4443
|
const [aid, deviceId, slotId] = JSON.parse(context);
|
|
4444
|
+
const tracker = client._seqTracker;
|
|
4445
|
+
const namespaceGeneration = this.seqTrackerNamespaceGenerations.get(ns) ?? 0;
|
|
3511
4446
|
const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
|
|
3512
4447
|
try {
|
|
3513
4448
|
await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
|
|
3514
4449
|
}
|
|
3515
4450
|
catch (exc) {
|
|
3516
|
-
this.
|
|
4451
|
+
if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration) {
|
|
4452
|
+
this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
|
|
4453
|
+
}
|
|
3517
4454
|
throw exc;
|
|
3518
4455
|
}
|
|
3519
4456
|
client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
|
|
@@ -3620,7 +4557,37 @@ export class MessageDeliveryEngine {
|
|
|
3620
4557
|
}
|
|
3621
4558
|
}
|
|
3622
4559
|
async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
|
|
3623
|
-
|
|
4560
|
+
const context = JSON.stringify([aid, deviceId, slotId]);
|
|
4561
|
+
const tracker = this.runtime.client._seqTracker;
|
|
4562
|
+
const namespaceGenerations = new Map(Object.keys(state).map((ns) => [ns, this.seqTrackerNamespaceGenerations.get(ns) ?? 0]));
|
|
4563
|
+
await this.enqueueSeqTrackerWrite(async () => {
|
|
4564
|
+
const current = context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker;
|
|
4565
|
+
if (throwOnError && !current)
|
|
4566
|
+
throw new Error('seq tracker commit invalidated');
|
|
4567
|
+
if (current)
|
|
4568
|
+
this.ensurePeerContext();
|
|
4569
|
+
const writing = { ...state };
|
|
4570
|
+
if (!throwOnError && current) {
|
|
4571
|
+
for (const ns of Object.keys(writing)) {
|
|
4572
|
+
if (!this.isReliableSeqNamespace(ns))
|
|
4573
|
+
continue;
|
|
4574
|
+
const committed = this.committedSeqTrackerStates.get(ns);
|
|
4575
|
+
if (committed)
|
|
4576
|
+
writing[ns] = { ...committed };
|
|
4577
|
+
else if (ns.startsWith('p2p.peer.v1/'))
|
|
4578
|
+
delete writing[ns];
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
await this.writeSeqTrackerStateNow(aid, deviceId, slotId, writing, throwOnError);
|
|
4582
|
+
if (throwOnError && context === this.currentSeqTrackerContext() && tracker === this.runtime.client._seqTracker) {
|
|
4583
|
+
for (const [ns, triplet] of Object.entries(writing)) {
|
|
4584
|
+
if (this.isReliableSeqNamespace(ns)
|
|
4585
|
+
&& (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGenerations.get(ns)) {
|
|
4586
|
+
this.committedSeqTrackerStates.set(ns, { ...triplet });
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
});
|
|
3624
4591
|
}
|
|
3625
4592
|
enqueueSeqTrackerWrite(operation) {
|
|
3626
4593
|
const run = this.seqTrackerWriteChain.then(operation, operation);
|
|
@@ -3673,10 +4640,16 @@ export class MessageDeliveryEngine {
|
|
|
3673
4640
|
const client = this.runtime.client;
|
|
3674
4641
|
if (!client._aid || !ns)
|
|
3675
4642
|
return;
|
|
4643
|
+
this.ensurePeerContext();
|
|
3676
4644
|
const aid = client._aid;
|
|
3677
4645
|
const deviceId = client._deviceId;
|
|
3678
4646
|
const slotId = client._slotId;
|
|
3679
4647
|
const seq = client._seqTracker.getContiguousSeq(ns);
|
|
4648
|
+
if (seq <= 0) {
|
|
4649
|
+
this.seqTrackerNamespaceGenerations.set(ns, (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) + 1);
|
|
4650
|
+
this.committedSeqTrackerStates.delete(ns);
|
|
4651
|
+
this.initializedPeers.delete(ns);
|
|
4652
|
+
}
|
|
3680
4653
|
this.dropSeqTrackerPending(ns);
|
|
3681
4654
|
const saveSeq = client._tokenStore.saveSeq;
|
|
3682
4655
|
if (seq > 0 && typeof saveSeq === 'function') {
|
|
@@ -3734,7 +4707,9 @@ export class MessageDeliveryEngine {
|
|
|
3734
4707
|
if (next < 0)
|
|
3735
4708
|
next = 0;
|
|
3736
4709
|
if (ns) {
|
|
3737
|
-
|
|
4710
|
+
this.ensurePeerContext();
|
|
4711
|
+
const localAck = client._seqTracker.getContiguousSeq(ns);
|
|
4712
|
+
const committedAck = Math.min(localAck, this.committedSeqTrackerStates.get(ns)?.ack ?? localAck);
|
|
3738
4713
|
if (next > committedAck) {
|
|
3739
4714
|
client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
|
|
3740
4715
|
next = committedAck;
|
|
@@ -3791,6 +4766,8 @@ export class MessageDeliveryEngine {
|
|
|
3791
4766
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
3792
4767
|
if (!queue || queue.size === 0)
|
|
3793
4768
|
return;
|
|
4769
|
+
const page = this.currentApplicationPage();
|
|
4770
|
+
let deliveredThrough = client._seqTracker.getContiguousSeq(ns);
|
|
3794
4771
|
let delivered = false;
|
|
3795
4772
|
const rootTrigger = this.deliveryChangedTrigger(source);
|
|
3796
4773
|
const drainContexts = new Map();
|
|
@@ -3798,16 +4775,17 @@ export class MessageDeliveryEngine {
|
|
|
3798
4775
|
while (true) {
|
|
3799
4776
|
if (pullScoped)
|
|
3800
4777
|
this.ensurePullOperationCurrent(ns);
|
|
3801
|
-
const contig = client._seqTracker.getContiguousSeq(ns);
|
|
4778
|
+
const contig = page ? Math.max(deliveredThrough, client._seqTracker.getContiguousSeq(ns)) : client._seqTracker.getContiguousSeq(ns);
|
|
3802
4779
|
const ready = [...queue.keys()]
|
|
3803
|
-
.filter((seq) =>
|
|
4780
|
+
.filter((seq) => !page?.consumed.has(JSON.stringify(['ordered', ns, seq]))
|
|
4781
|
+
&& seq <= Math.max(contig, throughSeq ?? contig) && (beforeSeq === undefined || seq < beforeSeq))
|
|
3804
4782
|
.sort((a, b) => a - b);
|
|
3805
4783
|
let seq = ready[0];
|
|
3806
4784
|
if (seq === undefined) {
|
|
3807
4785
|
const nextSeq = Math.max(contig, throughSeq ?? contig) + 1;
|
|
3808
4786
|
if (beforeSeq !== undefined && nextSeq >= beforeSeq)
|
|
3809
4787
|
break;
|
|
3810
|
-
if (!queue.has(nextSeq))
|
|
4788
|
+
if (!queue.has(nextSeq) || page?.consumed.has(JSON.stringify(['ordered', ns, nextSeq])))
|
|
3811
4789
|
break;
|
|
3812
4790
|
seq = nextSeq;
|
|
3813
4791
|
}
|
|
@@ -3816,10 +4794,11 @@ export class MessageDeliveryEngine {
|
|
|
3816
4794
|
const item = queue.get(seq);
|
|
3817
4795
|
if (!item)
|
|
3818
4796
|
continue;
|
|
3819
|
-
if (
|
|
3820
|
-
|
|
4797
|
+
if (this.hasPublishedSeq(ns, seq)) {
|
|
4798
|
+
this.consumeOrderedMessage(ns, seq, queue);
|
|
3821
4799
|
client._clientLog.debug(`publish ordered drain skipped duplicate: ns=${ns}, seq=${seq}, event=${item.event}`);
|
|
3822
|
-
client._markOrderedSeqDelivered(ns, seq);
|
|
4800
|
+
this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
|
|
4801
|
+
deliveredThrough = Math.max(deliveredThrough, seq);
|
|
3823
4802
|
continue;
|
|
3824
4803
|
}
|
|
3825
4804
|
const itemSource = item.source ?? source;
|
|
@@ -3839,12 +4818,13 @@ export class MessageDeliveryEngine {
|
|
|
3839
4818
|
throw new Error('pull invalidated');
|
|
3840
4819
|
if (pullScoped)
|
|
3841
4820
|
this.ensurePullOperationCurrent(ns);
|
|
3842
|
-
|
|
4821
|
+
this.consumeOrderedMessage(ns, seq, queue);
|
|
3843
4822
|
if (item.event === 'group.changed' && isJsonObject(item.payload)
|
|
3844
4823
|
&& item.payload.action === 'dissolved')
|
|
3845
4824
|
break;
|
|
3846
4825
|
this.markPublishedSeq(ns, seq);
|
|
3847
|
-
client._markOrderedSeqDelivered(ns, seq);
|
|
4826
|
+
this.completeApplicationDelivery(() => client._markOrderedSeqDelivered(ns, seq));
|
|
4827
|
+
deliveredThrough = Math.max(deliveredThrough, seq);
|
|
3848
4828
|
if (throughSeq !== undefined)
|
|
3849
4829
|
throughSeq = Math.max(throughSeq, seq);
|
|
3850
4830
|
delivered = true;
|
|
@@ -3954,6 +4934,87 @@ export class MessageDeliveryEngine {
|
|
|
3954
4934
|
return true;
|
|
3955
4935
|
}
|
|
3956
4936
|
async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull') {
|
|
4937
|
+
const client = this.runtime.client;
|
|
4938
|
+
const seqNum = Number(seq);
|
|
4939
|
+
if (!client._p2pPeerPullEnabled || event !== 'message.received' || source === 'history'
|
|
4940
|
+
|| !Number.isSafeInteger(seqNum) || seqNum <= 0 || !payload || typeof payload !== 'object'
|
|
4941
|
+
|| payload.storage === 'volatile') {
|
|
4942
|
+
return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
|
|
4943
|
+
}
|
|
4944
|
+
const context = this.ensurePeerContext();
|
|
4945
|
+
const generation = this.captureInlineAckGeneration();
|
|
4946
|
+
const ensureCurrent = () => {
|
|
4947
|
+
if (context !== this.ensurePeerContext() || generation !== this.captureInlineAckGeneration())
|
|
4948
|
+
throw new Error('delivery invalidated');
|
|
4949
|
+
if (this.isPullScopedSource(source))
|
|
4950
|
+
this.ensurePullOperationCurrent(ns);
|
|
4951
|
+
};
|
|
4952
|
+
const value = payload;
|
|
4953
|
+
const sender = String(value.sender_aid ?? value.from_aid ?? value.from ?? this.peerSender(ns));
|
|
4954
|
+
if (!sender)
|
|
4955
|
+
return this.publishPulledMessageInternal(event, ns, seq, payload, persist, source);
|
|
4956
|
+
const messageId = String(value.message_id ?? '');
|
|
4957
|
+
const key = JSON.stringify([context, event, seqNum]);
|
|
4958
|
+
const page = this.currentApplicationPage();
|
|
4959
|
+
const bindingMatches = (receipt) => receipt.sender_aid === sender
|
|
4960
|
+
&& (!receipt.message_id || !messageId || receipt.message_id === messageId);
|
|
4961
|
+
const local = page?.bindings.get(key);
|
|
4962
|
+
if (local) {
|
|
4963
|
+
if (!bindingMatches(local))
|
|
4964
|
+
throw new Error('P2P delivery receipt binding conflict');
|
|
4965
|
+
if (messageId && !local.message_id)
|
|
4966
|
+
local.message_id = messageId;
|
|
4967
|
+
return false;
|
|
4968
|
+
}
|
|
4969
|
+
const pending = this.deliveryInflight.get(key);
|
|
4970
|
+
if (pending) {
|
|
4971
|
+
if (client._dispatcher._isApplicationHandler?.())
|
|
4972
|
+
throw new Error('delivery cannot wait inside an application handler');
|
|
4973
|
+
await pending;
|
|
4974
|
+
ensureCurrent();
|
|
4975
|
+
const settled = this.settledDeliveries.get(key);
|
|
4976
|
+
if (settled && !bindingMatches(settled))
|
|
4977
|
+
throw new Error('P2P delivery receipt binding conflict');
|
|
4978
|
+
if (!messageId || settled?.message_id)
|
|
4979
|
+
return false;
|
|
4980
|
+
}
|
|
4981
|
+
const operation = Promise.resolve().then(async () => {
|
|
4982
|
+
ensureCurrent();
|
|
4983
|
+
let settled = this.settledDeliveries.get(key);
|
|
4984
|
+
if (settled && !bindingMatches(settled))
|
|
4985
|
+
throw new Error('P2P delivery receipt binding conflict');
|
|
4986
|
+
if (settled && messageId && !settled.message_id) {
|
|
4987
|
+
settled = { ...settled, message_id: messageId };
|
|
4988
|
+
}
|
|
4989
|
+
let published = false;
|
|
4990
|
+
if (!settled) {
|
|
4991
|
+
if (!this.isSelfSentP2PMessage(payload)) {
|
|
4992
|
+
await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source));
|
|
4993
|
+
published = true;
|
|
4994
|
+
}
|
|
4995
|
+
ensureCurrent();
|
|
4996
|
+
settled = { sender_aid: sender, ...(messageId ? { message_id: messageId } : {}) };
|
|
4997
|
+
}
|
|
4998
|
+
ensureCurrent();
|
|
4999
|
+
const binding = settled;
|
|
5000
|
+
page?.bindings.set(key, binding);
|
|
5001
|
+
this.completeApplicationDelivery(() => {
|
|
5002
|
+
this.settledDeliveries.set(key, binding);
|
|
5003
|
+
while (this.settledDeliveries.size > PUSHED_SEQS_LIMIT) {
|
|
5004
|
+
const first = this.settledDeliveries.keys().next().value;
|
|
5005
|
+
this.settledDeliveries.delete(first);
|
|
5006
|
+
}
|
|
5007
|
+
});
|
|
5008
|
+
this.markPublishedSeq(ns, seqNum);
|
|
5009
|
+
return published;
|
|
5010
|
+
});
|
|
5011
|
+
const completion = page ? operation.then(async (result) => { await page.completion; return result; }) : operation;
|
|
5012
|
+
this.deliveryInflight.set(key, completion);
|
|
5013
|
+
void completion.finally(() => { if (this.deliveryInflight.get(key) === completion)
|
|
5014
|
+
this.deliveryInflight.delete(key); }).catch(() => { });
|
|
5015
|
+
return await operation;
|
|
5016
|
+
}
|
|
5017
|
+
async publishPulledMessageInternal(event, ns, seq, payload, persist = true, source = 'pull') {
|
|
3957
5018
|
const client = this.runtime.client;
|
|
3958
5019
|
const generation = this.captureInlineAckGeneration();
|
|
3959
5020
|
const pullScoped = this.isPullScopedSource(source);
|
|
@@ -3976,9 +5037,9 @@ export class MessageDeliveryEngine {
|
|
|
3976
5037
|
return true;
|
|
3977
5038
|
}
|
|
3978
5039
|
const queue = client._pendingOrderedMsgs.get(ns);
|
|
3979
|
-
if (
|
|
5040
|
+
if (this.hasPublishedSeq(ns, seqNum)) {
|
|
3980
5041
|
client._clientLog.debug(`publish pulled skipped duplicate: event=${event}, ns=${ns}, seq=${seqNum}`);
|
|
3981
|
-
|
|
5042
|
+
this.consumeOrderedMessage(ns, seqNum, queue);
|
|
3982
5043
|
if (queue && queue.size === 0)
|
|
3983
5044
|
client._pendingOrderedMsgs.delete(ns);
|
|
3984
5045
|
return false;
|
|
@@ -3997,7 +5058,7 @@ export class MessageDeliveryEngine {
|
|
|
3997
5058
|
throw new Error('pull invalidated');
|
|
3998
5059
|
if (pullScoped)
|
|
3999
5060
|
this.ensurePullOperationCurrent(ns);
|
|
4000
|
-
|
|
5061
|
+
this.consumeOrderedMessage(ns, seqNum, queue);
|
|
4001
5062
|
if (queue && queue.size === 0)
|
|
4002
5063
|
client._pendingOrderedMsgs.delete(ns);
|
|
4003
5064
|
this.markPublishedSeq(ns, seqNum);
|
|
@@ -4014,7 +5075,7 @@ export class MessageDeliveryEngine {
|
|
|
4014
5075
|
throw new Error('pull invalidated');
|
|
4015
5076
|
if (pullScoped)
|
|
4016
5077
|
this.ensurePullOperationCurrent(ns);
|
|
4017
|
-
|
|
5078
|
+
this.consumeOrderedMessage(ns, seqNum, queue);
|
|
4018
5079
|
if (queue && queue.size === 0)
|
|
4019
5080
|
client._pendingOrderedMsgs.delete(ns);
|
|
4020
5081
|
this.markPublishedSeq(ns, seqNum);
|