@enbox/agent 0.5.1 → 0.5.2

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.
@@ -1,15 +1,23 @@
1
1
  import type { EnboxPlatformAgent } from './types/agent.js';
2
2
  import type { PermissionsApi } from './types/permissions.js';
3
- import type { GenericMessage, MessagesReadReply, UnionMessageReply } from '@enbox/dwn-sdk-js';
3
+ import type { GenericMessage, MessagesReadReply, MessagesSyncDiffEntry, UnionMessageReply } from '@enbox/dwn-sdk-js';
4
4
 
5
- import { DwnInterfaceName, DwnMethodName, Message } from '@enbox/dwn-sdk-js';
5
+ import { DwnInterfaceName, DwnMethodName, Encoder, Message } from '@enbox/dwn-sdk-js';
6
6
 
7
7
  import { DwnInterface } from './types/dwn.js';
8
8
  import { isRecordsWrite } from './utils.js';
9
9
  import { topologicalSort } from './sync-topological-sort.js';
10
10
 
11
- /** Entry type for fetched messages with optional data stream. */
12
- export type SyncMessageEntry = { message: GenericMessage; dataStream?: ReadableStream<Uint8Array> };
11
+ /** Maximum data size (in bytes) to buffer in memory for retry. Larger payloads are re-fetched. */
12
+ const MAX_BUFFER_SIZE = 1_048_576; // 1 MB
13
+
14
+ /** Entry type for fetched messages with optional data stream and retry buffer. */
15
+ export type SyncMessageEntry = {
16
+ message: GenericMessage;
17
+ dataStream?: ReadableStream<Uint8Array>;
18
+ /** Buffered data bytes for retry — avoids re-fetching from remote when stream is consumed. */
19
+ bufferedData?: Uint8Array;
20
+ };
13
21
 
14
22
  /**
15
23
  * 202: message was successfully written to the remote DWN
@@ -43,55 +51,165 @@ export async function getMessageCid(message: GenericMessage): Promise<string> {
43
51
  * Fetches missing messages from the remote DWN and processes them on the local DWN
44
52
  * in dependency order (topological sort).
45
53
  *
46
- * Messages that fail processing are re-fetched from the remote before each retry
47
- * pass rather than buffered in memory. ReadableStream is single-use, so a failed
48
- * message's data stream is consumed on the first attempt. Re-fetching provides a
49
- * fresh stream without holding all record data in memory simultaneously.
54
+ * Small data payloads (≤ 1 MB) are buffered during the initial fetch so that
55
+ * retries can replay the data from memory instead of re-fetching from remote.
56
+ * Large payloads are re-fetched on retry since buffering them would consume
57
+ * too much memory.
50
58
  */
51
- export async function pullMessages({ did, dwnUrl, delegateDid, protocol, messageCids, agent, permissionsApi }: {
59
+ export async function pullMessages({ did, dwnUrl, delegateDid, protocol, messageCids, prefetched, agent, permissionsApi }: {
52
60
  did: string;
53
61
  dwnUrl: string;
54
62
  delegateDid?: string;
55
63
  protocol?: string;
56
64
  messageCids: string[];
65
+ /** Pre-fetched message entries from the batched diff response (already have message + data). */
66
+ prefetched?: MessagesSyncDiffEntry[];
57
67
  agent: EnboxPlatformAgent;
58
68
  permissionsApi: PermissionsApi;
59
69
  }): Promise<void> {
60
- // Step 1: Fetch all missing messages from the remote in parallel.
61
- const fetched = await fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids, agent, permissionsApi });
70
+ // Convert prefetched diff entries into SyncMessageEntry format.
71
+ const prefetchedEntries: SyncMessageEntry[] = [];
72
+ if (prefetched) {
73
+ for (const entry of prefetched) {
74
+ if (!entry.message) { continue; }
75
+ const syncEntry: SyncMessageEntry = { message: entry.message };
76
+ if (entry.encodedData) {
77
+ // Convert base64url-encoded data to a ReadableStream.
78
+ const bytes = Encoder.base64UrlToBytes(entry.encodedData);
79
+ syncEntry.bufferedData = bytes;
80
+ syncEntry.dataStream = new ReadableStream<Uint8Array>({
81
+ start(controller): void {
82
+ controller.enqueue(bytes);
83
+ controller.close();
84
+ }
85
+ });
86
+ }
87
+ prefetchedEntries.push(syncEntry);
88
+ }
89
+ }
90
+
91
+ // Step 1: Fetch remaining messages (not prefetched) from the remote.
92
+ const fetched = messageCids.length > 0
93
+ ? await fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids, agent, permissionsApi })
94
+ : [];
95
+
96
+ // Merge prefetched entries with remotely fetched ones.
97
+ const allFetched = [...prefetchedEntries, ...fetched];
62
98
 
63
99
  // Step 2: Build dependency graph and topological sort.
64
- const sorted = topologicalSort(fetched);
100
+ const sorted = topologicalSort(allFetched);
101
+
102
+ // Step 3: Buffer small data streams so they can be replayed on retry.
103
+ await bufferSmallStreams(sorted);
65
104
 
