@agentunion/fastaun-browser 0.5.20 → 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.
Files changed (130) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  5. 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
  6. 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
  7. 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
  8. 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
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. 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
  14. 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
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. 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
  25. 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
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. 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 +106 -43
  30. 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 +180 -9
  31. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. 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 +1 -1
  34. 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 +2 -2
  35. package/dist/agent-md-schema.d.ts +1 -0
  36. package/dist/agent-md-schema.d.ts.map +1 -1
  37. package/dist/agent-md-schema.js +5 -4
  38. package/dist/agent-md-schema.js.map +1 -1
  39. package/dist/agent-md.d.ts +25 -1
  40. package/dist/agent-md.d.ts.map +1 -1
  41. package/dist/agent-md.js +148 -71
  42. package/dist/agent-md.js.map +1 -1
  43. package/dist/aid-store.d.ts +13 -0
  44. package/dist/aid-store.d.ts.map +1 -1
  45. package/dist/aid-store.js +55 -8
  46. package/dist/aid-store.js.map +1 -1
  47. package/dist/aid.d.ts.map +1 -1
  48. package/dist/aid.js +2 -1
  49. package/dist/aid.js.map +1 -1
  50. package/dist/auth.d.ts.map +1 -1
  51. package/dist/auth.js +4 -0
  52. package/dist/auth.js.map +1 -1
  53. package/dist/bundle.js +4981 -1219
  54. package/dist/cert-utils.d.ts +1 -0
  55. package/dist/cert-utils.d.ts.map +1 -1
  56. package/dist/cert-utils.js +21 -0
  57. package/dist/cert-utils.js.map +1 -1
  58. package/dist/client/delivery.d.ts +101 -16
  59. package/dist/client/delivery.d.ts.map +1 -1
  60. package/dist/client/delivery.js +1172 -160
  61. package/dist/client/delivery.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +4 -1
  63. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  64. package/dist/client/rpc-pipeline.js +122 -48
  65. package/dist/client/rpc-pipeline.js.map +1 -1
  66. package/dist/client/v2-e2ee.d.ts +22 -3
  67. package/dist/client/v2-e2ee.d.ts.map +1 -1
  68. package/dist/client/v2-e2ee.js +1258 -649
  69. package/dist/client/v2-e2ee.js.map +1 -1
  70. package/dist/client.d.ts +23 -2
  71. package/dist/client.d.ts.map +1 -1
  72. package/dist/client.js +391 -87
  73. package/dist/client.js.map +1 -1
  74. package/dist/config.d.ts +4 -0
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +1 -0
  77. package/dist/config.js.map +1 -1
  78. package/dist/events.d.ts +19 -1
  79. package/dist/events.d.ts.map +1 -1
  80. package/dist/events.js +146 -23
  81. package/dist/events.js.map +1 -1
  82. package/dist/facades.d.ts +42 -1
  83. package/dist/facades.d.ts.map +1 -1
  84. package/dist/facades.js +363 -7
  85. package/dist/facades.js.map +1 -1
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +1 -1
  89. package/dist/index.js.map +1 -1
  90. package/dist/keystore/index.d.ts +14 -1
  91. package/dist/keystore/index.d.ts.map +1 -1
  92. package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
  93. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  94. package/dist/keystore/indexeddb-identity-store.js +30 -13
  95. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  96. package/dist/keystore/indexeddb-shared.d.ts +19 -2
  97. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  98. package/dist/keystore/indexeddb-shared.js +193 -6
  99. package/dist/keystore/indexeddb-shared.js.map +1 -1
  100. package/dist/keystore/indexeddb-token-store.d.ts +15 -2
  101. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  102. package/dist/keystore/indexeddb-token-store.js +308 -27
  103. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  104. package/dist/org-protocol.d.ts +45 -0
  105. package/dist/org-protocol.d.ts.map +1 -1
  106. package/dist/org-protocol.js +757 -11
  107. package/dist/org-protocol.js.map +1 -1
  108. package/dist/ownership-v21.d.ts +21 -4
  109. package/dist/ownership-v21.d.ts.map +1 -1
  110. package/dist/ownership-v21.js +49 -16
  111. package/dist/ownership-v21.js.map +1 -1
  112. package/dist/ownership-workflow.d.ts +30 -0
  113. package/dist/ownership-workflow.d.ts.map +1 -1
  114. package/dist/ownership-workflow.js +412 -54
  115. package/dist/ownership-workflow.js.map +1 -1
  116. package/dist/seq-tracker.d.ts +2 -0
  117. package/dist/seq-tracker.d.ts.map +1 -1
  118. package/dist/seq-tracker.js +3 -2
  119. package/dist/seq-tracker.js.map +1 -1
  120. package/dist/tools/cross-sdk-agent.d.ts +60 -3
  121. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  122. package/dist/tools/cross-sdk-agent.js +433 -37
  123. package/dist/tools/cross-sdk-agent.js.map +1 -1
  124. package/dist/transport.d.ts +3 -0
  125. package/dist/transport.d.ts.map +1 -1
  126. package/dist/transport.js +62 -30
  127. package/dist/transport.js.map +1 -1
  128. package/dist/version.d.ts +1 -1
  129. package/dist/version.js +1 -1
  130. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { slotIsolationKey } from '../config.js';
2
+ import { StateError, ValidationError } from '../errors.js';
2
3
  import { normalizeGroupAid } from '../group-id.js';
3
4
  import { SeqTracker } from '../seq-tracker.js';
4
5
  import { ConnectionState, isJsonObject } from '../types.js';
@@ -16,7 +17,7 @@ const APP_MESSAGE_ENVELOPE_KEYS = [
16
17
  'module_id', 'message_type', 'type', 'kind', 'version',
17
18
  'from', 'from_aid', 'sender_aid', 'to', 'to_aid', 'group_id',
18
19
  'timestamp', 'created_at', 'encrypted',
19
- 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode',
20
+ 'context', 'protected_headers', 'headers', 'payload_type', 'mention_mode', 'need_render',
20
21
  ];
