@beam-network/sdk 0.5.0 → 0.5.1
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/README.md +5 -1
- package/dist/client.d.ts +14 -2
- package/dist/client.js +316 -169
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/models.d.ts +10 -58
- package/dist/nats-control.d.ts +33 -16
- package/dist/nats-control.js +186 -59
- package/dist/provider-signing.js +1 -2
- package/package.json +35 -35
- package/dist/errors.d.ts +0 -17
- package/dist/errors.js +0 -26
package/dist/client.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signListObjects, signUploadPartCopy } from "./provider-signing.js";
|
|
2
|
-
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes } from "./nats-control.js";
|
|
2
|
+
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
|
|
3
3
|
import { multipartPartNumber } from "./multipart-limits.js";
|
|
4
4
|
export { BEAM_DEFAULT_NATS_URL };
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export class BeamApiError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
body;
|
|
8
|
+
constructor(message, status, body) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.name = "BeamApiError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
8
15
|
export class BeamClient {
|
|
9
16
|
apiKey;
|
|
10
17
|
natsUrl;
|
|
@@ -12,10 +19,6 @@ export class BeamClient {
|
|
|
12
19
|
control;
|
|
13
20
|
routeSigningConcurrency;
|
|
14
21
|
routeSigningConcurrencyOverridden;
|
|
15
|
-
/** Last credit state Beam reported. Null until the first request or connect. */
|
|
16
|
-
get creditStatus() {
|
|
17
|
-
return this.control.creditStatus;
|
|
18
|
-
}
|
|
19
22
|
constructor(options = {}) {
|
|
20
23
|
if (!options.apiKey?.trim()) {
|
|
21
24
|
throw new Error("apiKey is required.");
|
|
@@ -32,8 +35,7 @@ export class BeamClient {
|
|
|
32
35
|
subjectPrefix: options.transferClientSubjectPrefix,
|
|
33
36
|
transferRuntimeShardCount: options.transferRuntimeShardCount,
|
|
34
37
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
35
|
-
maxPayloadBytes: options.maxPayloadBytes
|
|
36
|
-
onCreditWarning: options.onCreditWarning
|
|
38
|
+
maxPayloadBytes: options.maxPayloadBytes
|
|
37
39
|
});
|
|
38
40
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
39
41
|
if (!this.fetchImpl) {
|
|
@@ -71,9 +73,12 @@ export class BeamClient {
|
|
|
71
73
|
createTransfer(input) {
|
|
72
74
|
return this.prepareProviderTransfer(input);
|
|
73
75
|
}
|
|
74
|
-
transferStatus(transferId) {
|
|
76
|
+
async transferStatus(transferId) {
|
|
75
77
|
validateId(transferId, "transferId");
|
|
76
|
-
|
|
78
|
+
const status = await this.control.request("transfer.status", { transfer_id: transferId }, { transferId });
|
|
79
|
+
if (["completed", "failed", "cancelled"].includes(status.status))
|
|
80
|
+
this.control.releaseRecoveryLease(transferId);
|
|
81
|
+
return status;
|
|
77
82
|
}
|
|
78
83
|
distributeTransfer(transferId) {
|
|
79
84
|
validateId(transferId, "transferId");
|
|
@@ -88,6 +93,7 @@ export class BeamClient {
|
|
|
88
93
|
transferId,
|
|
89
94
|
idempotencyKey: `transfer:${transferId}:cancel`
|
|
90
95
|
});
|
|
96
|
+
this.control.releaseRecoveryLease(transferId);
|
|
91
97
|
return result;
|
|
92
98
|
}
|
|
93
99
|
async planTransfer(input) {
|
|
@@ -105,8 +111,10 @@ export class BeamClient {
|
|
|
105
111
|
}
|
|
106
112
|
async prepareTransfer(input) {
|
|
107
113
|
const transferId = await transferIdForIdempotencyKey(input.idempotencyKey);
|
|
114
|
+
const routeGenerationId = input.routeGenerationId ?? randomUuid();
|
|
108
115
|
const result = await this.control.request("transfer.prepare", compact({
|
|
109
116
|
transfer_id: transferId,
|
|
117
|
+
route_generation_id: routeGenerationId,
|
|
110
118
|
sources: input.sources,
|
|
111
119
|
destinations: input.destinations,
|
|
112
120
|
name: input.name,
|
|
@@ -125,56 +133,97 @@ export class BeamClient {
|
|
|
125
133
|
validateId(transferId, "transferId");
|
|
126
134
|
validateMultipartGroupManifest(input.multipartGroupManifest, transferId);
|
|
127
135
|
validateSignedRouteManifestContract(input.chunkRoutes, input.multipartGroupManifest);
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
136
|
+
const streamRoutes = async (chunkRoutes, multipartGroupManifest, routeGenerationId, urlsExpiresAt) => {
|
|
137
|
+
validateMultipartGroupManifest(multipartGroupManifest, transferId);
|
|
138
|
+
validateSignedRouteManifestContract(chunkRoutes, multipartGroupManifest);
|
|
139
|
+
const destinationCount = new Set(chunkRoutes.map((route) => route.destination_id)).size;
|
|
140
|
+
const orderedRoutes = chunkRoutes.map((route) => {
|
|
141
|
+
if (signedRouteDeliveryIndex(route) !== undefined)
|
|
142
|
+
return route;
|
|
143
|
+
if (destinationCount !== 1) {
|
|
144
|
+
throw new Error("delivery_index is required when manually attaching routes for multiple destinations");
|
|
145
|
+
}
|
|
146
|
+
return { ...route, delivery_index: route.chunk_index };
|
|
147
|
+
})
|
|
148
|
+
.sort(compareSignedRoutesByDelivery);
|
|
149
|
+
const streamId = await stableRouteStreamId({
|
|
150
|
+
transferId,
|
|
151
|
+
planIdentity: `${await routeCoordinateChecksum(orderedRoutes)}:${routeGenerationId}`,
|
|
152
|
+
totalRoutes: orderedRoutes.length,
|
|
153
|
+
totalChunks: countDistinctRouteChunks(orderedRoutes),
|
|
154
|
+
signedUrlFlow: multipartGroupManifest.length ? "signed_url_v2" : "signed_url_v1"
|
|
155
|
+
});
|
|
156
|
+
const stream = new RouteStreamSender(this.control, {
|
|
157
|
+
streamId,
|
|
158
|
+
transferId,
|
|
159
|
+
routeGenerationId,
|
|
160
|
+
totalRoutes: orderedRoutes.length,
|
|
161
|
+
totalChunks: countDistinctRouteChunks(orderedRoutes),
|
|
162
|
+
urlsExpiresAt,
|
|
163
|
+
signedUrlFlow: multipartGroupManifest.length ? "signed_url_v2" : "signed_url_v1",
|
|
164
|
+
autoDistribute: input.autoDistribute ?? true
|
|
165
|
+
});
|
|
166
|
+
await stream.begin();
|
|
167
|
+
if (multipartGroupManifest.length)
|
|
168
|
+
await stream.addManifestGroups(multipartGroupManifest);
|
|
169
|
+
for (const route of orderedRoutes)
|
|
170
|
+
await stream.addRoute(route);
|
|
171
|
+
return stream.complete();
|
|
172
|
+
};
|
|
173
|
+
const routeStreamLock = new AsyncMutex();
|
|
174
|
+
const releaseInitialStream = await routeStreamLock.acquire();
|
|
175
|
+
this.control.registerRecoveryLease({
|
|
148
176
|
transferId,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
planFingerprint: input.planFingerprint,
|
|
178
|
+
coordinateChecksum: input.coordinateChecksum,
|
|
179
|
+
replayRoutes: async (routeGenerationId) => {
|
|
180
|
+
await routeStreamLock.run(async () => {
|
|
181
|
+
const fresh = await input.recoveryFactory(routeGenerationId);
|
|
182
|
+
const attached = await streamRoutes(fresh.chunkRoutes, fresh.multipartGroupManifest, routeGenerationId, fresh.urlsExpiresAt);
|
|
183
|
+
if (!attached.success)
|
|
184
|
+
throw new Error(attached.error ?? attached.message ?? "route replay failed");
|
|
185
|
+
});
|
|
186
|
+
}
|
|
154
187
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
try {
|
|
189
|
+
const attached = await streamRoutes(input.chunkRoutes, input.multipartGroupManifest, input.routeGenerationId, input.urlsExpiresAt);
|
|
190
|
+
if (!attached.success)
|
|
191
|
+
this.control.releaseRecoveryLease(transferId);
|
|
192
|
+
return attached;
|
|
158
193
|
}
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (isRecoverableRouteStreamError(error))
|
|
196
|
+
this.control.continueRecoveryLease(transferId);
|
|
197
|
+
else
|
|
198
|
+
this.control.releaseRecoveryLease(transferId);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
releaseInitialStream();
|
|
161
203
|
}
|
|
162
|
-
return stream.complete();
|
|
163
204
|
}
|
|
164
205
|
async prepareProviderTransfer(input) {
|
|
165
206
|
const expiresIn = input.expiresIn ?? 3600;
|
|
166
207
|
const requestedSignedUrlFlow = input.signedUrlFlow ?? "signed_url_v1";
|
|
167
|
-
const
|
|
208
|
+
const retainedRecoveryInput = {
|
|
209
|
+
...input,
|
|
210
|
+
sources: input.sources.map((source) => ({ ...source })),
|
|
211
|
+
destinations: input.destinations.map((destination) => ({ ...destination })),
|
|
212
|
+
onBeforeTransferPrepare: undefined,
|
|
213
|
+
onPrepared: undefined,
|
|
214
|
+
throwIfCancelled: undefined
|
|
215
|
+
};
|
|
216
|
+
const preparedDestinations = retainedRecoveryInput.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
|
|
168
217
|
const destinationsById = new Map();
|
|
169
218
|
preparedDestinations.forEach((preparedDestination, index) => {
|
|
170
|
-
const destination =
|
|
219
|
+
const destination = retainedRecoveryInput.destinations[index];
|
|
171
220
|
if (!destination) {
|
|
172
221
|
throw new Error(`missing provider destination config for prepared destination ${preparedDestination.destination_id}`);
|
|
173
222
|
}
|
|
174
223
|
destinationsById.set(preparedDestination.destination_id, destination);
|
|
175
224
|
});
|
|
176
225
|
await input.throwIfCancelled?.();
|
|
177
|
-
const preparedSources = await Promise.all(
|
|
226
|
+
const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
|
|
178
227
|
await input.throwIfCancelled?.();
|
|
179
228
|
await input.onBeforeTransferPrepare?.();
|
|
180
229
|
const prepared = await this.prepareTransfer({
|
|
@@ -183,145 +232,201 @@ export class BeamClient {
|
|
|
183
232
|
name: input.name,
|
|
184
233
|
testMode: input.testMode,
|
|
185
234
|
signedUrlFlow: requestedSignedUrlFlow,
|
|
186
|
-
idempotencyKey: input.idempotencyKey
|
|
235
|
+
idempotencyKey: input.idempotencyKey,
|
|
236
|
+
routeGenerationId: input.routeGenerationId
|
|
187
237
|
});
|
|
188
238
|
if (!prepared.success) {
|
|
189
239
|
return prepared;
|
|
190
240
|
}
|
|
191
|
-
await input.onPrepared?.(prepared);
|
|
192
|
-
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
193
241
|
const sourcesById = new Map();
|
|
194
242
|
preparedSources.forEach((preparedSource, index) => {
|
|
195
|
-
const source =
|
|
243
|
+
const source = retainedRecoveryInput.sources[index];
|
|
196
244
|
if (!source) {
|
|
197
245
|
throw new Error(`missing provider source config for prepared source ${preparedSource.source_id}`);
|
|
198
246
|
}
|
|
199
247
|
sourcesById.set(preparedSource.source_id, source);
|
|
200
248
|
});
|
|
201
249
|
const multipartUploads = new Map();
|
|
202
|
-
const
|
|
203
|
-
let
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
const autoDistribute = input.distribute !== false;
|
|
251
|
+
let initialThrowIfCancelled = input.throwIfCancelled;
|
|
252
|
+
const routeStreamLock = new AsyncMutex();
|
|
253
|
+
const releaseInitialStream = await routeStreamLock.acquire();
|
|
254
|
+
const streamPreparedRoutes = async (routeGenerationId, recoveryReplay) => {
|
|
255
|
+
const pendingRoutes = new Set();
|
|
256
|
+
let signingConcurrency = this.routeSigningConcurrency;
|
|
257
|
+
let signedInWindow = 0;
|
|
258
|
+
let signingWindowStartedAt = performance.now();
|
|
259
|
+
let routeStream = null;
|
|
260
|
+
let routeStreamBeginAttempted = false;
|
|
261
|
+
let multipartManifestTask = null;
|
|
262
|
+
let multipartGroupWaiters = new Map();
|
|
263
|
+
let foregroundCancelled = false;
|
|
264
|
+
const throwIfCancelled = recoveryReplay || !initialThrowIfCancelled ? undefined : async (transferId) => {
|
|
265
|
+
try {
|
|
266
|
+
await initialThrowIfCancelled?.(transferId);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
foregroundCancelled = true;
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
try {
|
|
274
|
+
const streamId = await stableRouteStreamId({
|
|
275
|
+
transferId: prepared.transfer_id,
|
|
276
|
+
planIdentity: `${prepared.plan_descriptor.plan_nonce}:${routeGenerationId}`,
|
|
277
|
+
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
278
|
+
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
279
|
+
signedUrlFlow: requestedSignedUrlFlow
|
|
280
|
+
});
|
|
281
|
+
routeStream = new RouteStreamSender(this.control, {
|
|
282
|
+
streamId,
|
|
283
|
+
transferId: prepared.transfer_id,
|
|
284
|
+
routeGenerationId,
|
|
285
|
+
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
286
|
+
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
287
|
+
signedUrlFlow: requestedSignedUrlFlow,
|
|
288
|
+
autoDistribute,
|
|
289
|
+
urlsExpiresAt: expiresAtIso(expiresIn)
|
|
290
|
+
});
|
|
291
|
+
routeStreamBeginAttempted = true;
|
|
292
|
+
await routeStream.begin();
|
|
293
|
+
multipartGroupWaiters = createMultipartGroupWaiters(prepared, destinationsById);
|
|
294
|
+
multipartManifestTask = createMultipartGroupManifest({
|
|
295
|
+
prepared,
|
|
296
|
+
destinationsById,
|
|
297
|
+
multipartUploads,
|
|
298
|
+
expiresIn,
|
|
299
|
+
signedUrlFlow: requestedSignedUrlFlow,
|
|
300
|
+
concurrency: this.routeSigningConcurrency,
|
|
301
|
+
preserveUploadsOnFailure: recoveryReplay,
|
|
302
|
+
onGroupReady: async (state) => {
|
|
303
|
+
if (requestedSignedUrlFlow === "signed_url_v2") {
|
|
304
|
+
validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
|
|
305
|
+
await routeStream.addManifestGroups([state.manifest]);
|
|
306
|
+
}
|
|
307
|
+
multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
|
|
308
|
+
},
|
|
309
|
+
onGroupFailed: (groupId, error) => multipartGroupWaiters.get(groupId)?.reject(error)
|
|
310
|
+
});
|
|
311
|
+
const streamNextCompletedRoute = async () => {
|
|
312
|
+
const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
|
|
313
|
+
pendingRoutes.delete(settled.pending);
|
|
314
|
+
await routeStream.addRoute(settled.route);
|
|
315
|
+
signedInWindow += 1;
|
|
316
|
+
if (signedInWindow === ROUTE_STREAM_BATCH_ROUTES) {
|
|
317
|
+
const elapsedMs = performance.now() - signingWindowStartedAt;
|
|
318
|
+
if (!this.routeSigningConcurrencyOverridden && elapsedMs > 4_000 && signingConcurrency < 256) {
|
|
319
|
+
signingConcurrency = Math.min(256, signingConcurrency * 2);
|
|
320
|
+
}
|
|
321
|
+
signedInWindow = 0;
|
|
322
|
+
signingWindowStartedAt = performance.now();
|
|
240
323
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
324
|
+
};
|
|
325
|
+
for (const chunk of materializePlanChunks(prepared.plan_descriptor, prepared.transfer_id)) {
|
|
326
|
+
await throwIfCancelled?.(prepared.transfer_id);
|
|
327
|
+
for (const target of chunk.destinations) {
|
|
328
|
+
const pendingRoute = (async () => {
|
|
329
|
+
await throwIfCancelled?.(prepared.transfer_id);
|
|
330
|
+
const destination = destinationsById.get(target.destination_id);
|
|
331
|
+
if (!destination)
|
|
332
|
+
throw new Error(`BeamCore returned unknown destination_id: ${target.destination_id}`);
|
|
333
|
+
const source = sourcesById.get(chunk.source_id);
|
|
334
|
+
if (!source)
|
|
335
|
+
throw new Error(`BeamCore returned unknown source_id: ${chunk.source_id}`);
|
|
336
|
+
const metadata = target.metadata ?? {};
|
|
337
|
+
const finalObjectKey = typeof metadata.final_object_key === "string" ? metadata.final_object_key : target.object_key;
|
|
338
|
+
if (!finalObjectKey)
|
|
339
|
+
throw new Error("destination signing target is missing object_key");
|
|
340
|
+
const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
|
|
341
|
+
const upload = isHippiusDestination(destination)
|
|
342
|
+
? undefined
|
|
343
|
+
: await multipartGroupWaiters.get(multipartGroupId)?.promise;
|
|
344
|
+
if (!isHippiusDestination(destination) && !upload)
|
|
345
|
+
throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
|
|
346
|
+
return this.signProviderRoute({
|
|
347
|
+
chunk,
|
|
348
|
+
target,
|
|
349
|
+
source,
|
|
350
|
+
destination,
|
|
351
|
+
expiresIn,
|
|
352
|
+
...(upload ? { upload } : {}),
|
|
353
|
+
transferId: prepared.transfer_id,
|
|
354
|
+
finalObjectKey,
|
|
355
|
+
signedUrlFlow: requestedSignedUrlFlow,
|
|
356
|
+
partNumber: typeof metadata.part_number === "number"
|
|
357
|
+
? metadata.part_number
|
|
358
|
+
: multipartPartNumber(chunk.source_chunk_index),
|
|
359
|
+
});
|
|
360
|
+
})();
|
|
361
|
+
pendingRoutes.add(pendingRoute);
|
|
362
|
+
if (pendingRoutes.size >= signingConcurrency)
|
|
363
|
+
await streamNextCompletedRoute();
|
|
254
364
|
}
|
|
255
|
-
signedInWindow = 0;
|
|
256
|
-
signingWindowStartedAt = performance.now();
|
|
257
365
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
source,
|
|
284
|
-
destination,
|
|
285
|
-
expiresIn,
|
|
286
|
-
...(upload ? { upload } : {}),
|
|
366
|
+
while (pendingRoutes.size)
|
|
367
|
+
await streamNextCompletedRoute();
|
|
368
|
+
const multipartGroupManifest = await multipartManifestTask;
|
|
369
|
+
if (requestedSignedUrlFlow === "signed_url_v2") {
|
|
370
|
+
validateMultipartGroupManifest(multipartGroupManifest, prepared.transfer_id);
|
|
371
|
+
}
|
|
372
|
+
const attached = await routeStream.complete();
|
|
373
|
+
if (!attached.success) {
|
|
374
|
+
throw new Error(attached.error ?? attached.message ?? "route stream failed");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (routeStream)
|
|
379
|
+
await routeStream.abort().catch(() => undefined);
|
|
380
|
+
await Promise.allSettled(pendingRoutes);
|
|
381
|
+
if (multipartManifestTask)
|
|
382
|
+
await Promise.allSettled([multipartManifestTask]);
|
|
383
|
+
if (!recoveryReplay) {
|
|
384
|
+
if (foregroundCancelled || isRecoverableRouteStreamError(error)) {
|
|
385
|
+
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
this.control.releaseRecoveryLease(prepared.transfer_id);
|
|
389
|
+
await cancelAndAbortProviderFailure({
|
|
390
|
+
client: this,
|
|
287
391
|
transferId: prepared.transfer_id,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
? metadata.part_number
|
|
292
|
-
: multipartPartNumber(chunk.source_chunk_index),
|
|
392
|
+
cause: error,
|
|
393
|
+
uploads: multipartUploads,
|
|
394
|
+
abortBeforeCancel: !routeStreamBeginAttempted
|
|
293
395
|
});
|
|
294
|
-
}
|
|
295
|
-
pendingRoutes.add(pendingRoute);
|
|
296
|
-
if (pendingRoutes.size >= signingConcurrency)
|
|
297
|
-
await streamNextCompletedRoute();
|
|
396
|
+
}
|
|
298
397
|
}
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
this.control.registerRecoveryLease({
|
|
402
|
+
transferId: prepared.transfer_id,
|
|
403
|
+
planFingerprint: prepared.plan_fingerprint,
|
|
404
|
+
coordinateChecksum: prepared.coordinate_checksum,
|
|
405
|
+
replayRoutes: async (routeGenerationId) => {
|
|
406
|
+
await routeStreamLock.run(() => streamPreparedRoutes(routeGenerationId, true));
|
|
407
|
+
},
|
|
408
|
+
disposeSecrets: () => clearRecoverySecrets(retainedRecoveryInput)
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
try {
|
|
412
|
+
await input.onPrepared?.(prepared);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
416
|
+
throw error;
|
|
299
417
|
}
|
|
300
|
-
|
|
301
|
-
await
|
|
302
|
-
const multipartGroupManifest = await multipartManifestTask;
|
|
303
|
-
if (requestedSignedUrlFlow === "signed_url_v2") {
|
|
304
|
-
validateMultipartGroupManifest(multipartGroupManifest, prepared.transfer_id);
|
|
418
|
+
try {
|
|
419
|
+
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
305
420
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
throw
|
|
421
|
+
catch (error) {
|
|
422
|
+
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
423
|
+
throw error;
|
|
309
424
|
}
|
|
425
|
+
await streamPreparedRoutes(prepared.route_generation_id, false);
|
|
426
|
+
initialThrowIfCancelled = undefined;
|
|
310
427
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
await routeStream.abort();
|
|
314
|
-
await Promise.allSettled(pendingRoutes);
|
|
315
|
-
if (multipartManifestTask)
|
|
316
|
-
await Promise.allSettled([multipartManifestTask]);
|
|
317
|
-
await cancelAndAbortProviderFailure({
|
|
318
|
-
client: this,
|
|
319
|
-
transferId: prepared.transfer_id,
|
|
320
|
-
cause: error,
|
|
321
|
-
uploads: multipartUploads,
|
|
322
|
-
abortBeforeCancel: !routeStreamBeginAttempted
|
|
323
|
-
});
|
|
324
|
-
throw error;
|
|
428
|
+
finally {
|
|
429
|
+
releaseInitialStream();
|
|
325
430
|
}
|
|
326
431
|
return prepared;
|
|
327
432
|
}
|
|
@@ -486,6 +591,27 @@ export class BeamClient {
|
|
|
486
591
|
}
|
|
487
592
|
}
|
|
488
593
|
const ROUTE_STREAM_BATCH_ROUTES = 2_048;
|
|
594
|
+
class AsyncMutex {
|
|
595
|
+
tail = Promise.resolve();
|
|
596
|
+
async acquire() {
|
|
597
|
+
const previous = this.tail;
|
|
598
|
+
let release;
|
|
599
|
+
this.tail = new Promise((resolve) => {
|
|
600
|
+
release = resolve;
|
|
601
|
+
});
|
|
602
|
+
await previous;
|
|
603
|
+
return release;
|
|
604
|
+
}
|
|
605
|
+
async run(operation) {
|
|
606
|
+
const release = await this.acquire();
|
|
607
|
+
try {
|
|
608
|
+
return await operation();
|
|
609
|
+
}
|
|
610
|
+
finally {
|
|
611
|
+
release();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
489
615
|
class RouteStreamSender {
|
|
490
616
|
control;
|
|
491
617
|
options;
|
|
@@ -505,6 +631,7 @@ class RouteStreamSender {
|
|
|
505
631
|
async begin() {
|
|
506
632
|
await this.control.request("transfer.route_stream.begin", compact({
|
|
507
633
|
transfer_id: this.options.transferId,
|
|
634
|
+
route_generation_id: this.options.routeGenerationId,
|
|
508
635
|
stream_id: this.streamId,
|
|
509
636
|
total_routes: this.options.totalRoutes,
|
|
510
637
|
total_chunks: this.options.totalChunks,
|
|
@@ -524,6 +651,7 @@ class RouteStreamSender {
|
|
|
524
651
|
const identity = await stableManifestBatchIdentity([group]);
|
|
525
652
|
await this.control.request("transfer.route_stream.manifest", {
|
|
526
653
|
transfer_id: this.options.transferId,
|
|
654
|
+
route_generation_id: this.options.routeGenerationId,
|
|
527
655
|
stream_id: this.streamId,
|
|
528
656
|
manifest_batch_id: identity,
|
|
529
657
|
groups: [group]
|
|
@@ -562,6 +690,7 @@ class RouteStreamSender {
|
|
|
562
690
|
throw this.sendError;
|
|
563
691
|
return this.control.request("transfer.route_stream.complete", {
|
|
564
692
|
transfer_id: this.options.transferId,
|
|
693
|
+
route_generation_id: this.options.routeGenerationId,
|
|
565
694
|
stream_id: this.streamId,
|
|
566
695
|
expected_batches: this.batchIndex,
|
|
567
696
|
expected_routes: this.options.totalRoutes,
|
|
@@ -580,6 +709,7 @@ class RouteStreamSender {
|
|
|
580
709
|
const routes = this.batch.splice(0, this.batch.length);
|
|
581
710
|
const chunks = this.control.splitRoutesForPayload("transfer.route_stream.batch", {
|
|
582
711
|
transfer_id: this.options.transferId,
|
|
712
|
+
route_generation_id: this.options.routeGenerationId,
|
|
583
713
|
stream_id: this.streamId,
|
|
584
714
|
batch_id: `${this.streamId}:estimate`,
|
|
585
715
|
batch_index: this.batchIndex
|
|
@@ -599,6 +729,7 @@ class RouteStreamSender {
|
|
|
599
729
|
const batchId = `${this.streamId}:${batchIndex}:${coordinateChecksum}`;
|
|
600
730
|
await this.control.request("transfer.route_stream.batch", {
|
|
601
731
|
transfer_id: this.options.transferId,
|
|
732
|
+
route_generation_id: this.options.routeGenerationId,
|
|
602
733
|
stream_id: this.streamId,
|
|
603
734
|
batch_id: batchId,
|
|
604
735
|
batch_index: batchIndex,
|
|
@@ -878,11 +1009,13 @@ async function createMultipartGroupManifest(input) {
|
|
|
878
1009
|
"beam-transfer-id": input.prepared.transfer_id,
|
|
879
1010
|
...(input.signedUrlFlow === "signed_url_v2" ? { "beam-multipart-group-id": multipartGroupId } : {})
|
|
880
1011
|
};
|
|
881
|
-
const
|
|
1012
|
+
const retainedUpload = input.multipartUploads.get(multipartGroupId);
|
|
1013
|
+
const uploadId = retainedUpload?.uploadId ?? await createMultipartUpload({
|
|
882
1014
|
destination: destinationConfig,
|
|
883
1015
|
objectKey: finalObjectKey,
|
|
884
1016
|
metadata: finalObjectMetadata
|
|
885
1017
|
});
|
|
1018
|
+
const createdUpload = !retainedUpload;
|
|
886
1019
|
try {
|
|
887
1020
|
const urlsExpiresAt = expiresAtIso(input.expiresIn);
|
|
888
1021
|
const stagingObjectPrefix = buildStagingObjectPrefix({
|
|
@@ -940,11 +1073,13 @@ async function createMultipartGroupManifest(input) {
|
|
|
940
1073
|
return { ok: true, state };
|
|
941
1074
|
}
|
|
942
1075
|
catch (error) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1076
|
+
if (createdUpload) {
|
|
1077
|
+
try {
|
|
1078
|
+
await abortMultipartUpload(destinationConfig, finalObjectKey, uploadId);
|
|
1079
|
+
}
|
|
1080
|
+
catch (abortError) {
|
|
1081
|
+
throw new AggregateError([error, abortError], `multipart group setup and cleanup failed for ${multipartGroupId}`);
|
|
1082
|
+
}
|
|
948
1083
|
}
|
|
949
1084
|
throw error;
|
|
950
1085
|
}
|
|
@@ -957,6 +1092,8 @@ async function createMultipartGroupManifest(input) {
|
|
|
957
1092
|
});
|
|
958
1093
|
const failure = results.find((result) => !result.ok);
|
|
959
1094
|
if (failure && !failure.ok) {
|
|
1095
|
+
if (input.preserveUploadsOnFailure)
|
|
1096
|
+
throw failure.error;
|
|
960
1097
|
try {
|
|
961
1098
|
await abortCreatedUploads(input.multipartUploads);
|
|
962
1099
|
}
|
|
@@ -1235,3 +1372,13 @@ function isHippiusDestination(destination) {
|
|
|
1235
1372
|
function sleep(ms) {
|
|
1236
1373
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1237
1374
|
}
|
|
1375
|
+
function clearRecoverySecrets(input) {
|
|
1376
|
+
const secretKeys = new Set(["access_key_id", "secret_access_key", "session_token", "api_token", "token", "password"]);
|
|
1377
|
+
for (const provider of [...input.sources, ...input.destinations]) {
|
|
1378
|
+
const record = provider;
|
|
1379
|
+
for (const key of secretKeys) {
|
|
1380
|
+
if (key in record)
|
|
1381
|
+
record[key] = "";
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|