@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.
- package/dist/browser.mjs +9 -9
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/sync-engine-level.js +220 -9
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +126 -22
- package/dist/esm/sync-messages.js.map +1 -1
- package/dist/types/sync-engine-level.d.ts +61 -1
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +11 -7
- package/dist/types/sync-messages.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/sync-engine-level.ts +247 -14
- package/src/sync-messages.ts +143 -25
package/src/sync-messages.ts
CHANGED
|
@@ -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
|
-
/**
|
|
12
|
-
|
|
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
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
//
|
|
61
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
110
|
+
const failed: SyncMessageEntry[] = [];
|
|
76
111
|
|
|
77
112
|
for (const entry of pending) {
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
failedCids.push(cid);
|
|
121
|
+
failed.push(entry);
|
|
82
122
|
}
|
|
83
123
|
}
|
|
84
124
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
*/
|