66
- // Step 3: Process messages in dependency order with multi-pass retry.
67
- // Retry up to MAX_RETRY_PASSES times for messages that fail due to
68
- // dependency ordering issues (e.g., a RecordsWrite whose ProtocolsConfigure
69
- // hasn't committed yet). Failed messages are re-fetched from the remote
70
- // to obtain a fresh data stream, since ReadableStream is single-use.
105
+ // Step 4: Process messages in dependency order with multi-pass retry.
71
106
  const MAX_RETRY_PASSES = 3;
72
107
  let pending = sorted;
73
108
 
74
109
  for (let pass = 0; pass <= MAX_RETRY_PASSES && pending.length > 0; pass++) {
75
- const failedCids: string[] = [];
110
+ const failed: SyncMessageEntry[] = [];
76
111
 
77
112
  for (const entry of pending) {
78
- const pullReply = await agent.dwn.processRawMessage(did, entry.message, { dataStream: entry.dataStream });
113
+ // Create a fresh ReadableStream from the buffer if available (stream is single-use).
114
+ const dataStream = entry.bufferedData
115
+ ? new ReadableStream<Uint8Array>({ start(c): void { c.enqueue(entry.bufferedData!); c.close(); } })
116
+ : entry.dataStream;
117
+
118
+ const pullReply = await agent.dwn.processRawMessage(did, entry.message, { dataStream });
119
+
79
120
  if (!syncMessageReplyIsSuccessful(pullReply)) {
80
- const cid = await getMessageCid(entry.message);
81
- failedCids.push(cid);
121
+ failed.push(entry);
82
122
  }
83
123
  }
84
124
 
85
- // Re-fetch failed messages from the remote to get fresh data streams.
86
- if (failedCids.length > 0) {
87
- const reFetched = await fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids: failedCids, agent, permissionsApi });
88
- pending = topologicalSort(reFetched);
125
+ if (failed.length > 0) {
126
+ // Separate entries that have a buffer (can retry locally) from those
127
+ // that need a fresh fetch (large payloads whose stream was consumed).
128
+ const needsRefetch: string[] = [];
129
+ const canRetry: SyncMessageEntry[] = [];
130
+
131
+ for (const entry of failed) {
132
+ if (entry.bufferedData || !entry.dataStream) {
133
+ // Has a buffer or has no data — can retry without re-fetching.
134
+ canRetry.push(entry);
135
+ } else {
136
+ // Large payload whose stream was consumed — must re-fetch.
137
+ const cid = await getMessageCid(entry.message);
138
+ needsRefetch.push(cid);
139
+ }
140
+ }
141
+
142
+ // Re-fetch only the large-payload messages that we couldn't buffer.
143
+ if (needsRefetch.length > 0) {
144
+ const reFetched = await fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids: needsRefetch, agent, permissionsApi });
145
+ canRetry.push(...reFetched);
146
+ }
147
+
148
+ pending = topologicalSort(canRetry);
89
149
  } else {
90
150
  pending = [];
91
151
  }
92
152
  }
93
153
  }
94
154
 
155
+ /**
156
+ * Buffers small data streams into `Uint8Array` so they can be replayed on retry.
157
+ * Streams larger than `MAX_BUFFER_SIZE` are left as-is (will be re-fetched on retry).
158
+ */
159
+ async function bufferSmallStreams(entries: SyncMessageEntry[]): Promise<void> {
160
+ for (const entry of entries) {
161
+ if (!entry.dataStream) {
162
+ continue;
163
+ }
164
+
165
+ // Read the stream into memory. If it exceeds the threshold, stop and
166
+ // leave the entry without a buffer (it will be re-fetched on retry).
167
+ const chunks: Uint8Array[] = [];
168
+ let totalSize = 0;
169
+ let exceededThreshold = false;
170
+ const reader = entry.dataStream.getReader();
171
+
172
+ try {
173
+ for (;;) {
174
+ const { done, value } = await reader.read();
175
+ if (done) { break; }
176
+ totalSize += value.byteLength;
177
+ if (totalSize > MAX_BUFFER_SIZE) {
178
+ exceededThreshold = true;
179
+ break;
180
+ }
181
+ chunks.push(value);
182
+ }
183
+ } finally {
184
+ reader.releaseLock();
185
+ }
186
+
187
+ if (exceededThreshold) {
188
+ // Stream exceeded the buffer threshold. Leave dataStream consumed —
189
+ // the retry path will re-fetch from remote.
190
+ entry.dataStream = undefined;
191
+ continue;
192
+ }
193
+
194
+ // Combine chunks into a single Uint8Array buffer.
195
+ const buffer = new Uint8Array(totalSize);
196
+ let offset = 0;
197
+ for (const chunk of chunks) {
198
+ buffer.set(chunk, offset);
199
+ offset += chunk.byteLength;
200
+ }
201
+
202
+ entry.bufferedData = buffer;
203
+ // Create a fresh ReadableStream from the buffer for the first processing attempt.
204
+ entry.dataStream = new ReadableStream<Uint8Array>({
205
+ start(controller): void {
206
+ controller.enqueue(buffer);
207
+ controller.close();
208
+ }
209
+ });
210
+ }
211
+ }
212
+
95
213
  /**
96
214
  * Fetches messages from a remote DWN by their CIDs using MessagesRead.
97
215
  */