@agentunion/fastaun-browser 0.5.19 → 0.5.21

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