21
22
  const RECALL_PAYLOAD_KEYS = [
22
23
  'message_ids', 'target_message_seqs',
@@ -36,6 +37,11 @@ const APP_GROUP_EVENT_ENVELOPE_KEYS = [
36
37
  'actor_aid', 'sender_aid', 'member_aid', 'target_aid', 'operator_aid',
37
38
  'created_at', 'timestamp', 't_server', 'status', 'device_id',
38
39
  ];
40
+ const APPLICATION_EVENT_FAMILIES = {
41
+ 'message.received': 'message.batch', 'message.recalled': 'message.batch',
42
+ 'group.message_created': 'group.message_batch', 'group.message_recalled': 'group.message_batch',
43
+ 'group.changed': 'group.event_batch',
44
+ };
39
45
  const MAX_INLINE_PENDING_ACK_NAMESPACES = 4096;
40
46
  function formatDeliveryError(error) {
41
47
  return error instanceof Error ? error : String(error);
@@ -120,6 +126,440 @@ function p2pAppEventFromPlainPullMessage(message) {
120
126
  return { event: 'message.received', payload: message };
121
127
  }
122
128
  export class MessageDeliveryEngine {
129
+ peerBootstrapContext = '';
130
+ peerTracker = null;
131
+ peerTrackerGeneration = 0;
132
+ peerBootstrapFloor = null;
133
+ peerInitializations = new Map();
134
+ peerInitialized = new Set();
135
+ committedSeqTrackerStates = new Map();
136
+ seqTrackerNamespaceGenerations = new Map();
137
+ peerDirectoryRequests = new Map();
138
+ peerRecoveries = new Map();
139
+ peerPendingSeqHints = new Map();
140
+ peerPendingConfirmedThrough = new Map();
141
+ peerRecoveryTimer = null;
142
+ peerVolatileCursors = new Map();
143
+ peerDirectoryDirty = false;
144
+ peerDirectoryRefreshing = false;
145
+ p2pDeliveryInflight = new Map();
146
+ p2pDeliveryCompleted = new Map();
147
+ peerNamespace(sender) {
148
+ return `p2p.peer.v1/${btoa(String.fromCharCode(...new TextEncoder().encode(sender))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}`;
149
+ }
150
+ peerSender(ns) {
151
+ if (!ns.startsWith('p2p.peer.v1/'))
152
+ return '';
153
+ try {
154
+ return new TextDecoder().decode(Uint8Array.from(atob(ns.slice(12).replace(/-/g, '+').replace(/_/g, '/')), (value) => value.charCodeAt(0)));
155
+ }
156
+ catch {
157
+ return '';
158
+ }
159
+ }
160
+ peerContext() {
161
+ const client = this.runtime.client;
162
+ const context = JSON.stringify([client._aid, client._deviceId, slotIsolationKey(client._slotId)]);
163
+ if (context !== this.peerBootstrapContext || this.peerTracker !== client._seqTracker) {
164
+ this.peerBootstrapContext = context;
165
+ this.peerTracker = client._seqTracker;
166
+ this.peerTrackerGeneration += 1;
167
+ this.peerBootstrapFloor = null;
168
+ this.peerInitializations.clear();
169
+ this.peerInitialized.clear();
170
+ this.committedSeqTrackerStates.clear();
171
+ this.seqTrackerNamespaceGenerations.clear();
172
+ this.peerRecoveries.clear();
173
+ this.peerPendingSeqHints.clear();
174
+ this.peerPendingConfirmedThrough.clear();
175
+ this.peerVolatileCursors.clear();
176
+ }
177
+ return JSON.stringify([context, this.peerTrackerGeneration]);
178
+ }
179
+ isPeerInitialized(ns) {
180
+ if (!this.peerInitialized.has(ns))
181
+ return false;
182
+ if (this.runtime.client._seqTracker.snapshotNamespace(ns) !== null)
183
+ return true;
184
+ this.peerInitialized.delete(ns);
185
+ this.committedSeqTrackerStates.delete(ns);
186
+ return false;
187
+ }
188
+ async initializePeer(sender, initial) {
189
+ const client = this.runtime.client;
190
+ const context = this.peerContext();
191
+ const scope = [client._aid, client._deviceId, client._slotId];
192
+ if (!sender)
193
+ throw new ValidationError('sender_aid is required');
194
+ const ns = this.peerNamespace(sender);
195
+ if (this.isPeerInitialized(ns))
196
+ return;
197
+ const restored = client._seqTracker.exportSyncState()[ns];
198
+ if (restored && (restored.ack > 0 || restored.tail > 0 || restored.head > 0)) {
199
+ this.committedSeqTrackerStates.set(ns, { ...restored });
200
+ this.peerInitialized.add(ns);
201
+ return;
202
+ }
203
+ if (initial === undefined && this.peerBootstrapFloor !== null) {
204
+ let pageToken = '';
205
+ const visited = new Set();
206
+ do {
207
+ const directory = await this.peers({ filter: 'all', ...(pageToken ? { page_token: pageToken } : {}) }, true);
208
+ if (this.peerContext() !== context)
209
+ throw new StateError('peer initialization invalidated');
210
+ if (this.isPeerInitialized(ns))
211
+ return;
212
+ if (directory.has_more !== true)
213
+ break;
214
+ const next = directory.next_page_token;
215
+ if (typeof next !== 'string' || !next || visited.has(next))
216
+ throw new ValidationError('invalid peer directory pagination');
217
+ visited.add(next);
218
+ pageToken = next;
219
+ } while (true);
220
+ }
221
+ const existing = this.peerInitializations.get(sender);
222
+ if (existing)
223
+ return await existing;
224
+ const task = (async () => {
225
+ const ack = initial ?? this.peerBootstrapFloor;
226
+ if (!Number.isSafeInteger(ack) || Number(ack) < 0)
227
+ throw new StateError('peer bootstrap is not ready');
228
+ const state = { ack: Number(ack), tail: Number(ack), head: Number(ack) };
229
+ if (typeof client._tokenStore.saveSyncStates !== 'function')
230
+ throw new StateError('peer cursor persistence unavailable');
231
+ await this.enqueueSeqTrackerWrite(async () => {
232
+ if (this.peerContext() !== context)
233
+ throw new StateError('peer initialization invalidated');
234
+ await client._tokenStore.saveSyncStates(...scope, { [ns]: state });
235
+ if (this.peerContext() === context)
236
+ this.committedSeqTrackerStates.set(ns, { ...state });
237
+ });
238
+ if (this.peerContext() !== context)
239
+ throw new StateError('peer initialization invalidated');
240
+ client._seqTracker.forceContiguousSeq(ns, Number(ack));
241
+ client._seqTracker.getSyncState(ns);
242
+ this.peerInitialized.add(ns);
243
+ })();
244
+ this.peerInitializations.set(sender, task);
245
+ try {
246
+ await task;
247
+ }
248
+ finally {
249
+ if (this.peerInitializations.get(sender) === task)
250
+ this.peerInitializations.delete(sender);
251
+ }
252
+ }
253
+ peerDurableAck(ns) {
254
+ return this.committedSeqTrackerAck(ns);
255
+ }
256
+ rememberCommittedSeqState(ns) {
257
+ this.peerContext();
258
+ if (!this.isReliableSeqNamespace(ns) || this.committedSeqTrackerStates.has(ns))
259
+ return;
260
+ this.committedSeqTrackerStates.set(ns, { ...this.syncState(ns) });
261
+ }
262
+ committedSeqTrackerAck(ns) {
263
+ this.peerContext();
264
+ return this.committedSeqTrackerStates.get(ns)?.ack ?? 0;
265
+ }
266
+ captureSeqNamespaceGeneration(ns) {
267
+ return JSON.stringify([this.peerContext(), this.seqTrackerNamespaceGenerations.get(ns) ?? 0]);
268
+ }
269
+ isSeqNamespaceGenerationCurrent(ns, generation) {
270
+ return this.captureSeqNamespaceGeneration(ns) === generation;
271
+ }
272
+ isReliableSeqNamespace(ns) {
273
+ return ns.startsWith('p2p.peer.v1/') || ns.startsWith('group:') || ns.startsWith('group_event:');
274
+ }
275
+ async peers(params = {}, background = false) {
276
+ const client = this.runtime.client;
277
+ const context = this.peerContext();
278
+ const generation = this.captureInlineGeneration();
279
+ const filter = String(params.filter ?? 'pending');
280
+ const key = JSON.stringify([context, filter, params.page_token ?? '', params.limit ?? null]);
281
+ const existing = this.peerDirectoryRequests.get(key);
282
+ if (existing) {
283
+ if (!background)
284
+ existing.application = true;
285
+ return await existing.promise;
286
+ }
287
+ const request = { application: !background, promise: Promise.resolve({}) };
288
+ request.promise = (async () => {
289
+ const result = await client._callRawV2Rpc('message.peers', { ...params, cursor_scope: 'peer', filter, ...(background ? { _rpc_background: true } : { _rpc_foreground: true }) });
290
+ if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
291
+ throw new StateError('peer directory invalidated');
292
+ if (result.catalog_ready !== true || !Array.isArray(result.peers) || (filter === 'pending' && result.has_more === true))
293
+ throw new StateError('peer catalog is not ready', { retryable: true });
294
+ if (result.cursor_scope !== 'peer' || (result.filter !== undefined && result.filter !== filter))
295
+ throw new ValidationError('peer directory scope mismatch');
296
+ for (const peer of result.peers) {
297
+ if (!peer || typeof peer !== 'object' || typeof peer.sender_aid !== 'string' || !peer.sender_aid.trim())
298
+ throw new ValidationError('invalid peer sender_aid');
299
+ const initial = peer.initial_after_seq ?? peer.server_ack_seq ?? result.bootstrap_floor_seq;
300
+ if (!Number.isSafeInteger(initial) || Number(initial) < 0)
301
+ throw new ValidationError('invalid peer initial_after_seq');
302
+ }
303
+ const floor = result.bootstrap_floor_seq;
304
+ if (!Number.isSafeInteger(floor) || Number(floor) < 0)
305
+ throw new ValidationError('invalid bootstrap_floor_seq');
306
+ if (this.peerBootstrapFloor === null) {
307
+ if (typeof client._tokenStore.updateInstanceState !== 'function')
308
+ throw new StateError('peer bootstrap persistence unavailable');
309
+ await client._tokenStore.updateInstanceState(client._aid, client._deviceId, client._slotId, (state) => {
310
+ const saved = state.p2p_peer_bootstrap_floor_seq;
311
+ if (Number.isSafeInteger(saved) && saved !== floor)
312
+ throw new StateError('peer bootstrap changed');
313
+ state.p2p_peer_bootstrap_floor_seq = floor;
314
+ });
315
+ if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
316
+ throw new StateError('peer bootstrap invalidated');
317
+ this.peerBootstrapFloor = Number(floor);
318
+ }
319
+ else if (this.peerBootstrapFloor !== floor)
320
+ throw new StateError('peer bootstrap changed');
321
+ const rows = result.peers;
322
+ for (const peer of rows)
323
+ await this.initializePeer(String(peer.sender_aid ?? ''), Number(peer.initial_after_seq ?? peer.server_ack_seq ?? floor));
324
+ if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
325
+ throw new StateError('peer directory invalidated');
326
+ for (const peer of rows) {
327
+ const ns = this.peerNamespace(String(peer.sender_aid));
328
+ const next = peer.next_pending_seq;
329
+ const confirmed = Math.max(this.peerPendingConfirmedThrough.get(ns) ?? 0, Number(peer.server_ack_seq ?? peer.initial_after_seq ?? floor));
330
+ this.peerPendingConfirmedThrough.set(ns, confirmed);
331
+ if (typeof next === 'number' && Number.isSafeInteger(next) && next >= confirmed)
332
+ this.peerPendingSeqHints.set(ns, Math.max(this.peerPendingSeqHints.get(ns) ?? next, next));
333
+ else if ((this.peerPendingSeqHints.get(ns) ?? Infinity) <= confirmed)
334
+ this.peerPendingSeqHints.delete(ns);
335
+ }
336
+ if (request.application || rows.length > 0) {
337
+ client._dispatcher.enqueue('message.peers_snapshot', {
338
+ aid: client._aid, device_id: client._deviceId, slot_id: client._slotId, cursor_scope: 'peer', filter,
339
+ trigger: request.application ? 'application' : 'background', catalog_revision: result.catalog_revision,
340
+ peers: rows.map((peer) => ({ ...peer })), peer_count: rows.length, bootstrap_floor_seq: floor,
341
+ ...(result.pending_count_estimate === undefined ? {} : { pending_count_estimate: result.pending_count_estimate }),
342
+ ...(filter === 'all' ? { has_more: result.has_more === true, next_page_token: result.next_page_token } : {}),
343
+ }, () => this.peerContext() === context && this.isInlineGenerationCurrent(generation));
344
+ }
345
+ return result;
346
+ })();
347
+ this.peerDirectoryRequests.set(key, request);
348
+ try {
349
+ return await request.promise;
350
+ }
351
+ finally {
352
+ if (this.peerDirectoryRequests.get(key) === request)
353
+ this.peerDirectoryRequests.delete(key);
354
+ }
355
+ }
356
+ async pullPeers(params = {}, background = false) {
357
+ const context = this.peerContext();
358
+ const generation = this.captureInlineGeneration();
359
+ const sender = String(params.sender_aid ?? params.peer_aid ?? '').trim();
360
+ if (!sender && ('after_seq' in params || 'after_volatile_cursor' in params || params.force === true || 'window_mode' in params))
361
+ throw new ValidationError('sender_aid is required for an explicit cursor or window');
362
+ const pending = sender && this.peerBootstrapFloor !== null && this.peerInitialized.has(this.peerNamespace(sender))
363
+ ? [] : (await this.peers({}, background)).peers;
364
+ if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
365
+ throw new StateError('pull invalidated');
366
+ const peers = sender ? [{ sender_aid: sender }] : background ? this.orderBackgroundPeers(pending) : pending;
367
+ const budget = { remaining: Math.max(1, Math.trunc(Number(params.max_pages ?? 100) || 100)) };
368
+ const results = await Promise.all(peers.map(async (peer) => {
369
+ const aid = String(peer.sender_aid ?? '');
370
+ await this.initializePeer(aid);
371
+ try {
372
+ return await this.runtime.client._v2E2EE.pullPeerV2(aid, params, background, budget);
373
+ }
374
+ catch (error) {
375
+ if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
376
+ throw new StateError('pull invalidated');
377
+ this.schedulePeerRecovery(aid, Number(peer.head_seq ?? 0), peer.volatile_has_more === true);
378
+ return { sender_aid: aid, messages: [], complete: false, error: String(error) };
379
+ }
380
+ }));
381
+ if (this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
382
+ throw new StateError('pull invalidated');
383
+ return { cursor_scope: 'peer', peers: results, messages: results.flatMap((peer) => peer.messages ?? []), partial: results.some((peer) => !peer.complete) };
384
+ }
385
+ schedulePeerRecovery(sender, head = 0, volatile = false) {
386
+ const client = this.runtime.client;
387
+ if (!sender || client._sessionOptions?.background_sync === false || client._closing)
388
+ return;
389
+ this.peerContext();
390
+ const work = this.peerRecoveries.get(sender) ?? { head: 0, volatile: false, running: false, retryAt: 0, pending: false };
391
+ work.head = Math.max(work.head, head);
392
+ work.volatile ||= volatile;
393
+ work.pending = true;
394
+ this.peerRecoveries.set(sender, work);
395
+ this.requestPeerRecovery();
396
+ }
397
+ requestPeerRecovery() {
398
+ const client = this.runtime.client;
399
+ if (!client._p2pPeerPullEnabled || client._sessionOptions?.background_sync === false || client._closing || client.state !== ConnectionState.READY)
400
+ return;
401
+ for (const [sender, work] of this.peerRecoveries) {
402
+ if (work.running)
403
+ continue;
404
+ if (work.retryAt > Date.now()) {
405
+ if (this.peerRecoveryTimer === null)
406
+ this.peerRecoveryTimer = setTimeout(() => { this.peerRecoveryTimer = null; this.requestBackgroundPullRecovery(); }, Math.max(1, work.retryAt - Date.now()));
407
+ continue;
408
+ }
409
+ work.running = true;
410
+ work.pending = false;
411
+ const generation = this.captureInlineGeneration();
412
+ client._safeAsync((async () => {
413
+ try {
414
+ if (this.peerBootstrapFloor === null)
415
+ await this.peers({}, true);
416
+ await this.initializePeer(sender);
417
+ const result = await client._v2E2EE.pullPeerV2(sender, {}, true, { remaining: 100 }, { retainMessages: false });
418
+ if (!this.isInlineGenerationCurrent(generation))
419
+ return;
420
+ if (result.complete && Number(result.ack_seq ?? 0) >= work.head && !work.pending)
421
+ this.peerRecoveries.delete(sender);
422
+ else
423
+ work.retryAt = result.complete && !work.pending ? Date.now() + 1000 : 0;
424
+ }
425
+ catch {
426
+ work.retryAt = Date.now() + 1000;
427
+ }
428
+ finally {
429
+ work.running = false;
430
+ if (this.isInlineGenerationCurrent(generation))
431
+ queueMicrotask(() => this.requestPeerRecovery());
432
+ }
433
+ })());
434
+ }
435
+ }
436
+ async refreshPeerDirectory() {
437
+ const result = await this.peers({}, true);
438
+ for (const peer of this.orderBackgroundPeers(result.peers))
439
+ this.schedulePeerRecovery(String(peer.sender_aid ?? ''), Number(peer.head_seq ?? 0), peer.volatile_has_more === true);
440
+ }
441
+ orderBackgroundPeers(peers) {
442
+ const hint = (peer) => this.peerBackgroundPullPriority(this.peerNamespace(String(peer.sender_aid)));
443
+ return [...peers].sort((left, right) => hint(left) - hint(right));
444
+ }
445
+ peerBackgroundPullPriority(ns) {
446
+ this.peerContext();
447
+ if (!ns.startsWith('p2p.peer.v1/'))
448
+ return undefined;
449
+ return this.peerPendingSeqHints.get(ns) ?? Math.max(this.peerPendingConfirmedThrough.get(ns) ?? 0, this.runtime.client._seqTracker.getContiguousSeq(ns));
450
+ }
451
+ consumePeerBackgroundPullPriority(ns, covered, next) {
452
+ this.peerContext();
453
+ if (covered < (this.peerPendingConfirmedThrough.get(ns) ?? 0))
454
+ return;
455
+ this.peerPendingConfirmedThrough.set(ns, Math.max(this.peerPendingConfirmedThrough.get(ns) ?? 0, covered));
456
+ if ((this.peerPendingSeqHints.get(ns) ?? Infinity) <= covered)
457
+ this.peerPendingSeqHints.delete(ns);
458
+ if (next !== undefined)
459
+ this.peerPendingSeqHints.set(ns, Math.max(this.peerPendingSeqHints.get(ns) ?? next, next));
460
+ }
461
+ peerVolatileCursor(sender) { this.peerContext(); return this.peerVolatileCursors.get(sender) ?? ''; }
462
+ setPeerVolatileCursor(sender, cursor, page) { this.deferApplicationAfterDelivery(page, () => { this.peerContext(); this.peerVolatileCursors.set(sender, cursor); }); }
463
+ async ownPeerType() {
464
+ return String(await this.runtime.client._agentMdManager?.ownType?.() ?? '');
465
+ }
466
+ requestPeerDirectoryRefresh() {
467
+ this.peerDirectoryDirty = true;
468
+ const client = this.runtime.client;
469
+ if (this.peerDirectoryRefreshing || client._sessionOptions?.background_sync === false || client._closing || client.state !== ConnectionState.READY)
470
+ return;
471
+ this.peerDirectoryRefreshing = true;
472
+ const generation = this.captureInlineGeneration();
473
+ client._safeAsync((async () => {
474
+ try {
475
+ do {
476
+ this.peerDirectoryDirty = false;
477
+ await this.refreshPeerDirectory();
478
+ } while (this.peerDirectoryDirty && this.isInlineGenerationCurrent(generation));
479
+ }
480
+ catch {
481
+ this.peerDirectoryDirty = true;
482
+ if (this.peerRecoveryTimer === null)
483
+ this.peerRecoveryTimer = setTimeout(() => {
484
+ this.peerRecoveryTimer = null;
485
+ if (this.isInlineGenerationCurrent(generation))
486
+ this.requestBackgroundPullRecovery();
487
+ }, 1000);
488
+ }
489
+ finally {
490
+ this.peerDirectoryRefreshing = false;
491
+ }
492
+ })());
493
+ }
494
+ handlePeerNotification(data) {
495
+ const client = this.runtime.client;
496
+ if (!isJsonObject(data) || !this.messageTargetsCurrentInstance(data))
497
+ return;
498
+ const sender = String(data.sender_aid ?? data.from_aid ?? data.from ?? '').trim();
499
+ if (String(data.kind ?? '') === 'message.online_unread_hint' || !sender || data.cursor_scope !== 'peer') {
500
+ this.requestPeerDirectoryRefresh();
501
+ return;
502
+ }
503
+ const seq = positiveSafeSequenceHint(data.seq);
504
+ const ns = this.peerNamespace(sender);
505
+ this.isPeerInitialized(ns);
506
+ const volatile = this.isVolatileMessage(data) || data.volatile_has_more === true;
507
+ if (seq > 0 && seq <= client._seqTracker.getContiguousSeq(ns) && !volatile)
508
+ return;
509
+ if (seq > 0)
510
+ client._seqTracker.updateMaxSeen(ns, seq);
511
+ const inline = this.inlineMessage(data);
512
+ if (inline && seq > 0 && !volatile) {
513
+ const generation = this.captureInlineGeneration();
514
+ client._safeAsync((async () => {
515
+ let consumed = false;
516
+ try {
517
+ if (this.peerBootstrapFloor === null)
518
+ await this.peers({}, true);
519
+ await this.initializePeer(sender);
520
+ consumed = await client._v2E2EE.serializePeerPage(ns, async () => {
521
+ const state = this.syncState(ns);
522
+ const previous = data.prev_peer_seq;
523
+ const expectedVersion = inline.version === 'v1' ? 'v1' : 'v2';
524
+ if (!this.isInlineGenerationCurrent(generation) || !Number.isSafeInteger(previous) || Number(previous) < 0
525
+ || Number(previous) >= seq || Number(previous) > state.ack || state.tail !== state.ack || state.head !== state.ack
526
+ || !this.inlineP2PBindingMatches(data, inline, seq, expectedVersion)
527
+ || (inline.prev_peer_seq !== undefined && inline.prev_peer_seq !== previous))
528
+ return false;
529
+ const decoded = await client._v2E2EE.decodeInlineP2PMessage(inline);
530
+ if (!decoded || !this.isInlineGenerationCurrent(generation) || !this.decodedInlineP2PMatches(data, decoded, seq))
531
+ return false;
532
+ decoded.need_render = inline.need_render !== false;
533
+ const event = this.p2pAppEventForMessage(decoded);
534
+ await client._publishAppEvent(event.event, event.payload, 'inline_push', ns);
535
+ if (!this.isInlineGenerationCurrent(generation))
536
+ return false;
537
+ const snapshot = client._seqTracker.snapshotNamespace(ns);
538
+ try {
539
+ client._seqTracker.forceContiguousSeq(ns, Math.max(state.ack, seq));
540
+ await client._commitSeqTrackerState(ns);
541
+ if (!this.isInlineGenerationCurrent(generation))
542
+ return false;
543
+ }
544
+ catch (error) {
545
+ if (this.isInlineGenerationCurrent(generation))
546
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
547
+ throw error;
548
+ }
549
+ await client._v2E2EE.ackPeerV2({ sender_aid: sender, up_to_seq: client._seqTracker.getContiguousSeq(ns) });
550
+ return true;
551
+ });
552
+ }
553
+ catch {
554
+ consumed = false;
555
+ }
556
+ if (!consumed && this.isInlineGenerationCurrent(generation))
557
+ this.schedulePeerRecovery(sender, seq, false);
558
+ })());
559
+ return;
560
+ }
561
+ this.schedulePeerRecovery(sender, seq, volatile);
562
+ }
123
563
  runtime;
124
564
  syncRun = 0;
125
565
  syncingNamespaces = new Set();
@@ -141,6 +581,7 @@ export class MessageDeliveryEngine {
141
581
  pendingGroupInlineAcks = null;
142
582
  pendingGroupInlineAckNamespaces = null;
143
583
  pendingTailWindows = new Map();
584
+ deferredApplicationPages = new Map();
144
585
  volatileContext = '';
145
586
  volatilePullCursorValue = '';
146
587
  volatileSeenMessages = new Map();
@@ -152,10 +593,143 @@ export class MessageDeliveryEngine {
152
593
  constructor(runtime) {
153
594
  this.runtime = runtime;
154
595
  }
596
+ beginApplicationPage(namespace, source = 'pull', reservation) {
597
+ const client = this.runtime.client;
598
+ if (typeof client._dispatcher?._snapshotApplicationHandlers !== 'function')
599
+ return undefined;
600
+ const generation = this.captureInlineGeneration();
601
+ const runtimeGeneration = Number(client._v2RuntimeGeneration ?? 0);
602
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(namespace);
603
+ let resolve;
604
+ let reject;
605
+ const done = new Promise((yes, no) => { resolve = yes; reject = no; });
606
+ void done.catch(() => { });
607
+ const handlers = client._dispatcher._snapshotApplicationHandlers(Object.values(APPLICATION_EVENT_FAMILIES));
608
+ const family = namespace.startsWith('group_event:') ? 'group.event_batch' : namespace.startsWith('group:') ? 'group.message_batch' : 'message.batch';
609
+ return { namespace, source: canonicalDeliverySource(source), handlers, batch: (handlers.get(family)?.length ?? 0) > 0,
610
+ items: [], effects: [], afterDelivery: [], cleanup: [], claims: new Map(), consumed: new Set(), seen: new Set(), done, resolve, reject, closed: false, flushed: false, deferred: false, reservation,
611
+ ensureCurrent: () => {
612
+ if (!this.isSeqNamespaceGenerationCurrent(namespace, namespaceGeneration) || !this.isInlineGenerationCurrent(generation)
613
+ || Number(client._v2RuntimeGeneration ?? 0) !== runtimeGeneration
614
+ || client._rpcPipeline?.isPullNamespaceCurrent?.(namespace) === false)
615
+ throw new StateError('delivery invalidated');
616
+ } };
617
+ }
618
+ async flushApplicationPage(page) {
619
+ if (!page)
620
+ return;
621
+ if (page.flushed)
622
+ return;
623
+ if (page.closed)
624
+ throw new StateError('delivery page closed');
625
+ if (page.deferred) {
626
+ page.ensureCurrent();
627
+ return;
628
+ }
629
+ try {
630
+ page.ensureCurrent();
631
+ const family = page.items.length > 0 ? APPLICATION_EVENT_FAMILIES[page.items[0].event] : '';
632
+ const handlers = page.handlers.get(family) ?? [];
633
+ if (page.batch && page.items.length > 0 && handlers.length > 0) {
634
+ await this.runtime.client._dispatcher._publishAndWait(family, {
635
+ namespace: page.namespace, items: page.items, source: page.source,
636
+ }, handlers, page.reservation, () => { page.ensureCurrent(); return true; });
637
+ }
638
+ page.ensureCurrent();
639
+ for (const effect of page.effects) {
640
+ await effect();
641
+ page.ensureCurrent();
642
+ }
643
+ for (const effect of page.afterDelivery) {
644
+ await effect();
645
+ page.ensureCurrent();
646
+ }
647
+ page.closed = true;
648
+ page.flushed = true;
649
+ page.resolve();
650
+ }
651
+ catch (error) {
652
+ this.abortApplicationPage(page, error);
653
+ throw error;
654
+ }
655
+ finally {
656
+ for (const cleanup of page.cleanup.splice(0))
657
+ cleanup();
658
+ for (const key of page.claims.keys())
659
+ if (this.p2pDeliveryInflight.get(key) === page.done)
660
+ this.p2pDeliveryInflight.delete(key);
661
+ }
662
+ }
663
+ abortApplicationPage(page, error) {
664
+ if (!page || page.closed)
665
+ return;
666
+ page.closed = true;
667
+ page.reject(error);
668
+ for (const cleanup of page.cleanup.splice(0))
669
+ cleanup();
670
+ for (const key of page.claims.keys())
671
+ if (this.p2pDeliveryInflight.get(key) === page.done)
672
+ this.p2pDeliveryInflight.delete(key);
673
+ }
674
+ deferApplicationEffect(page, effect) {
675
+ if (page?.batch) {
676
+ if (page.closed)
677
+ throw new StateError('delivery page closed');
678
+ page.effects.push(effect);
679
+ }
680
+ else
681
+ void effect();
682
+ }
683
+ deferApplicationAfterDelivery(page, effect) {
684
+ if (page?.batch) {
685
+ if (page.closed)
686
+ throw new StateError('delivery page closed');
687
+ page.afterDelivery.push(effect);
688
+ }
689
+ else
690
+ void effect();
691
+ }
692
+ async applicationOwnership(key, page, operation) {
693
+ if (page?.batch && page.claims.has(key))
694
+ return undefined;
695
+ for (;;) {
696
+ const pending = this.p2pDeliveryInflight.get(key);
697
+ if (!pending)
698
+ break;
699
+ await pending;
700
+ page?.ensureCurrent();
701
+ }
702
+ if (page?.batch) {
703
+ if (page.closed)
704
+ throw new StateError('delivery page closed');
705
+ page.claims.set(key, { sender_aid: '', message_id: '' });
706
+ this.p2pDeliveryInflight.set(key, page.done);
707
+ return await operation();
708
+ }
709
+ const task = Promise.resolve().then(operation);
710
+ const owner = task.then(() => undefined);
711
+ this.p2pDeliveryInflight.set(key, owner);
712
+ try {
713
+ return await task;
714
+ }
715
+ finally {
716
+ if (this.p2pDeliveryInflight.get(key) === owner)
717
+ this.p2pDeliveryInflight.delete(key);
718
+ void owner.catch(() => { });
719
+ }
720
+ }
155
721
  resetInlineAckState() {
156
722
  this.syncStopped('aborted');
157
723
  this.inlineGeneration += 1;
158
724
  this.deliveryGeneration += 1;
725
+ this.peerRecoveries.clear();
726
+ this.peerPendingSeqHints.clear();
727
+ this.peerPendingConfirmedThrough.clear();
728
+ this.peerDirectoryDirty = false;
729
+ this.peerDirectoryRefreshing = false;
730
+ if (this.peerRecoveryTimer !== null)
731
+ clearTimeout(this.peerRecoveryTimer);
732
+ this.peerRecoveryTimer = null;
159
733
  this.pendingPullDeliveryChanges = null;
160
734
  void this.runtime.client._rpcPipeline?.invalidatePulls?.();
161
735
  this.realtimeSyncing = null;
@@ -175,6 +749,10 @@ export class MessageDeliveryEngine {
175
749
  this.pendingGroupInlineAcks = null;
176
750
  this.pendingGroupInlineAckNamespaces = null;
177
751
  this.pendingTailWindows.clear();
752
+ for (const pages of this.deferredApplicationPages.values())
753
+ for (const page of new Set(pages.values()))
754
+ this.abortApplicationPage(page, new StateError('delivery invalidated'));
755
+ this.deferredApplicationPages.clear();
178
756
  this.syncingNamespaces.clear();
179
757
  this.syncSessions.clear();
180
758
  }
@@ -197,34 +775,57 @@ export class MessageDeliveryEngine {
197
775
  captureInlineGeneration() {
198
776
  return this.inlineGeneration;
199
777
  }
200
- deferTailWindow(ns, proof) {
778
+ deferTailWindow(ns, proof, page) {
201
779
  const tracker = new SeqTracker();
202
780
  tracker.restoreNamespaceSnapshot(ns, this.runtime.client._seqTracker.snapshotNamespace(ns));
203
781
  tracker.commitTailWindow(ns, proof);
782
+ if (page)
783
+ page.tailProof = proof;
204
784
  this.pendingTailWindows.set(ns, proof);
205
785
  }
786
+ pendingTailProofs(ns) {
787
+ const proofs = new Set();
788
+ const latest = this.pendingTailWindows.get(ns);
789
+ if (latest)
790
+ proofs.add(latest);
791
+ for (const page of this.deferredApplicationPages.get(ns)?.values() ?? [])
792
+ if (page.tailProof)
793
+ proofs.add(page.tailProof);
794
+ return [...proofs].sort((a, b) => a.windowStartSeq - b.windowStartSeq);
795
+ }
206
796
  prospectiveTailState(ns) {
207
- const proof = this.pendingTailWindows.get(ns);
208
- if (!proof)
797
+ const proofs = this.pendingTailProofs(ns);
798
+ if (proofs.length === 0)
209
799
  return this.syncState(ns);
210
800
  const tracker = new SeqTracker();
211
801
  tracker.restoreNamespaceSnapshot(ns, this.runtime.client._seqTracker.snapshotNamespace(ns));
212
- tracker.commitTailWindow(ns, proof);
802
+ for (const proof of proofs)
803
+ tracker.commitTailWindow(ns, proof);
213
804
  return tracker.getSyncState(ns);
214
805
  }
215
806
  pendingTailDeliveryThrough(ns, scanThrough) {
216
- const proof = this.pendingTailWindows.get(ns);
217
- return proof && scanThrough >= proof.windowStartSeq - 1 ? Math.max(scanThrough, proof.headSeq) : scanThrough;
807
+ for (const proof of this.pendingTailProofs(ns))
808
+ if (scanThrough >= proof.windowStartSeq - 1)
809
+ scanThrough = Math.max(scanThrough, proof.headSeq);
810
+ return scanThrough;
218
811
  }
219
812
  commitPendingTailWindow(ns, scanThrough) {
220
- const proof = this.pendingTailWindows.get(ns);
221
- if (proof && scanThrough >= proof.windowStartSeq - 1)
222
- this.runtime.client._seqTracker.commitTailWindow(ns, proof);
813
+ for (const proof of this.pendingTailProofs(ns))
814
+ if (scanThrough >= proof.windowStartSeq - 1) {
815
+ this.runtime.client._seqTracker.commitTailWindow(ns, proof);
816
+ scanThrough = Math.max(scanThrough, proof.headSeq);
817
+ }
223
818
  }
224
819
  completePendingTailWindow(ns) {
225
820
  const proof = this.pendingTailWindows.get(ns);
226
821
  if (proof && this.runtime.client._seqTracker.getContiguousSeq(ns) >= proof.headSeq)
227
822
  this.pendingTailWindows.delete(ns);
823
+ const pages = this.deferredApplicationPages.get(ns);
824
+ for (const [seq, page] of pages ?? [])
825
+ if (page.flushed && seq <= this.runtime.client._seqTracker.getContiguousSeq(ns))
826
+ pages.delete(seq);
827
+ if (pages?.size === 0)
828
+ this.deferredApplicationPages.delete(ns);
228
829
  }
229
830
  beginRealtimeSync(ns) {
230
831
  if (!ns || this.realtimeSyncing?.has(ns))
@@ -384,6 +985,9 @@ export class MessageDeliveryEngine {
384
985
  this.requestBackgroundPullRecovery();
385
986
  }
386
987
  requestBackgroundPullRecovery() {
988
+ this.requestPeerRecovery();
989
+ if (this.runtime.client._p2pPeerPullEnabled && this.peerDirectoryDirty)
990
+ this.requestPeerDirectoryRefresh();
387
991
  if (this.backgroundPullRecoveryQueued)
388
992
  return;
389
993
  const generation = this.captureInlineGeneration();
@@ -436,6 +1040,11 @@ export class MessageDeliveryEngine {
436
1040
  onForegroundPullsDrained() {
437
1041
  this.requestBackgroundPullRecovery();
438
1042
  const client = this.runtime.client;
1043
+ if (client._p2pPeerPullEnabled) {
1044
+ if (this.peerDirectoryDirty)
1045
+ this.requestPeerDirectoryRefresh();
1046
+ return;
1047
+ }
439
1048
  if (!client._aid || client._sessionOptions?.background_sync === false
440
1049
  || client.state !== ConnectionState.READY || client._closing)
441
1050
  return;
@@ -645,7 +1254,12 @@ export class MessageDeliveryEngine {
645
1254
  client._pushedSeqs.set(ns, new Set(keep));
646
1255
  }
647
1256
  }
648
- markPublishedSeq(ns, seq) {
1257
+ markPublishedSeq(ns, seq, page) {
1258
+ if (page?.batch) {
1259
+ page.consumed.add(seq);
1260
+ this.deferApplicationEffect(page, () => this.markPublishedSeq(ns, seq));
1261
+ return;
1262
+ }
649
1263
  const client = this.runtime.client;
650
1264
  let pushed = client._pushedSeqs.get(ns);
651
1265
  if (!pushed) {
@@ -1092,12 +1706,12 @@ export class MessageDeliveryEngine {
1092
1706
  throw new Error(`${method} response must be an object`);
1093
1707
  }
1094
1708
  const client = this.runtime.client;
1709
+ this.rememberCommittedSeqState(ns);
1710
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
1095
1711
  this.ensurePullOperationCurrent(ns);
1712
+ const applicationPage = this.beginApplicationPage(ns);
1096
1713
  const response = result;
1097
1714
  const volatileMessages = this.volatileMessages(response.messages);
1098
- if (method === 'message.pull' && volatileMessages.length > 0) {
1099
- await this.publishPlainVolatileMessages(volatileMessages, 'pull', true);
1100
- }
1101
1715
  const messages = deduplicatePlainForwardPageMessages(response.messages, afterSeq);
1102
1716
  const coordinator = this.forwardCoordinator();
1103
1717
  const pageContigBefore = client._seqTracker.getContiguousSeq(ns);
@@ -1124,12 +1738,14 @@ export class MessageDeliveryEngine {
1124
1738
  let publishedCount = 0;
1125
1739
  let committed = false;
1126
1740
  try {
1741
+ if (method === 'message.pull' && volatileMessages.length > 0)
1742
+ await this.publishPlainVolatileMessages(volatileMessages, 'pull', true, applicationPage);
1127
1743
  for (const rawMessage of messages) {
1128
1744
  this.ensurePullOperationCurrent(ns);
1129
1745
  const seq = positiveSafeSequenceHint(rawMessage.seq);
1130
1746
  if (method === 'message.pull') {
1131
1747
  const appEvent = p2pAppEventFromPlainPullMessage(rawMessage);
1132
- if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false)) {
1748
+ if (await this.publishPulledMessage(appEvent.event, ns, seq, appEvent.payload, false, 'pull', undefined, applicationPage)) {
1133
1749
  publishedCount += 1;
1134
1750
  }
1135
1751
  this.ensurePullOperationCurrent(ns);
@@ -1137,16 +1753,16 @@ export class MessageDeliveryEngine {
1137
1753
  }
1138
1754
  const message = normalizeGroupMentionMode(rawMessage);
1139
1755
  if (this.recallEventFromGroupMessage(message)) {
1140
- if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull')) {
1756
+ if (await this.publishGroupRecallTombstone(groupId, seq, message, 'pull', applicationPage)) {
1141
1757
  this.ensurePullOperationCurrent(ns);
1142
- this.markPublishedSeq(ns, seq);
1758
+ this.markPublishedSeq(ns, seq, applicationPage);
1143
1759
  publishedCount += 1;
1144
1760
  }
1145
1761
  }
1146
1762
  else if (this.isSelfSentGroupMessage(message)) {
1147
- this.markPublishedSeq(ns, seq);
1763
+ this.markPublishedSeq(ns, seq, applicationPage);
1148
1764
  }
1149
- else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false)) {
1765
+ else if (await this.publishPulledMessage('group.message_created', ns, seq, message, false, 'pull', undefined, applicationPage)) {
1150
1766
  this.ensurePullOperationCurrent(ns);
1151
1767
  publishedCount += 1;
1152
1768
  }
@@ -1158,10 +1774,13 @@ export class MessageDeliveryEngine {
1158
1774
  const scanThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, retentionFloor, visibilityFloor, deferredServerCursor, ...messages.map((message) => positiveSafeSequenceHint(message.seq)));
1159
1775
  const state = this.syncState(ns);
1160
1776
  const deliveryThrough = this.pendingTailDeliveryThrough(ns, scanThrough >= state.tail - 1 ? Math.max(scanThrough, state.head) : scanThrough);
1161
- await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough);
1777
+ await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough, applicationPage);
1778
+ await this.flushApplicationPage(applicationPage);
1162
1779
  this.ensurePullOperationCurrent(ns);
1163
1780
  if (!this.isInlineGenerationCurrent(pageInlineGeneration))
1164
1781
  throw new Error('pull invalidated');
1782
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
1783
+ throw new Error('pull invalidated');
1165
1784
  pageTrackerSnapshot = client._seqTracker.snapshotNamespace(ns);
1166
1785
  this.commitPendingTailWindow(ns, scanThrough);
1167
1786
  if (contiguousSeq === undefined)
@@ -1176,14 +1795,16 @@ export class MessageDeliveryEngine {
1176
1795
  this.ensurePullOperationCurrent(ns);
1177
1796
  await client._commitSeqTrackerState(ns);
1178
1797
  this.ensurePullOperationCurrent(ns);
1179
- if (!this.isInlineGenerationCurrent(pageInlineGeneration))
1798
+ if (!this.isInlineGenerationCurrent(pageInlineGeneration) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
1180
1799
  throw new Error('pull invalidated');
1181
1800
  }
1182
1801
  this.completePendingTailWindow(ns);
1183
1802
  committed = true;
1184
1803
  }
1185
1804
  catch (exc) {
1805
+ this.abortApplicationPage(applicationPage, exc);
1186
1806
  if (this.isPullOperationCurrent(ns) && this.isInlineGenerationCurrent(pageInlineGeneration)
1807
+ && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
1187
1808
  && pageTrackerSnapshot !== undefined && !committed && typeof client._seqTracker.restoreNamespaceSnapshot === 'function') {
1188
1809
  client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageTrackerSnapshot);
1189
1810
  this.dropSeqTrackerPending(ns);
@@ -1246,49 +1867,73 @@ export class MessageDeliveryEngine {
1246
1867
  isGroupEventNamespace(ns) {
1247
1868
  return ns.startsWith('group_event:');
1248
1869
  }
1249
- async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false, source = 'push', batch) {
1870
+ async publishOrderedQueueItem(ns, event, seq, payload, pullResponse = false, source = 'push', batch, page) {
1250
1871
  const client = this.runtime.client;
1251
1872
  const pullScoped = this.isPullScopedSource(source);
1252
1873
  if (pullScoped)
1253
1874
  this.ensurePullOperationCurrent(ns);
1254
1875
  if (event === 'group.changed' && this.isGroupEventNamespace(ns)) {
1255
- await this.publishOrderedGroupChanged(payload, source);
1876
+ await this.publishOrderedGroupChanged(payload, source, page, ns);
1256
1877
  if (pullScoped)
1257
1878
  this.ensurePullOperationCurrent(ns);
1258
1879
  return;
1259
1880
  }
1260
1881
  if (event === 'message.recalled') {
1261
- await this.publishMessageRecallTombstone(seq, payload, source);
1882
+ await this.publishMessageRecallTombstone(seq, payload, source, page);
1262
1883
  if (pullScoped)
1263
1884
  this.ensurePullOperationCurrent(ns);
1264
1885
  return;
1265
1886
  }
1266
1887
  if (pullResponse) {
1267
- await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
1888
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch, page));
1268
1889
  if (pullScoped)
1269
1890
  this.ensurePullOperationCurrent(ns);
1270
1891
  return;
1271
1892
  }
1272
- await client._publishAppEvent(event, payload, source, ns, batch);
1893
+ await client._publishAppEvent(event, payload, source, ns, batch, page);
1273
1894
  if (pullScoped)
1274
1895
  this.ensurePullOperationCurrent(ns);
1275
1896
  }
1276
- async publishOrderedGroupChanged(payload, source = 'ordered') {
1897
+ async publishOrderedGroupChanged(payload, source = 'ordered', page, ns = '', owned = false) {
1277
1898
  const client = this.runtime.client;
1899
+ if (!owned && isJsonObject(payload) && Number.isSafeInteger(payload.event_seq) && Number(payload.event_seq) > 0) {
1900
+ const key = JSON.stringify([this.peerContext(), 'group.changed.action', ns || this.applicationNamespace('group.changed', payload), payload.event_seq]);
1901
+ const batchActive = page ? page.batch : (client._dispatcher?._snapshotApplicationHandlers?.(['group.event_batch']).get('group.event_batch')?.length ?? 0) > 0;
1902
+ if (batchActive || this.p2pDeliveryInflight.has(key)) {
1903
+ await this.applicationOwnership(key, page, async () => {
1904
+ if (this.p2pDeliveryCompleted.has(key))
1905
+ return;
1906
+ await this.publishOrderedGroupChanged(payload, source, page, ns, true);
1907
+ this.deferApplicationEffect(page, () => {
1908
+ this.p2pDeliveryCompleted.set(key, { sender_aid: '', message_id: '' });
1909
+ if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT)
1910
+ this.p2pDeliveryCompleted.delete(this.p2pDeliveryCompleted.keys().next().value);
1911
+ });
1912
+ });
1913
+ return;
1914
+ }
1915
+ }
1278
1916
  const generation = this.captureInlineGeneration();
1279
- await client._publishAppEvent('group.changed', payload, source);
1917
+ await client._publishAppEvent('group.changed', payload, source, ns, undefined, page);
1280
1918
  if (!this.isInlineGenerationCurrent(generation))
1281
1919
  throw new Error('pull invalidated');
1282
- if (isJsonObject(payload)) {
1283
- const eventPayload = payload;
1284
- client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
1285
- if (eventPayload.action === 'dissolved') {
1286
- const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
1287
- const groupId = String(rawGroupId).trim();
1288
- if (groupId)
1289
- client._cleanupDissolvedGroup?.(groupId);
1920
+ this.deferApplicationEffect(page, () => {
1921
+ if (isJsonObject(payload)) {
1922
+ const eventPayload = payload;
1923
+ client._groupState?.handleGroupChangedV2Membership?.(eventPayload);
1924
+ if (eventPayload.action === 'dissolved') {
1925
+ const rawGroupId = eventPayload.group_id ?? eventPayload.group_aid ?? '';
1926
+ const groupId = String(rawGroupId).trim();
1927
+ if (groupId) {
1928
+ if (page)
1929
+ page.cleanup.push(() => { if (page.flushed)
1930
+ client._cleanupDissolvedGroup?.(groupId); });
1931
+ else
1932
+ client._cleanupDissolvedGroup?.(groupId);
1933
+ }
1934
+ }
1290
1935
  }
1291
- }
1936
+ });
1292
1937
  }
1293
1938
  isInstanceScopedMessageEvent(event) {
1294
1939
  return event === 'message.received'
@@ -1405,6 +2050,7 @@ export class MessageDeliveryEngine {
1405
2050
  if (protectedHeaders)
1406
2051
  envelope.protected_headers = protectedHeaders;
1407
2052
  setIfPresent('payload_type', firstValue(message.payload_type, protectedHeaders?.payload_type));
2053
+ setIfPresent('need_render', message.need_render !== false);
1408
2054
  setIfPresent('mention_mode', message.mention_mode);
1409
2055
  return envelope;
1410
2056
  }
@@ -1473,6 +2119,7 @@ export class MessageDeliveryEngine {
1473
2119
  if (protectedHeaders)
1474
2120
  envelope.protected_headers = protectedHeaders;
1475
2121
  setIfPresent('payload_type', firstValue(params.payload_type, protectedHeaders?.payload_type, body.type));
2122
+ setIfPresent('need_render', params.need_render !== false);
1476
2123
  if (method === 'group.send')
1477
2124
  setIfPresent('mention_mode', resultObj.mention_mode);
1478
2125
  return envelope;
@@ -1618,7 +2265,7 @@ export class MessageDeliveryEngine {
1618
2265
  const sender = String(message.from_aid ?? message.from ?? '').trim();
1619
2266
  const messageId = String(message.message_id ?? '').trim();
1620
2267
  const cursor = String(message.volatile_cursor ?? '').trim();
1621
- return `${sender}|${messageId || cursor}`;
2268
+ return `${sender}|${messageId}|${cursor}`;
1622
2269
  }
1623
2270
  isVolatileSeen(message) {
1624
2271
  this.ensureVolatileContext();
@@ -1629,7 +2276,12 @@ export class MessageDeliveryEngine {
1629
2276
  }
1630
2277
  return this.volatileSeenMessages.has(this.volatileSeenKey(message));
1631
2278
  }
1632
- markVolatileSeen(message) {
2279
+ markVolatileSeen(message, page) {
2280
+ if (page?.batch) {
2281
+ page.seen.add(`volatile:${this.volatileSeenKey(message)}`);
2282
+ this.deferApplicationEffect(page, () => this.markVolatileSeen(message));
2283
+ return;
2284
+ }
1633
2285
  this.ensureVolatileContext();
1634
2286
  const now = Date.now();
1635
2287
  const expiresAt = Math.max(now + 60_000, Number(message.expires_at ?? now + 1_800_000) || now + 1_800_000);
@@ -1641,16 +2293,37 @@ export class MessageDeliveryEngine {
1641
2293
  this.volatileSeenMessages.delete(oldest);
1642
2294
  }
1643
2295
  }
1644
- beginVolatileDelivery(message) {
2296
+ async waitVolatileApplicationDelivery(message, page) {
2297
+ const key = JSON.stringify([this.peerContext(), 'volatile', this.volatileSeenKey(message)]);
2298
+ if (page?.claims.has(key))
2299
+ return;
2300
+ const pending = this.p2pDeliveryInflight.get(key);
2301
+ if (pending) {
2302
+ await pending;
2303
+ page?.ensureCurrent();
2304
+ }
2305
+ }
2306
+ beginVolatileDelivery(message, page) {
2307
+ if (page?.seen.has(`volatile:${this.volatileSeenKey(message)}`))
2308
+ return 'seen';
1645
2309
  if (this.isVolatileSeen(message))
1646
2310
  return 'seen';
1647
2311
  const key = this.volatileSeenKey(message);
1648
2312
  if (this.volatileInFlightMessages.has(key))
1649
2313
  return 'inflight';
1650
2314
  this.volatileInFlightMessages.add(key);
2315
+ if (page?.batch) {
2316
+ const ownerKey = JSON.stringify([this.peerContext(), 'volatile', key]);
2317
+ page.claims.set(ownerKey, { sender_aid: '', message_id: '' });
2318
+ this.p2pDeliveryInflight.set(ownerKey, page.done);
2319
+ }
1651
2320
  return 'claimed';
1652
2321
  }
1653
- finishVolatileDelivery(message) {
2322
+ finishVolatileDelivery(message, page) {
2323
+ if (page?.batch) {
2324
+ page.cleanup.push(() => this.finishVolatileDelivery(message));
2325
+ return;
2326
+ }
1654
2327
  this.ensureVolatileContext();
1655
2328
  this.volatileInFlightMessages.delete(this.volatileSeenKey(message));
1656
2329
  }
@@ -1659,7 +2332,11 @@ export class MessageDeliveryEngine {
1659
2332
  delete result.seq;
1660
2333
  return result;
1661
2334
  }
1662
- async ackVolatileMessages(messages, v2, advanceCursor) {
2335
+ async ackVolatileMessages(messages, v2, advanceCursor, page) {
2336
+ if (page?.batch) {
2337
+ this.deferApplicationAfterDelivery(page, async () => { await this.ackVolatileMessages(messages, v2, advanceCursor); });
2338
+ return undefined;
2339
+ }
1663
2340
  const cursors = [...new Set(messages
1664
2341
  .map((item) => String(item.volatile_cursor ?? '').trim())
1665
2342
  .filter(Boolean))];
@@ -1673,11 +2350,12 @@ export class MessageDeliveryEngine {
1673
2350
  }
1674
2351
  return result;
1675
2352
  }
1676
- async publishPlainVolatileMessages(messages, source, advanceCursor) {
2353
+ async publishPlainVolatileMessages(messages, source, advanceCursor, page) {
1677
2354
  const processed = [];
1678
2355
  for (const rawMessage of messages) {
1679
2356
  const message = this.publicVolatileMessage(rawMessage);
1680
- const claim = this.beginVolatileDelivery(message);
2357
+ await this.waitVolatileApplicationDelivery(message, page);
2358
+ const claim = this.beginVolatileDelivery(message, page);
1681
2359
  if (claim === 'seen') {
1682
2360
  processed.push(rawMessage);
1683
2361
  continue;
@@ -1686,16 +2364,16 @@ export class MessageDeliveryEngine {
1686
2364
  continue;
1687
2365
  try {
1688
2366
  const appEvent = this.p2pAppEventForMessage(message);
1689
- await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source);
1690
- this.markVolatileSeen(message);
2367
+ await this.runtime.client._publishAppEvent(appEvent.event, appEvent.payload, source, page?.namespace ?? '', undefined, page);
2368
+ this.markVolatileSeen(message, page);
1691
2369
  }
1692
2370
  finally {
1693
- this.finishVolatileDelivery(message);
2371
+ this.finishVolatileDelivery(message, page);
1694
2372
  }
1695
2373
  processed.push(rawMessage);
1696
2374
  }
1697
2375
  if (processed.length > 0)
1698
- await this.ackVolatileMessages(processed, false, advanceCursor);
2376
+ await this.ackVolatileMessages(processed, false, advanceCursor, page);
1699
2377
  return processed.map((item) => this.publicVolatileMessage(item));
1700
2378
  }
1701
2379
  messageRecallDedupKey(payload) {
@@ -1724,31 +2402,36 @@ export class MessageDeliveryEngine {
1724
2402
  return `p2p|tombstone:${tombstoneId}`;
1725
2403
  return `p2p|unknown:${Date.now()}:${Math.random()}`;
1726
2404
  }
1727
- async publishMessageRecallTombstone(seq, message, source = 'push') {
2405
+ async publishMessageRecallTombstone(seq, message, source = 'push', page, owned = false) {
1728
2406
  const client = this.runtime.client;
1729
2407
  const generation = this.captureInlineGeneration();
1730
2408
  const eventPayload = this.recallEventFromMessage(message);
1731
2409
  if (!eventPayload)
1732
2410
  return false;
1733
2411
  const dedupKey = this.messageRecallDedupKey(eventPayload);
2412
+ if (!owned)
2413
+ return await this.applicationOwnership(JSON.stringify([this.peerContext(), 'message.recalled', dedupKey]), page, () => this.publishMessageRecallTombstone(seq, message, source, page, true)) ?? false;
1734
2414
  let seen = client._messageRecallSeen;
1735
2415
  if (!seen) {
1736
2416
  seen = new Map();
1737
2417
  client._messageRecallSeen = seen;
1738
2418
  }
1739
- if (seen.has(dedupKey)) {
2419
+ if (seen.has(dedupKey) || page?.seen.has(`message.recalled:${dedupKey}`)) {
1740
2420
  client._clientLog.debug(`message.recalled dedup suppressed: seq=${String(seq)} key=${dedupKey}`);
1741
2421
  return false;
1742
2422
  }
1743
- await client._publishAppEvent('message.recalled', eventPayload, source);
2423
+ await client._publishAppEvent('message.recalled', eventPayload, source, page?.namespace ?? '', undefined, page);
1744
2424
  if (!this.isInlineGenerationCurrent(generation))
1745
2425
  throw new Error('pull invalidated');
1746
- seen.set(dedupKey, Date.now());
1747
- if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
1748
- const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
1749
- for (const [oldKey] of drop)
1750
- seen.delete(oldKey);
1751
- }
2426
+ page?.seen.add(`message.recalled:${dedupKey}`);
2427
+ this.deferApplicationEffect(page, () => {
2428
+ seen.set(dedupKey, Date.now());
2429
+ if (seen.size > MESSAGE_RECALL_SEEN_LIMIT) {
2430
+ const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - MESSAGE_RECALL_SEEN_LIMIT);
2431
+ for (const [oldKey] of drop)
2432
+ seen.delete(oldKey);
2433
+ }
2434
+ });
1752
2435
  client._clientLog.debug(`message.recalled published: seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1753
2436
  return true;
1754
2437
  }
@@ -1831,7 +2514,7 @@ export class MessageDeliveryEngine {
1831
2514
  return `${normalizedGroupId}|tombstone:${tombstoneId}`;
1832
2515
  return `${normalizedGroupId}|unknown:${Date.now()}:${Math.random()}`;
1833
2516
  }
1834
- async publishGroupRecallTombstone(groupId, seq, message, source = 'push') {
2517
+ async publishGroupRecallTombstone(groupId, seq, message, source = 'push', page, owned = false) {
1835
2518
  const client = this.runtime.client;
1836
2519
  const generation = this.captureInlineGeneration();
1837
2520
  const eventPayload = this.recallEventFromGroupMessage(message);
@@ -1840,24 +2523,29 @@ export class MessageDeliveryEngine {
1840
2523
  const rawGroupId = String(eventPayload.group_id ?? groupId ?? '').trim();
1841
2524
  const dedupGroupId = rawGroupId;
1842
2525
  const dedupKey = this.groupRecallDedupKey(dedupGroupId, eventPayload);
2526
+ if (!owned)
2527
+ return await this.applicationOwnership(JSON.stringify([this.peerContext(), 'group.message_recalled', dedupKey]), page, () => this.publishGroupRecallTombstone(groupId, seq, message, source, page, true)) ?? false;
1843
2528
  let seen = client._groupRecallSeen;
1844
2529
  if (!seen) {
1845
2530
  seen = new Map();
1846
2531
  client._groupRecallSeen = seen;
1847
2532
  }
1848
- if (seen.has(dedupKey)) {
2533
+ if (seen.has(dedupKey) || page?.seen.has(`group.message_recalled:${dedupKey}`)) {
1849
2534
  client._clientLog.debug(`group.message_recalled dedup suppressed: group=${groupId} seq=${String(seq)} key=${dedupKey}`);
1850
2535
  return false;
1851
2536
  }
1852
- await client._publishAppEvent('group.message_recalled', eventPayload, source);
2537
+ await client._publishAppEvent('group.message_recalled', eventPayload, source, page?.namespace ?? '', undefined, page);
1853
2538
  if (!this.isInlineGenerationCurrent(generation))
1854
2539
  throw new Error('pull invalidated');
1855
- seen.set(dedupKey, Date.now());
1856
- if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
1857
- const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
1858
- for (const [oldKey] of drop)
1859
- seen.delete(oldKey);
1860
- }
2540
+ page?.seen.add(`group.message_recalled:${dedupKey}`);
2541
+ this.deferApplicationEffect(page, () => {
2542
+ seen.set(dedupKey, Date.now());
2543
+ if (seen.size > GROUP_RECALL_SEEN_LIMIT) {
2544
+ const drop = [...seen.entries()].sort((a, b) => a[1] - b[1]).slice(0, seen.size - GROUP_RECALL_SEEN_LIMIT);
2545
+ for (const [oldKey] of drop)
2546
+ seen.delete(oldKey);
2547
+ }
2548
+ });
1861
2549
  client._clientLog.debug(`group.message_recalled published: group=${groupId} seq=${String(seq)} ids=${JSON.stringify(eventPayload.message_ids)}`);
1862
2550
  return true;
1863
2551
  }
@@ -1899,9 +2587,139 @@ export class MessageDeliveryEngine {
1899
2587
  client._safeAsync(this.fillGroupGap(groupId));
1900
2588
  await this.publishGroupRecallTombstone(groupId, seq, wrapped);
1901
2589
  }
1902
- async publishAppEvent(event, payload, source = 'direct', ns = '', batch) {
2590
+ async publishAppEvent(event, payload, source = 'direct', ns = '', batch, page) {
1903
2591
  const client = this.runtime.client;
2592
+ const seq = isJsonObject(payload) ? payload.seq : undefined;
2593
+ const groupSeq = isJsonObject(payload) && event === 'group.changed' ? payload.event_seq : seq;
2594
+ if ((event === 'group.message_created' || event === 'group.changed') && source !== 'history'
2595
+ && isJsonObject(payload) && !this.isVolatileMessage(payload) && Number.isSafeInteger(groupSeq) && Number(groupSeq) > 0) {
2596
+ const groupNs = ns || this.applicationNamespace(event, payload);
2597
+ const key = JSON.stringify([this.peerContext(), event, groupNs, groupSeq]);
2598
+ const family = APPLICATION_EVENT_FAMILIES[event];
2599
+ const batchActive = page ? page.batch : (client._dispatcher?._snapshotApplicationHandlers?.([family]).get(family)?.length ?? 0) > 0;
2600
+ if (batchActive || this.p2pDeliveryInflight.has(key)) {
2601
+ await this.applicationOwnership(key, page, async () => {
2602
+ if (this.p2pDeliveryCompleted.has(key))
2603
+ return;
2604
+ await this.publishAppEventInternal(event, payload, source, ns, batch, page);
2605
+ this.deferApplicationEffect(page, () => {
2606
+ this.p2pDeliveryCompleted.set(key, { sender_aid: String(payload.from ?? ''), message_id: String(payload.message_id ?? '') });
2607
+ if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT)
2608
+ this.p2pDeliveryCompleted.delete(this.p2pDeliveryCompleted.keys().next().value);
2609
+ });
2610
+ });
2611
+ return;
2612
+ }
2613
+ }
2614
+ if (!client._p2pPeerPullEnabled || event !== 'message.received' || source === 'history'
2615
+ || !isJsonObject(payload) || this.isVolatileMessage(payload) || !Number.isSafeInteger(seq) || Number(seq) <= 0) {
2616
+ return await this.publishAppEventInternal(event, payload, source, ns, batch, page);
2617
+ }
2618
+ const context = this.peerContext();
2619
+ const generation = this.captureInlineGeneration();
2620
+ const aid = client._aid;
2621
+ const deviceId = client._deviceId;
2622
+ const slotId = client._slotId;
2623
+ const ensureCurrent = () => {
2624
+ if (client._aid !== aid || client._deviceId !== deviceId || client._slotId !== slotId
2625
+ || this.peerContext() !== context || !this.isInlineGenerationCurrent(generation))
2626
+ throw new StateError('delivery invalidated');
2627
+ };
2628
+ const sender = String(payload.sender_aid ?? payload.from_aid ?? payload.from ?? '');
2629
+ if (!sender)
2630
+ return await this.publishAppEventInternal(event, payload, source, ns, batch, page);
2631
+ const key = JSON.stringify([context, event, seq]);
2632
+ const receipt = { sender_aid: sender, message_id: String(payload.message_id ?? '') };
2633
+ const validate = (saved, expected = receipt) => {
2634
+ if (saved.sender_aid !== expected.sender_aid || (saved.message_id && expected.message_id && saved.message_id !== expected.message_id))
2635
+ throw new ValidationError('delivery receipt identity conflict');
2636
+ };
2637
+ const local = page?.batch ? page.claims.get(key) : undefined;
2638
+ if (local) {
2639
+ validate(local);
2640
+ if (!local.message_id)
2641
+ local.message_id = receipt.message_id;
2642
+ return;
2643
+ }
2644
+ for (;;) {
2645
+ const inflight = this.p2pDeliveryInflight.get(key);
2646
+ if (!inflight)
2647
+ break;
2648
+ await inflight;
2649
+ ensureCurrent();
2650
+ const completed = this.p2pDeliveryCompleted.get(key);
2651
+ if (completed)
2652
+ validate(completed);
2653
+ if (completed?.message_id || !receipt.message_id)
2654
+ return;
2655
+ }
2656
+ if (page?.batch) {
2657
+ page.ensureCurrent();
2658
+ if (page.closed || (ns && ns !== page.namespace))
2659
+ throw new StateError('delivery page invalidated');
2660
+ const completed = this.p2pDeliveryCompleted.get(key);
2661
+ if (completed) {
2662
+ validate(completed);
2663
+ if (!completed.message_id && receipt.message_id)
2664
+ this.deferApplicationEffect(page, () => { completed.message_id = receipt.message_id; });
2665
+ return;
2666
+ }
2667
+ page.claims.set(key, receipt);
2668
+ this.p2pDeliveryInflight.set(key, page.done);
2669
+ await this.publishAppEventInternal(event, payload, source, ns, batch, page);
2670
+ this.deferApplicationEffect(page, () => {
2671
+ this.p2pDeliveryCompleted.set(key, { ...receipt });
2672
+ if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT)
2673
+ this.p2pDeliveryCompleted.delete(this.p2pDeliveryCompleted.keys().next().value);
2674
+ });
2675
+ return;
2676
+ }
2677
+ const task = Promise.resolve().then(async () => {
2678
+ ensureCurrent();
2679
+ let pending = this.p2pDeliveryCompleted.get(key);
2680
+ if (pending)
2681
+ validate(pending);
2682
+ if (!pending) {
2683
+ await this.publishAppEventInternal(event, payload, source, ns, batch, page);
2684
+ ensureCurrent();
2685
+ pending = { ...receipt };
2686
+ this.p2pDeliveryCompleted.set(key, pending);
2687
+ }
2688
+ else if (!pending.message_id && receipt.message_id) {
2689
+ pending = { ...receipt };
2690
+ this.p2pDeliveryCompleted.set(key, pending);
2691
+ }
2692
+ if (this.p2pDeliveryCompleted.size > PUSHED_SEQS_LIMIT) {
2693
+ const oldest = this.p2pDeliveryCompleted.keys().next().value;
2694
+ if (oldest !== undefined)
2695
+ this.p2pDeliveryCompleted.delete(oldest);
2696
+ }
2697
+ });
2698
+ this.p2pDeliveryInflight.set(key, task);
2699
+ try {
2700
+ await task;
2701
+ }
2702
+ finally {
2703
+ if (this.p2pDeliveryInflight.get(key) === task)
2704
+ this.p2pDeliveryInflight.delete(key);
2705
+ }
2706
+ }
2707
+ async publishAppEventInternal(event, payload, source = 'direct', ns = '', batch, page) {
2708
+ const client = this.runtime.client;
2709
+ const family = APPLICATION_EVENT_FAMILIES[event];
2710
+ const handlers = page?.handlers ?? (family ? client._dispatcher._snapshotApplicationHandlers?.([event, family]) : undefined);
1904
2711
  const generation = this.deliveryGeneration;
2712
+ const runtimeGeneration = Number(client._v2RuntimeGeneration ?? 0);
2713
+ const namespace = ns || this.applicationNamespace(event, payload);
2714
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(namespace);
2715
+ const guard = () => {
2716
+ page?.ensureCurrent();
2717
+ if (generation !== this.deliveryGeneration || Number(client._v2RuntimeGeneration ?? 0) !== runtimeGeneration
2718
+ || !this.isSeqNamespaceGenerationCurrent(namespace, namespaceGeneration)
2719
+ || client._rpcPipeline?.isPullNamespaceCurrent?.(namespace) === false)
2720
+ throw new StateError('delivery invalidated');
2721
+ return true;
2722
+ };
1905
2723
  if ((event === 'message.received' || event === 'group.message_created') && isJsonObject(payload)) {
1906
2724
  client._maybeAppendEchoTraceReceive(payload);
1907
2725
  }
@@ -1921,6 +2739,8 @@ export class MessageDeliveryEngine {
1921
2739
  client._clientLog.debug(`agent_md etag inject skipped: ${String(exc)}`);
1922
2740
  }
1923
2741
  }
2742
+ if (client._p2pPeerPullEnabled && generation !== this.deliveryGeneration)
2743
+ throw new StateError('delivery invalidated');
1924
2744
  const canonicalSource = canonicalDeliverySource(source);
1925
2745
  let normalized = this.normalizePublishedMessagePayload(event, payload, source);
1926
2746
  if (isJsonObject(normalized)
@@ -1928,13 +2748,46 @@ export class MessageDeliveryEngine {
1928
2748
  && canonicalSource !== 'direct') {
1929
2749
  normalized = { ...normalized, source: canonicalSource };
1930
2750
  }
1931
- if (['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
2751
+ if (page?.batch && family) {
2752
+ page.ensureCurrent();
2753
+ if (page.closed || (ns && ns !== page.namespace))
2754
+ throw new StateError('delivery page invalidated');
2755
+ page.items.push({ event, payload: normalized });
2756
+ }
2757
+ else if (family && handlers && !page) {
2758
+ const batchHandlers = handlers.get(family) ?? [];
2759
+ if (batchHandlers.length > 0) {
2760
+ await client._dispatcher._publishAndWait(family, { namespace: ns || this.applicationNamespace(event, payload),
2761
+ items: [{ event, payload: normalized }], source: canonicalSource }, batchHandlers, undefined, guard);
2762
+ }
2763
+ else
2764
+ await client._dispatcher._publishAndWait(event, normalized, undefined, undefined, guard);
2765
+ }
2766
+ else if (['message.received', 'group.message_created', 'message.recalled', 'group.message_recalled', 'group.changed'].includes(event)
1932
2767
  && typeof client._dispatcher._publishAndWait === 'function') {
1933
- await client._dispatcher._publishAndWait(event, normalized);
2768
+ await client._dispatcher._publishAndWait(event, normalized, undefined, page?.reservation, guard);
1934
2769
  }
1935
2770
  else {
1936
2771
  client._dispatcher.enqueue(event, normalized);
1937
2772
  }
2773
+ if (page?.batch) {
2774
+ this.deferApplicationEffect(page, () => this.recordApplicationDeliveryChange(event, payload, source, ns, batch, generation));
2775
+ return;
2776
+ }
2777
+ if (family)
2778
+ guard();
2779
+ await this.recordApplicationDeliveryChange(event, payload, source, ns, batch, generation);
2780
+ }
2781
+ applicationNamespace(event, payload) {
2782
+ if (isJsonObject(payload) && event.startsWith('group.'))
2783
+ return `${event === 'group.changed' ? 'group_event' : 'group'}:${String(payload.group_id ?? payload.group_aid ?? '')}`;
2784
+ const client = this.runtime.client;
2785
+ const sender = isJsonObject(payload) ? String(payload.sender_aid ?? payload.from_aid ?? payload.from ?? '') : '';
2786
+ return client._p2pPeerPullEnabled && sender ? this.peerNamespace(sender) : `p2p:${client._aid ?? ''}`;
2787
+ }
2788
+ async recordApplicationDeliveryChange(event, payload, source, ns, batch, generation) {
2789
+ const client = this.runtime.client;
2790
+ const canonicalSource = canonicalDeliverySource(source);
1938
2791
  if (generation !== this.deliveryGeneration)
1939
2792
  return;
1940
2793
  if (canonicalSource !== 'pull' && canonicalSource !== 'pending_retry'
@@ -2082,6 +2935,10 @@ export class MessageDeliveryEngine {
2082
2935
  }
2083
2936
  onRawMessageReceived(data) {
2084
2937
  const client = this.runtime.client;
2938
+ if (client._p2pPeerPullEnabled) {
2939
+ this.handlePeerNotification(data);
2940
+ return;
2941
+ }
2085
2942
  client._clientLog.debug(`_onRawMessageReceived enter: from=${data?.from ?? '-'} mid=${data?.message_id ?? '-'} seq=${data?.seq ?? '-'}`);
2086
2943
  const ns = isJsonObject(data) && client._aid && data.seq !== undefined
2087
2944
  ? `p2p:${client._aid}`
@@ -2109,6 +2966,11 @@ export class MessageDeliveryEngine {
2109
2966
  msg.type = 'message.recalled';
2110
2967
  if (!this.messageTargetsCurrentInstance(msg))
2111
2968
  return;
2969
+ if (client._p2pPeerPullEnabled) {
2970
+ this.handlePeerNotification(msg);
2971
+ await this.publishMessageRecallTombstone(msg.seq, msg);
2972
+ return;
2973
+ }
2112
2974
  const seq = positiveSafeSequenceHint(msg.seq);
2113
2975
  if (client._aid && seq > 0) {
2114
2976
  const ns = `p2p:${client._aid}`;
@@ -2327,7 +3189,7 @@ export class MessageDeliveryEngine {
2327
3189
  if (client._v2Session) {
2328
3190
  try {
2329
3191
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
2330
- await client._rpcPipeline.runPullSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }), true);
3192
+ await client._rpcPipeline.runPullNamespaceSerialized(key, () => client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true }), true);
2331
3193
  return;
2332
3194
  }
2333
3195
  catch (exc) {
@@ -2350,15 +3212,15 @@ export class MessageDeliveryEngine {
2350
3212
  after_message_seq: afterSeq,
2351
3213
  limit: 50,
2352
3214
  });
2353
- await client._rpcPipeline.runPullSerialized(key, async () => {
2354
- const result = await client._rpcPipeline.rawCall('group.pull', {
3215
+ await client._rpcPipeline.runPullNamespaceSerialized(key, async () => {
3216
+ const result = await client._rpcPipeline.runPullSerialized(key, () => client._rpcPipeline.rawCall('group.pull', {
2355
3217
  group_id: groupId,
2356
3218
  after_message_seq: afterSeq,
2357
3219
  device_id: client._deviceId,
2358
3220
  slot_id: client._slotId,
2359
3221
  limit: 50,
2360
3222
  _rpc_background: true,
2361
- }, { background: true });
3223
+ }, { background: true, pullGateKey: key }), true);
2362
3224
  if (!this.isInlineGenerationCurrent(generation))
2363
3225
  throw new Error('pull invalidated');
2364
3226
  rawCompleted = true;
@@ -2405,6 +3267,10 @@ export class MessageDeliveryEngine {
2405
3267
  }, delayMs);
2406
3268
  }
2407
3269
  async fillP2pGap(retryAttempt = 0) {
3270
+ if (this.runtime.client._p2pPeerPullEnabled) {
3271
+ this.requestPeerDirectoryRefresh();
3272
+ return;
3273
+ }
2408
3274
  const client = this.runtime.client;
2409
3275
  const generation = this.captureInlineGeneration();
2410
3276
  if (client._sessionOptions?.background_sync === false)
@@ -2496,17 +3362,17 @@ export class MessageDeliveryEngine {
2496
3362
  let failed = false;
2497
3363
  try {
2498
3364
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', { group_id: groupId, after_seq: afterSeq, limit: 50 });
2499
- const messages = await client._rpcPipeline.runPullSerialized(key, async () => {
3365
+ const messages = await client._rpcPipeline.runPullNamespaceSerialized(key, async () => {
2500
3366
  if (client._v2Session)
2501
- return await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true });
2502
- const result = await client._rpcPipeline.rawCall('group.pull', {
3367
+ return await client._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true, background: true });
3368
+ const result = await client._rpcPipeline.runPullSerialized(key, () => client._rpcPipeline.rawCall('group.pull', {
2503
3369
  group_id: groupId,
2504
3370
  after_message_seq: afterSeq,
2505
3371
  device_id: client._deviceId,
2506
3372
  slot_id: client._slotId,
2507
3373
  limit: 50,
2508
3374
  _rpc_background: true,
2509
- }, { background: true });
3375
+ }, { background: true, pullGateKey: key }), true);
2510
3376
  if (!this.isInlineGenerationCurrent(generation))
2511
3377
  throw new Error('pull invalidated');
2512
3378
  if (isJsonObject(result))
@@ -2544,6 +3410,8 @@ export class MessageDeliveryEngine {
2544
3410
  if (!groupId)
2545
3411
  return;
2546
3412
  const ns = `group_event:${groupId}`;
3413
+ this.rememberCommittedSeqState(ns);
3414
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
2547
3415
  const requestedAfterSeq = Number(options?.afterSeq ?? 0);
2548
3416
  const afterSeq = Number.isSafeInteger(requestedAfterSeq) && requestedAfterSeq > 0
2549
3417
  ? requestedAfterSeq
@@ -2593,49 +3461,69 @@ export class MessageDeliveryEngine {
2593
3461
  const eventSeqs = [];
2594
3462
  let hasDissolvedEvent = false;
2595
3463
  let publishedEventCount = 0;
2596
- for (const evt of eventObjects) {
2597
- const eventSeq = Number(evt.event_seq ?? 0);
2598
- if (Number.isFinite(eventSeq) && eventSeq > 0)
2599
- eventSeqs.push(eventSeq);
2600
- evt._from_gap_fill = true;
2601
- if (evt.action === 'dissolved')
2602
- hasDissolvedEvent = true;
2603
- const et = String(evt.event_type ?? '');
2604
- if (et === 'group.message_created')
2605
- continue;
2606
- const cs = evt.client_signature;
2607
- if (cs && typeof cs === 'object') {
2608
- if (client._shouldSkipEventSignature(evt)) {
2609
- delete evt.client_signature;
3464
+ const applicationPage = this.beginApplicationPage(ns);
3465
+ try {
3466
+ for (const evt of eventObjects) {
3467
+ const eventSeq = Number(evt.event_seq ?? 0);
3468
+ if (Number.isFinite(eventSeq) && eventSeq > 0)
3469
+ eventSeqs.push(eventSeq);
3470
+ evt._from_gap_fill = true;
3471
+ if (evt.action === 'dissolved')
3472
+ hasDissolvedEvent = true;
3473
+ const et = String(evt.event_type ?? '');
3474
+ if (et === 'group.message_created')
3475
+ continue;
3476
+ const cs = evt.client_signature;
3477
+ if (cs && typeof cs === 'object') {
3478
+ if (client._shouldSkipEventSignature(evt)) {
3479
+ delete evt.client_signature;
3480
+ }
3481
+ else {
3482
+ const verified = await client._verifyEventSignature(evt, cs);
3483
+ evt._verified = client._isEventSignatureVerified(verified);
3484
+ }
2610
3485
  }
2611
- else {
2612
- const verified = await client._verifyEventSignature(evt, cs);
2613
- evt._verified = client._isEventSignatureVerified(verified);
3486
+ if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
3487
+ this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
3488
+ publishedEventCount += 1;
2614
3489
  }
2615
3490
  }
2616
- if (Number.isFinite(eventSeq) && eventSeq > 0 && !client._pushedSeqs.get(ns)?.has(eventSeq)) {
2617
- this.enqueueOrderedMessage(ns, 'group.changed', eventSeq, evt, 'pull');
2618
- publishedEventCount += 1;
2619
- }
3491
+ const deliveryThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, effectiveFloor, ...eventSeqs);
3492
+ await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough, applicationPage);
3493
+ await this.flushApplicationPage(applicationPage);
3494
+ }
3495
+ catch (error) {
3496
+ this.abortApplicationPage(applicationPage, error);
3497
+ throw error;
2620
3498
  }
2621
- const deliveryThrough = Math.max(pageContigBefore, contiguousSeq ?? 0, effectiveFloor, ...eventSeqs);
2622
- await this.drainOrderedMessages(ns, undefined, false, false, undefined, 'pull', deliveryThrough);
2623
3499
  if (!this.isInlineGenerationCurrent(generation))
2624
3500
  throw new Error('pull invalidated');
2625
3501
  if (hasDissolvedEvent) {
2626
3502
  this.backgroundPullDemands.delete(ns);
2627
3503
  break;
2628
3504
  }
2629
- if (contiguousSeq === undefined)
2630
- client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
2631
- else
2632
- client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
2633
- if (effectiveFloor > client._seqTracker.getContiguousSeq(ns)) {
2634
- client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
3505
+ if (!this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3506
+ throw new Error('pull invalidated');
3507
+ const pageSnapshot = client._seqTracker.snapshotNamespace(ns);
3508
+ let ackContig = pageContigBefore;
3509
+ try {
3510
+ if (contiguousSeq === undefined)
3511
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
3512
+ else
3513
+ client._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq, contiguousSeq);
3514
+ if (effectiveFloor > client._seqTracker.getContiguousSeq(ns))
3515
+ client._seqTracker.forceContiguousSeq(ns, effectiveFloor);
3516
+ ackContig = client._seqTracker.getContiguousSeq(ns);
3517
+ if (ackContig !== pageContigBefore) {
3518
+ await this.commitSeqTrackerState(ns);
3519
+ if (!this.isInlineGenerationCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3520
+ throw new Error('pull invalidated');
3521
+ }
2635
3522
  }
2636
- const ackContig = client._seqTracker.getContiguousSeq(ns);
2637
- if (ackContig !== pageContigBefore && !hasDissolvedEvent) {
2638
- this.persistSeq(ns);
3523
+ catch (error) {
3524
+ if (this.isInlineGenerationCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3525
+ client._seqTracker._restoreNamespaceSnapshotPreservingPush(ns, pageSnapshot);
3526
+ throw error;
2639
3527
  }
2640
3528
  if (eventObjects.length > 0 && ackContig > 0 && ackContig !== pageContigBefore) {
2641
3529
  pendingAckSeq = Math.max(pendingAckSeq, ackContig);
@@ -2817,14 +3705,14 @@ export class MessageDeliveryEngine {
2817
3705
  max_pages: maxPages,
2818
3706
  };
2819
3707
  const invoke = async () => {
2820
- const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, maxPages });
3708
+ const messages = await client._pullGroupV2(groupId, Number(request.after_seq), pageLimit, { gateLocked: true, background, maxPages });
2821
3709
  return { messages, raw_count: messages.length };
2822
3710
  };
2823
3711
  const pipeline = client._rpcPipeline;
2824
3712
  if (!pipeline)
2825
3713
  return await invoke();
2826
3714
  const key = pipeline.pullGateKeyForCall('group.pull', request);
2827
- return await pipeline.runPullSerialized(key, invoke, background);
3715
+ return await pipeline.runPullNamespaceSerialized(key, invoke, background);
2828
3716
  }
2829
3717
  syncState(ns) {
2830
3718
  const tracker = this.runtime.client._seqTracker;
@@ -3131,6 +4019,8 @@ export class MessageDeliveryEngine {
3131
4019
  }
3132
4020
  async commitInlineMessage(ns, seq, event, payload, generation) {
3133
4021
  const client = this.runtime.client;
4022
+ this.rememberCommittedSeqState(ns);
4023
+ const namespaceGeneration = this.captureSeqNamespaceGeneration(ns);
3134
4024
  const tracker = client._seqTracker;
3135
4025
  let snapshot = null;
3136
4026
  let trackerChanged = false;
@@ -3140,7 +4030,7 @@ export class MessageDeliveryEngine {
3140
4030
  const published = selfSync
3141
4031
  ? (this.markPublishedSeq(ns, seq), true)
3142
4032
  : await this.publishPulledMessage(event, ns, seq, payload, true, 'inline_push');
3143
- if (!this.isInlineGenerationCurrent(generation))
4033
+ if (!this.isInlineGenerationCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3144
4034
  return false;
3145
4035
  if (!published)
3146
4036
  return false;
@@ -3154,12 +4044,13 @@ export class MessageDeliveryEngine {
3154
4044
  if (!this.isInlineGenerationCurrent(generation))
3155
4045
  return false;
3156
4046
  await client._commitSeqTrackerState(ns);
3157
- if (!this.isInlineGenerationCurrent(generation))
4047
+ if (!this.isInlineGenerationCurrent(generation) || !this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration))
3158
4048
  return false;
3159
4049
  return true;
3160
4050
  }
3161
4051
  catch (exc) {
3162
- if (trackerChanged && this.isInlineGenerationCurrent(generation) && typeof tracker.restoreNamespaceSnapshot === 'function') {
4052
+ if (trackerChanged && this.isInlineGenerationCurrent(generation) && this.isSeqNamespaceGenerationCurrent(ns, namespaceGeneration)
4053
+ && typeof tracker.restoreNamespaceSnapshot === 'function') {
3163
4054
  tracker._restoreNamespaceSnapshotPreservingPush(ns, snapshot);
3164
4055
  }
3165
4056
  client._clientLog.debug(`inline realtime consume rolled back: ns=${ns} seq=${seq} err=${formatDeliveryError(exc)}`);
@@ -3398,16 +4289,17 @@ export class MessageDeliveryEngine {
3398
4289
  return { messages: [], raw_count: 0 };
3399
4290
  const request = { group_id: groupId, ...(tail ? { window_mode: 'tail' } : {}), after_seq: live.ack, limit: pageLimit };
3400
4291
  const key = client._rpcPipeline.pullGateKeyForCall('group.pull', request);
3401
- return await client._rpcPipeline.runPullSerialized(key, async () => {
4292
+ return await client._rpcPipeline.runPullNamespaceSerialized(key, async () => {
3402
4293
  if (tail) {
3403
4294
  const tailResult = await client._v2E2EE.pullGroupV2TailInternal({
3404
4295
  group_id: groupId, window_mode: 'tail', after_seq: live.ack, limit: pageLimit,
3405
- }, true);
4296
+ }, true, true);
3406
4297
  tailCompleted = true;
3407
4298
  return tailResult;
3408
4299
  }
3409
4300
  const messages = await client._pullGroupV2(groupId, live.ack, pageLimit, {
3410
4301
  gateLocked: true,
4302
+ background,
3411
4303
  ...(maxPages ? { maxPages } : {}),
3412
4304
  });
3413
4305
  return { messages, raw_count: messages.length };
@@ -3584,6 +4476,10 @@ export class MessageDeliveryEngine {
3584
4476
  }
3585
4477
  async onV2PushNotification(data) {
3586
4478
  const client = this.runtime.client;
4479
+ if (client._p2pPeerPullEnabled) {
4480
+ this.handlePeerNotification(data);
4481
+ return;
4482
+ }
3587
4483
  const ns = client._aid ? `p2p:${client._aid}` : '';
3588
4484
  if (!ns)
3589
4485
  return;
@@ -3679,6 +4575,10 @@ export class MessageDeliveryEngine {
3679
4575
  }
3680
4576
  enqueueOnlineUnreadP2pHint(data) {
3681
4577
  const client = this.runtime.client;
4578
+ if (client._p2pPeerPullEnabled) {
4579
+ this.requestPeerDirectoryRefresh();
4580
+ return;
4581
+ }
3682
4582
  if (!client._aid || !client._v2Session || client._sessionOptions?.background_sync === false)
3683
4583
  return;
3684
4584
  const ns = `p2p:${client._aid}`;
@@ -3726,6 +4626,7 @@ export class MessageDeliveryEngine {
3726
4626
  if (state && typeof state === 'object' && Object.keys(state).length > 0) {
3727
4627
  state = await this.migrateSeqStateGroupIds(state);
3728
4628
  client._seqTracker.restoreSyncState(state);
4629
+ this.restoreCommittedSeqStates();
3729
4630
  return;
3730
4631
  }
3731
4632
  }
@@ -3737,6 +4638,7 @@ export class MessageDeliveryEngine {
3737
4638
  if (state && typeof state === 'object' && Object.keys(state).length > 0) {
3738
4639
  state = await this.migrateSeqStateGroupIds(state);
3739
4640
  client._seqTracker.restoreState(state);
4641
+ this.restoreCommittedSeqStates();
3740
4642
  }
3741
4643
  return;
3742
4644
  }
@@ -3751,6 +4653,7 @@ export class MessageDeliveryEngine {
3751
4653
  if (isJsonObject(state)) {
3752
4654
  const migrated = await this.migrateSeqStateGroupIds(state);
3753
4655
  client._seqTracker.restoreState(migrated);
4656
+ this.restoreCommittedSeqStates();
3754
4657
  }
3755
4658
  }
3756
4659
  }
@@ -3767,6 +4670,16 @@ export class MessageDeliveryEngine {
3767
4670
  async migrateSeqStateGroupIds(state) {
3768
4671
  return state;
3769
4672
  }
4673
+ restoreCommittedSeqStates() {
4674
+ this.peerContext();
4675
+ for (const [ns, state] of Object.entries(this.runtime.client._seqTracker.exportSyncState())) {
4676
+ if (!this.isReliableSeqNamespace(ns))
4677
+ continue;
4678
+ this.committedSeqTrackerStates.set(ns, { ...state });
4679
+ if (ns.startsWith('p2p.peer.v1/'))
4680
+ this.peerInitialized.add(ns);
4681
+ }
4682
+ }
3770
4683
  async saveSeqTrackerState() {
3771
4684
  const client = this.runtime.client;
3772
4685
  const context = this.currentSeqTrackerContext();
@@ -3777,6 +4690,7 @@ export class MessageDeliveryEngine {
3777
4690
  const state = typeof client._seqTracker.exportSyncState === 'function'
3778
4691
  ? client._seqTracker.exportSyncState()
3779
4692
  : Object.fromEntries(Object.entries(client._seqTracker.exportState()).map(([ns, seq]) => [ns, { ack: Number(seq), tail: Number(seq), head: Number(seq) }]));
4693
+ this.useCommittedSeqStates(state);
3780
4694
  const pending = this.seqTrackerPending();
3781
4695
  if (pending.size > 0 && client._seqTrackerPendingPersistContext === context) {
3782
4696
  const liveNamespaces = new Set(Object.keys(state));
@@ -3807,11 +4721,15 @@ export class MessageDeliveryEngine {
3807
4721
  const state = typeof client._seqTracker.getSyncState === 'function'
3808
4722
  ? client._seqTracker.getSyncState(ns)
3809
4723
  : { ack: seq, tail: seq, head: seq };
3810
- this.mergeSeqTrackerPending(context, { [ns]: state });
4724
+ const pendingState = { [ns]: state };
4725
+ this.useCommittedSeqStates(pendingState);
4726
+ this.mergeSeqTrackerPending(context, pendingState);
3811
4727
  this.scheduleSeqTrackerFlush();
3812
4728
  }
3813
4729
  async commitSeqTrackerState(ns) {
3814
4730
  const client = this.runtime.client;
4731
+ const tracker = client._seqTracker;
4732
+ const namespaceGeneration = this.seqTrackerNamespaceGenerations.get(ns) ?? 0;
3815
4733
  const context = this.currentSeqTrackerContext();
3816
4734
  if (!context || !ns)
3817
4735
  return;
@@ -3824,10 +4742,18 @@ export class MessageDeliveryEngine {
3824
4742
  const [aid, deviceId, slotId] = JSON.parse(context);
3825
4743
  const pendingBeforeCommit = this.takeSeqTrackerPending(context, ns);
3826
4744
  try {
3827
- await this.writeSeqTrackerState(aid, deviceId, slotId, { [ns]: state }, true);
4745
+ await this.enqueueSeqTrackerWrite(async () => {
4746
+ if (client._seqTracker !== tracker)
4747
+ throw new StateError('seq tracker commit invalidated');
4748
+ await this.writeSeqTrackerStateNow(aid, deviceId, slotId, { [ns]: state }, true);
4749
+ if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration
4750
+ && this.currentSeqTrackerContext() === context && this.isReliableSeqNamespace(ns))
4751
+ this.committedSeqTrackerStates.set(ns, { ...state });
4752
+ });
3828
4753
  }
3829
4754
  catch (exc) {
3830
- this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
4755
+ if (client._seqTracker === tracker && (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) === namespaceGeneration)
4756
+ this.restoreSeqTrackerPending(context, ns, pendingBeforeCommit);
3831
4757
  throw exc;
3832
4758
  }
3833
4759
  client._clientLog.debug(`seq_tracker atomic commit: ns=${ns} A/T/H=${JSON.stringify(state)} context=${context}`);
@@ -3934,7 +4860,25 @@ export class MessageDeliveryEngine {
3934
4860
  }
3935
4861
  }
3936
4862
  async writeSeqTrackerState(aid, deviceId, slotId, state, throwOnError = false) {
3937
- await this.enqueueSeqTrackerWrite(() => this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError));
4863
+ await this.enqueueSeqTrackerWrite(() => {
4864
+ if (!throwOnError && this.currentSeqTrackerContext() === JSON.stringify([aid, deviceId, slotId]))
4865
+ this.useCommittedSeqStates(state);
4866
+ return this.writeSeqTrackerStateNow(aid, deviceId, slotId, state, throwOnError);
4867
+ });
4868
+ }
4869
+ useCommittedSeqStates(state) {
4870
+ this.peerContext();
4871
+ for (const ns of Object.keys(state)) {
4872
+ if (!this.isReliableSeqNamespace(ns))
4873
+ continue;
4874
+ if (!ns.startsWith('p2p.peer.v1/'))
4875
+ this.rememberCommittedSeqState(ns);
4876
+ const durable = this.committedSeqTrackerStates.get(ns);
4877
+ if (durable)
4878
+ state[ns] = { ...durable };
4879
+ else
4880
+ delete state[ns];
4881
+ }
3938
4882
  }
3939
4883
  enqueueSeqTrackerWrite(operation) {
3940
4884
  const run = this.seqTrackerWriteChain.then(operation, operation);
@@ -3988,6 +4932,11 @@ export class MessageDeliveryEngine {
3988
4932
  const deviceId = client._deviceId;
3989
4933
  const slotId = client._slotId;
3990
4934
  const seq = client._seqTracker.getContiguousSeq(ns);
4935
+ if (seq <= 0) {
4936
+ this.seqTrackerNamespaceGenerations.set(ns, (this.seqTrackerNamespaceGenerations.get(ns) ?? 0) + 1);
4937
+ this.committedSeqTrackerStates.delete(ns);
4938
+ this.peerInitialized.delete(ns);
4939
+ }
3991
4940
  this.dropSeqTrackerPending(ns);
3992
4941
  const saveSeq = client._tokenStore.saveSeq;
3993
4942
  if (seq > 0 && typeof saveSeq === 'function') {
@@ -4045,7 +4994,9 @@ export class MessageDeliveryEngine {
4045
4994
  if (next < 0)
4046
4995
  next = 0;
4047
4996
  if (ns) {
4048
- const committedAck = client._seqTracker.getContiguousSeq(ns);
4997
+ if (ns.startsWith('group:') || ns.startsWith('group_event:'))
4998
+ this.rememberCommittedSeqState(ns);
4999
+ const committedAck = this.isReliableSeqNamespace(ns) ? this.committedSeqTrackerAck(ns) : client._seqTracker.getContiguousSeq(ns);
4049
5000
  if (next > committedAck) {
4050
5001
  client._clientLog.warn(`ack clamp: method=${method} ${field}=${original} > committed_ack=${committedAck}, clamp`);
4051
5002
  next = committedAck;
@@ -4075,11 +5026,11 @@ export class MessageDeliveryEngine {
4075
5026
  return next === raw ? params : { ...params, [field]: next };
4076
5027
  };
4077
5028
  if (method === 'message.v2.ack') {
4078
- const ns = client._aid ? `p2p:${client._aid}` : '';
5029
+ const ns = client._p2pPeerPullEnabled ? this.peerNamespace(String(params.sender_aid ?? params.peer_aid ?? '').trim()) : client._aid ? `p2p:${client._aid}` : '';
4079
5030
  return clampField('up_to_seq', ns);
4080
5031
  }
4081
5032
  if (method === 'message.ack') {
4082
- const ns = client._aid ? `p2p:${client._aid}` : '';
5033
+ const ns = client._p2pPeerPullEnabled ? this.peerNamespace(String(params.sender_aid ?? params.peer_aid ?? '').trim()) : client._aid ? `p2p:${client._aid}` : '';
4083
5034
  return clampField('seq', ns);
4084
5035
  }
4085
5036
  if (method === 'group.v2.ack' || method === 'group.ack_messages' || method === 'group.ack_events') {
@@ -4094,7 +5045,18 @@ export class MessageDeliveryEngine {
4094
5045
  }
4095
5046
  return params;
4096
5047
  }
4097
- async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, batch, source = 'pull', throughSeq) {
5048
+ consumeOrderedSequence(ns, seq, page) {
5049
+ const client = this.runtime.client;
5050
+ const queue = client._pendingOrderedMsgs.get(ns);
5051
+ if (page?.batch)
5052
+ page.consumed.add(seq);
5053
+ this.deferApplicationEffect(page, () => {
5054
+ queue?.delete(seq);
5055
+ if (queue?.size === 0 && client._pendingOrderedMsgs.get(ns) === queue)
5056
+ client._pendingOrderedMsgs.delete(ns);
5057
+ });
5058
+ }
5059
+ async drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, batch, source = 'pull', throughSeq, page) {
4098
5060
  const client = this.runtime.client;
4099
5061
  const pullScoped = this.isPullScopedSource(source);
4100
5062
  if (pullScoped)
@@ -4109,15 +5071,40 @@ export class MessageDeliveryEngine {
4109
5071
  .sort((a, b) => a - b);
4110
5072
  let delivered = false;
4111
5073
  const drainBatches = new Map();
5074
+ const deferredPages = this.deferredApplicationPages.get(ns);
5075
+ const isolatedPage = page && deferredPages && [...deferredPages.values()].includes(page);
4112
5076
  try {
4113
5077
  for (const seq of ready) {
4114
5078
  if (pullScoped)
4115
5079
  this.ensurePullOperationCurrent(ns);
4116
5080
  const item = queue.get(seq);
4117
- if (!item)
5081
+ if (!item || page?.consumed.has(seq))
5082
+ continue;
5083
+ let deferredPage = deferredPages?.get(seq);
5084
+ if (isolatedPage && deferredPage !== page)
5085
+ continue;
5086
+ if (deferredPage && deferredPage !== page) {
5087
+ if ([...deferredPages].some(([pendingSeq, pendingPage]) => pendingPage === deferredPage && !ready.includes(pendingSeq)))
5088
+ continue;
5089
+ await this.flushApplicationPage(page);
5090
+ deferredPage.ensureCurrent();
5091
+ if (deferredPage.closed) {
5092
+ const retry = this.beginApplicationPage(ns, deferredPage.source);
5093
+ retry.handlers = deferredPage.handlers;
5094
+ retry.batch = deferredPage.batch;
5095
+ retry.tailProof = deferredPage.tailProof;
5096
+ for (const [pendingSeq, pendingPage] of deferredPages)
5097
+ if (pendingPage === deferredPage)
5098
+ deferredPages.set(pendingSeq, retry);
5099
+ deferredPage = retry;
5100
+ }
5101
+ deferredPage.deferred = false;
5102
+ await this.drainOrderedMessages(ns, beforeSeq, pullResponse, false, batch, source, throughSeq, deferredPage);
5103
+ await this.flushApplicationPage(deferredPage);
4118
5104
  continue;
5105
+ }
4119
5106
  if (client._pushedSeqs.get(ns)?.has(seq)) {
4120
- queue.delete(seq);
5107
+ this.consumeOrderedSequence(ns, seq, page);
4121
5108
  continue;
4122
5109
  }
4123
5110
  const itemSource = item.source ?? source;
@@ -4130,15 +5117,16 @@ export class MessageDeliveryEngine {
4130
5117
  drainBatches.set(itemSource, itemBatch);
4131
5118
  }
4132
5119
  }
4133
- await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse, itemSource, itemBatch);
5120
+ if (!page?.seen.has(`ordered:${seq}`))
5121
+ await this.publishOrderedQueueItem(ns, item.event, seq, item.payload, pullResponse, itemSource, itemBatch, page);
4134
5122
  if (!this.isInlineGenerationCurrent(generation))
4135
5123
  throw new Error('pull invalidated');
4136
5124
  if (pullScoped)
4137
5125
  this.ensurePullOperationCurrent(ns);
4138
- queue.delete(seq);
5126
+ this.consumeOrderedSequence(ns, seq, page);
4139
5127
  if (item.event === 'group.changed' && isJsonObject(item.payload) && item.payload.action === 'dissolved')
4140
5128
  continue;
4141
- this.markPublishedSeq(ns, seq);
5129
+ this.markPublishedSeq(ns, seq, page);
4142
5130
  delivered = true;
4143
5131
  }
4144
5132
  }
@@ -4147,7 +5135,7 @@ export class MessageDeliveryEngine {
4147
5135
  await this.flushRealtimeDeliveryChanges(drainBatch);
4148
5136
  }
4149
5137
  }
4150
- if (queue.size === 0) {
5138
+ if (!page?.batch && queue.size === 0) {
4151
5139
  client._pendingOrderedMsgs.delete(ns);
4152
5140
  if (delivered && persist) {
4153
5141
  if (pullScoped)
@@ -4156,7 +5144,7 @@ export class MessageDeliveryEngine {
4156
5144
  }
4157
5145
  }
4158
5146
  }
4159
- async publishOrderedMessage(event, ns, seq, payload, source = 'push', operationBatch) {
5147
+ async publishOrderedMessage(event, ns, seq, payload, source = 'push', operationBatch, page) {
4160
5148
  const client = this.runtime.client;
4161
5149
  const generation = this.captureInlineGeneration();
4162
5150
  const ownsBatch = !operationBatch && (source === 'push' || source === 'inline_push');
@@ -4164,39 +5152,55 @@ export class MessageDeliveryEngine {
4164
5152
  try {
4165
5153
  const seqNum = Number(seq);
4166
5154
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
4167
- await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch);
5155
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch, page);
4168
5156
  return true;
4169
5157
  }
4170
- if (client._pushedSeqs.get(ns)?.has(seqNum)) {
5158
+ if (page?.consumed.has(seqNum) || client._pushedSeqs.get(ns)?.has(seqNum)) {
4171
5159
  const queue = client._pendingOrderedMsgs.get(ns);
4172
- queue?.delete(seqNum);
4173
- if (queue && queue.size === 0)
5160
+ this.consumeOrderedSequence(ns, seqNum, page);
5161
+ if (!page?.batch && queue && queue.size === 0)
4174
5162
  client._pendingOrderedMsgs.delete(ns);
4175
5163
  return false;
4176
5164
  }
4177
5165
  if (event === 'message.received' && this.isSelfSentP2PMessage(payload)) {
4178
- this.markPublishedSeq(ns, seqNum);
5166
+ this.markPublishedSeq(ns, seqNum, page);
4179
5167
  client._clientLog.debug(`publish ordered skipped current-device self-sync: ns=${ns}, seq=${seqNum}`);
4180
5168
  return false;
4181
5169
  }
4182
5170
  const contig = client._seqTracker.getContiguousSeq(ns);
4183
5171
  if (seqNum > contig) {
5172
+ const owner = this.deferredApplicationPages.get(ns)?.get(seqNum);
5173
+ if (page && source === 'tail' && owner && owner !== page)
5174
+ return false;
4184
5175
  this.enqueueOrderedMessage(ns, event, seqNum, payload, source);
5176
+ if (page && source === 'tail') {
5177
+ let pages = this.deferredApplicationPages.get(ns);
5178
+ if (!pages) {
5179
+ pages = new Map();
5180
+ this.deferredApplicationPages.set(ns, pages);
5181
+ }
5182
+ pages.set(seqNum, page);
5183
+ page.deferred = true;
5184
+ if (page.batch) {
5185
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch, page);
5186
+ page.seen.add(`ordered:${seqNum}`);
5187
+ }
5188
+ }
4185
5189
  return false;
4186
5190
  }
4187
- await this.drainOrderedMessages(ns, seqNum, true, true, batch, source);
4188
- if (client._pushedSeqs.get(ns)?.has(seqNum))
5191
+ await this.drainOrderedMessages(ns, seqNum, true, true, batch, source, undefined, page);
5192
+ if (page?.consumed.has(seqNum) || client._pushedSeqs.get(ns)?.has(seqNum))
4189
5193
  return false;
4190
5194
  const queue = client._pendingOrderedMsgs.get(ns);
4191
- await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch);
5195
+ await this.publishOrderedQueueItem(ns, event, seqNum, payload, false, source, batch, page);
4192
5196
  if (!this.isInlineGenerationCurrent(generation))
4193
5197
  throw new Error('pull invalidated');
4194
- queue?.delete(seqNum);
4195
- if (queue && queue.size === 0)
5198
+ this.consumeOrderedSequence(ns, seqNum, page);
5199
+ if (!page?.batch && queue && queue.size === 0)
4196
5200
  client._pendingOrderedMsgs.delete(ns);
4197
- this.markPublishedSeq(ns, seqNum);
4198
- await this.drainOrderedMessages(ns, undefined, false, true, batch, source);
4199
- if (!client._pendingOrderedMsgs.get(ns))
5201
+ this.markPublishedSeq(ns, seqNum, page);
5202
+ await this.drainOrderedMessages(ns, undefined, false, true, batch, source, undefined, page);
5203
+ if (!page?.batch && !client._pendingOrderedMsgs.get(ns))
4200
5204
  await this.saveSeqTrackerState();
4201
5205
  return true;
4202
5206
  }
@@ -4205,8 +5209,16 @@ export class MessageDeliveryEngine {
4205
5209
  await this.flushRealtimeDeliveryChanges(batch);
4206
5210
  }
4207
5211
  }
4208
- async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull', operationBatch) {
5212
+ async publishPulledMessage(event, ns, seq, payload, persist = true, source = 'pull', operationBatch, page) {
4209
5213
  const client = this.runtime.client;
5214
+ const deferredPage = this.deferredApplicationPages.get(ns)?.get(Number(seq));
5215
+ if (deferredPage && deferredPage !== page && !deferredPage.flushed)
5216
+ return false;
5217
+ if (client._p2pPeerPullEnabled && event === 'message.received' && ns.startsWith('p2p.peer.v1/')) {
5218
+ await client._publishAppEvent(event, payload, source, ns, operationBatch, page);
5219
+ this.markPublishedSeq(ns, Number(seq), page);
5220
+ return true;
5221
+ }
4210
5222
  const generation = this.captureInlineGeneration();
4211
5223
  const ownsBatch = !operationBatch && (source === 'push' || source === 'inline_push');
4212
5224
  const batch = operationBatch ?? (ownsBatch ? this.createDeliveryChangeBatch(source) : undefined);
@@ -4217,52 +5229,52 @@ export class MessageDeliveryEngine {
4217
5229
  const seqNum = Number(seq);
4218
5230
  if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0 || !ns) {
4219
5231
  if (event === 'message.recalled') {
4220
- const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload, source));
5232
+ const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seq, payload, source, page));
4221
5233
  if (pullScoped)
4222
5234
  this.ensurePullOperationCurrent(ns);
4223
5235
  return published;
4224
5236
  }
4225
- await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
5237
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch, page));
4226
5238
  if (pullScoped)
4227
5239
  this.ensurePullOperationCurrent(ns);
4228
5240
  return true;
4229
5241
  }
4230
5242
  const queue = client._pendingOrderedMsgs.get(ns);
4231
- if (client._pushedSeqs.get(ns)?.has(seqNum)) {
4232
- queue?.delete(seqNum);
4233
- if (queue && queue.size === 0)
5243
+ if (page?.consumed.has(seqNum) || client._pushedSeqs.get(ns)?.has(seqNum)) {
5244
+ this.consumeOrderedSequence(ns, seqNum, page);
5245
+ if (!page?.batch && queue && queue.size === 0)
4234
5246
  client._pendingOrderedMsgs.delete(ns);
4235
5247
  return false;
4236
5248
  }
4237
5249
  if (event === 'message.received' && this.isSelfSentP2PMessage(payload)) {
4238
- this.markPublishedSeq(ns, seqNum);
5250
+ this.markPublishedSeq(ns, seqNum, page);
4239
5251
  client._clientLog.debug(`publish pulled skipped current-device self-sync: ns=${ns}, seq=${seqNum}`);
4240
5252
  return source === 'inline_push';
4241
5253
  }
4242
- await this.drainOrderedMessages(ns, seqNum, false, persist, batch, source);
5254
+ await this.drainOrderedMessages(ns, seqNum, false, persist, batch, source, undefined, page);
4243
5255
  if (pullScoped)
4244
5256
  this.ensurePullOperationCurrent(ns);
4245
5257
  if (event === 'message.recalled') {
4246
- const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload, source));
5258
+ const published = await client._withPullResponseProcessing(ns, () => this.publishMessageRecallTombstone(seqNum, payload, source, page));
4247
5259
  if (!this.isInlineGenerationCurrent(generation))
4248
5260
  throw new Error('pull invalidated');
4249
5261
  if (pullScoped)
4250
5262
  this.ensurePullOperationCurrent(ns);
4251
- queue?.delete(seqNum);
4252
- if (queue && queue.size === 0)
5263
+ this.consumeOrderedSequence(ns, seqNum, page);
5264
+ if (!page?.batch && queue && queue.size === 0)
4253
5265
  client._pendingOrderedMsgs.delete(ns);
4254
- this.markPublishedSeq(ns, seqNum);
5266
+ this.markPublishedSeq(ns, seqNum, page);
4255
5267
  return published;
4256
5268
  }
4257
- await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch));
5269
+ await client._withPullResponseProcessing(ns, () => client._publishAppEvent(event, payload, source, ns, batch, page));
4258
5270
  if (!this.isInlineGenerationCurrent(generation))
4259
5271
  throw new Error('pull invalidated');
4260
5272
  if (pullScoped)
4261
5273
  this.ensurePullOperationCurrent(ns);
4262
- queue?.delete(seqNum);
4263
- if (queue && queue.size === 0)
5274
+ this.consumeOrderedSequence(ns, seqNum, page);
5275
+ if (!page?.batch && queue && queue.size === 0)
4264
5276
  client._pendingOrderedMsgs.delete(ns);
4265
- this.markPublishedSeq(ns, seqNum);
5277
+ this.markPublishedSeq(ns, seqNum, page);
4266
5278
  return true;
4267
5279
  }
4268
5280
  finally {