@beam-network/sdk 0.5.0 → 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/README.md +6 -2
- package/dist/client.d.ts +19 -2
- package/dist/client.js +331 -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 +187 -60
- 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,27 @@
|
|
|
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
|
+
export class BeamRouteRecoveryPendingError extends Error {
|
|
4
|
+
transferId;
|
|
5
|
+
cause;
|
|
6
|
+
constructor(transferId, cause) {
|
|
7
|
+
super(`Transfer ${transferId} is prepared and route recovery is continuing in the background.`);
|
|
8
|
+
this.name = "BeamRouteRecoveryPendingError";
|
|
9
|
+
this.transferId = transferId;
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
3
13
|
import { multipartPartNumber } from "./multipart-limits.js";
|
|
4
14
|
export { BEAM_DEFAULT_NATS_URL };
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
15
|
+
export class BeamApiError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
body;
|
|
18
|
+
constructor(message, status, body) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.body = body;
|
|
22
|
+
this.name = "BeamApiError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
8
25
|
export class BeamClient {
|
|
9
26
|
apiKey;
|
|
10
27
|
natsUrl;
|
|
@@ -12,10 +29,6 @@ export class BeamClient {
|
|
|
12
29
|
control;
|
|
13
30
|
routeSigningConcurrency;
|
|
14
31
|
routeSigningConcurrencyOverridden;
|
|
15
|
-
/** Last credit state Beam reported. Null until the first request or connect. */
|
|
16
|
-
get creditStatus() {
|
|
17
|
-
return this.control.creditStatus;
|
|
18
|
-
}
|
|
19
32
|
constructor(options = {}) {
|
|
20
33
|
if (!options.apiKey?.trim()) {
|
|
21
34
|
throw new Error("apiKey is required.");
|
|
@@ -32,8 +45,7 @@ export class BeamClient {
|
|
|
32
45
|
subjectPrefix: options.transferClientSubjectPrefix,
|
|
33
46
|
transferRuntimeShardCount: options.transferRuntimeShardCount,
|
|
34
47
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
35
|
-
maxPayloadBytes: options.maxPayloadBytes
|
|
36
|
-
onCreditWarning: options.onCreditWarning
|
|
48
|
+
maxPayloadBytes: options.maxPayloadBytes
|
|
37
49
|
});
|
|
38
50
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
39
51
|
if (!this.fetchImpl) {
|
|
@@ -71,9 +83,12 @@ export class BeamClient {
|
|
|
71
83
|
createTransfer(input) {
|
|
72
84
|
return this.prepareProviderTransfer(input);
|
|
73
85
|
}
|
|
74
|
-
transferStatus(transferId) {
|
|
86
|
+
async transferStatus(transferId) {
|
|
75
87
|
validateId(transferId, "transferId");
|
|
76
|
-
|
|
88
|
+
const status = await this.control.request("transfer.status", { transfer_id: transferId }, { transferId });
|
|
89
|
+
if (["completed", "failed", "cancelled"].includes(status.status))
|
|
90
|
+
this.control.releaseRecoveryLease(transferId);
|
|
91
|
+
return status;
|
|
77
92
|
}
|
|
78
93
|
distributeTransfer(transferId) {
|
|
79
94
|
validateId(transferId, "transferId");
|
|
@@ -88,6 +103,7 @@ export class BeamClient {
|
|
|
88
103
|
transferId,
|
|
89
104
|
idempotencyKey: `transfer:${transferId}:cancel`
|
|
90
105
|
});
|
|
106
|
+
this.control.releaseRecoveryLease(transferId);
|
|
91
107
|
return result;
|
|
92
108
|
}
|
|
93
109
|
async planTransfer(input) {
|
|
@@ -105,8 +121,10 @@ export class BeamClient {
|
|
|
105
121
|
}
|
|
106
122
|
async prepareTransfer(input) {
|
|
107
123
|
const transferId = await transferIdForIdempotencyKey(input.idempotencyKey);
|
|
124
|
+
const routeGenerationId = input.routeGenerationId ?? randomUuid();
|
|
108
125
|
const result = await this.control.request("transfer.prepare", compact({
|
|
109
126
|
transfer_id: transferId,
|
|
127
|
+
route_generation_id: routeGenerationId,
|
|
110
128
|
sources: input.sources,
|
|
111
129
|
destinations: input.destinations,
|
|
112
130
|
name: input.name,
|
|
@@ -125,56 +143,98 @@ export class BeamClient {
|
|
|
125
143
|
validateId(transferId, "transferId");
|
|
126
144
|
validateMultipartGroupManifest(input.multipartGroupManifest, transferId);
|
|
127
145
|
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
|
-
|
|
146
|
+
const streamRoutes = async (chunkRoutes, multipartGroupManifest, routeGenerationId, urlsExpiresAt) => {
|
|
147
|
+
validateMultipartGroupManifest(multipartGroupManifest, transferId);
|
|
148
|
+
validateSignedRouteManifestContract(chunkRoutes, multipartGroupManifest);
|
|
149
|
+
const destinationCount = new Set(chunkRoutes.map((route) => route.destination_id)).size;
|
|
150
|
+
const orderedRoutes = chunkRoutes.map((route) => {
|
|
151
|
+
if (signedRouteDeliveryIndex(route) !== undefined)
|
|
152
|
+
return route;
|
|
153
|
+
if (destinationCount !== 1) {
|
|
154
|
+
throw new Error("delivery_index is required when manually attaching routes for multiple destinations");
|
|
155
|
+
}
|
|
156
|
+
return { ...route, delivery_index: route.chunk_index };
|
|
157
|
+
})
|
|
158
|
+
.sort(compareSignedRoutesByDelivery);
|
|
159
|
+
const streamId = await stableRouteStreamId({
|
|
160
|
+
transferId,
|
|
161
|
+
planIdentity: `${await routeCoordinateChecksum(orderedRoutes)}:${routeGenerationId}`,
|
|
162
|
+
totalRoutes: orderedRoutes.length,
|
|
163
|
+
totalChunks: countDistinctRouteChunks(orderedRoutes),
|
|
164
|
+
signedUrlFlow: multipartGroupManifest.length ? "signed_url_v2" : "signed_url_v1"
|
|
165
|
+
});
|
|
166
|
+
const stream = new RouteStreamSender(this.control, {
|
|
167
|
+
streamId,
|
|
168
|
+
transferId,
|
|
169
|
+
routeGenerationId,
|
|
170
|
+
totalRoutes: orderedRoutes.length,
|
|
171
|
+
totalChunks: countDistinctRouteChunks(orderedRoutes),
|
|
172
|
+
urlsExpiresAt,
|
|
173
|
+
signedUrlFlow: multipartGroupManifest.length ? "signed_url_v2" : "signed_url_v1",
|
|
174
|
+
autoDistribute: input.autoDistribute ?? true
|
|
175
|
+
});
|
|
176
|
+
await stream.begin();
|
|
177
|
+
if (multipartGroupManifest.length)
|
|
178
|
+
await stream.addManifestGroups(multipartGroupManifest);
|
|
179
|
+
for (const route of orderedRoutes)
|
|
180
|
+
await stream.addRoute(route);
|
|
181
|
+
return stream.complete();
|
|
182
|
+
};
|
|
183
|
+
const routeStreamLock = new AsyncMutex();
|
|
184
|
+
const releaseInitialStream = await routeStreamLock.acquire();
|
|
185
|
+
this.control.registerRecoveryLease({
|
|
148
186
|
transferId,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
187
|
+
planFingerprint: input.planFingerprint,
|
|
188
|
+
coordinateChecksum: input.coordinateChecksum,
|
|
189
|
+
replayRoutes: async (routeGenerationId) => {
|
|
190
|
+
await routeStreamLock.run(async () => {
|
|
191
|
+
const fresh = await input.recoveryFactory(routeGenerationId);
|
|
192
|
+
const attached = await streamRoutes(fresh.chunkRoutes, fresh.multipartGroupManifest, routeGenerationId, fresh.urlsExpiresAt);
|
|
193
|
+
if (!attached.success)
|
|
194
|
+
throw new Error(attached.error ?? attached.message ?? "route replay failed");
|
|
195
|
+
});
|
|
196
|
+
}
|
|
154
197
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
198
|
+
try {
|
|
199
|
+
const attached = await streamRoutes(input.chunkRoutes, input.multipartGroupManifest, input.routeGenerationId, input.urlsExpiresAt);
|
|
200
|
+
if (!attached.success)
|
|
201
|
+
this.control.releaseRecoveryLease(transferId);
|
|
202
|
+
return attached;
|
|
158
203
|
}
|
|
159
|
-
|
|
160
|
-
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (isRecoverableRouteStreamError(error)) {
|
|
206
|
+
this.control.continueRecoveryLease(transferId);
|
|
207
|
+
throw new BeamRouteRecoveryPendingError(transferId, error);
|
|
208
|
+
}
|
|
209
|
+
this.control.releaseRecoveryLease(transferId);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
releaseInitialStream();
|
|
161
214
|
}
|
|
162
|
-
return stream.complete();
|
|
163
215
|
}
|
|
164
216
|
async prepareProviderTransfer(input) {
|
|
165
217
|
const expiresIn = input.expiresIn ?? 3600;
|
|
166
218
|
const requestedSignedUrlFlow = input.signedUrlFlow ?? "signed_url_v1";
|
|
167
|
-
const
|
|
219
|
+
const retainedRecoveryInput = {
|
|
220
|
+
...input,
|
|
221
|
+
sources: input.sources.map((source) => ({ ...source })),
|
|
222
|
+
destinations: input.destinations.map((destination) => ({ ...destination })),
|
|
223
|
+
onBeforeTransferPrepare: undefined,
|
|
224
|
+
onPrepared: undefined,
|
|
225
|
+
throwIfCancelled: undefined
|
|
226
|
+
};
|
|
227
|
+
const preparedDestinations = retainedRecoveryInput.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
|
|
168
228
|
const destinationsById = new Map();
|
|
169
229
|
preparedDestinations.forEach((preparedDestination, index) => {
|
|
170
|
-
const destination =
|
|
230
|
+
const destination = retainedRecoveryInput.destinations[index];
|
|
171
231
|
if (!destination) {
|
|
172
232
|
throw new Error(`missing provider destination config for prepared destination ${preparedDestination.destination_id}`);
|
|
173
233
|
}
|
|
174
234
|
destinationsById.set(preparedDestination.destination_id, destination);
|
|
175
235
|
});
|
|
176
236
|
await input.throwIfCancelled?.();
|
|
177
|
-
const preparedSources = await Promise.all(
|
|
237
|
+
const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
|
|
178
238
|
await input.throwIfCancelled?.();
|
|
179
239
|
await input.onBeforeTransferPrepare?.();
|
|
180
240
|
const prepared = await this.prepareTransfer({
|
|
@@ -183,145 +243,205 @@ export class BeamClient {
|
|
|
183
243
|
name: input.name,
|
|
184
244
|
testMode: input.testMode,
|
|
185
245
|
signedUrlFlow: requestedSignedUrlFlow,
|
|
186
|
-
idempotencyKey: input.idempotencyKey
|
|
246
|
+
idempotencyKey: input.idempotencyKey,
|
|
247
|
+
routeGenerationId: input.routeGenerationId
|
|
187
248
|
});
|
|
188
249
|
if (!prepared.success) {
|
|
189
250
|
return prepared;
|
|
190
251
|
}
|
|
191
|
-
await input.onPrepared?.(prepared);
|
|
192
|
-
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
193
252
|
const sourcesById = new Map();
|
|
194
253
|
preparedSources.forEach((preparedSource, index) => {
|
|
195
|
-
const source =
|
|
254
|
+
const source = retainedRecoveryInput.sources[index];
|
|
196
255
|
if (!source) {
|
|
197
256
|
throw new Error(`missing provider source config for prepared source ${preparedSource.source_id}`);
|
|
198
257
|
}
|
|
199
258
|
sourcesById.set(preparedSource.source_id, source);
|
|
200
259
|
});
|
|
201
260
|
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
|
-
|
|
261
|
+
const autoDistribute = input.distribute !== false;
|
|
262
|
+
let initialThrowIfCancelled = input.throwIfCancelled;
|
|
263
|
+
const routeStreamLock = new AsyncMutex();
|
|
264
|
+
const releaseInitialStream = await routeStreamLock.acquire();
|
|
265
|
+
const streamPreparedRoutes = async (routeGenerationId, recoveryReplay) => {
|
|
266
|
+
const pendingRoutes = new Set();
|
|
267
|
+
let signingConcurrency = this.routeSigningConcurrency;
|
|
268
|
+
let signedInWindow = 0;
|
|
269
|
+
let signingWindowStartedAt = performance.now();
|
|
270
|
+
let routeStream = null;
|
|
271
|
+
let routeStreamBeginAttempted = false;
|
|
272
|
+
let multipartManifestTask = null;
|
|
273
|
+
let multipartGroupWaiters = new Map();
|
|
274
|
+
let foregroundCancelled = false;
|
|
275
|
+
const throwIfCancelled = recoveryReplay || !initialThrowIfCancelled ? undefined : async (transferId) => {
|
|
276
|
+
try {
|
|
277
|
+
await initialThrowIfCancelled?.(transferId);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
foregroundCancelled = true;
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
try {
|
|
285
|
+
const streamId = await stableRouteStreamId({
|
|
286
|
+
transferId: prepared.transfer_id,
|
|
287
|
+
planIdentity: `${prepared.plan_descriptor.plan_nonce}:${routeGenerationId}`,
|
|
288
|
+
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
289
|
+
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
290
|
+
signedUrlFlow: requestedSignedUrlFlow
|
|
291
|
+
});
|
|
292
|
+
routeStream = new RouteStreamSender(this.control, {
|
|
293
|
+
streamId,
|
|
294
|
+
transferId: prepared.transfer_id,
|
|
295
|
+
routeGenerationId,
|
|
296
|
+
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
297
|
+
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
298
|
+
signedUrlFlow: requestedSignedUrlFlow,
|
|
299
|
+
autoDistribute,
|
|
300
|
+
urlsExpiresAt: expiresAtIso(expiresIn)
|
|
301
|
+
});
|
|
302
|
+
routeStreamBeginAttempted = true;
|
|
303
|
+
await routeStream.begin();
|
|
304
|
+
multipartGroupWaiters = createMultipartGroupWaiters(prepared, destinationsById);
|
|
305
|
+
multipartManifestTask = createMultipartGroupManifest({
|
|
306
|
+
prepared,
|
|
307
|
+
destinationsById,
|
|
308
|
+
multipartUploads,
|
|
309
|
+
expiresIn,
|
|
310
|
+
signedUrlFlow: requestedSignedUrlFlow,
|
|
311
|
+
concurrency: this.routeSigningConcurrency,
|
|
312
|
+
preserveUploadsOnFailure: recoveryReplay,
|
|
313
|
+
onGroupReady: async (state) => {
|
|
314
|
+
if (requestedSignedUrlFlow === "signed_url_v2") {
|
|
315
|
+
validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
|
|
316
|
+
await routeStream.addManifestGroups([state.manifest]);
|
|
317
|
+
}
|
|
318
|
+
multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
|
|
319
|
+
},
|
|
320
|
+
onGroupFailed: (groupId, error) => multipartGroupWaiters.get(groupId)?.reject(error)
|
|
321
|
+
});
|
|
322
|
+
const streamNextCompletedRoute = async () => {
|
|
323
|
+
const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
|
|
324
|
+
pendingRoutes.delete(settled.pending);
|
|
325
|
+
await routeStream.addRoute(settled.route);
|
|
326
|
+
signedInWindow += 1;
|
|
327
|
+
if (signedInWindow === ROUTE_STREAM_BATCH_ROUTES) {
|
|
328
|
+
const elapsedMs = performance.now() - signingWindowStartedAt;
|
|
329
|
+
if (!this.routeSigningConcurrencyOverridden && elapsedMs > 4_000 && signingConcurrency < 256) {
|
|
330
|
+
signingConcurrency = Math.min(256, signingConcurrency * 2);
|
|
331
|
+
}
|
|
332
|
+
signedInWindow = 0;
|
|
333
|
+
signingWindowStartedAt = performance.now();
|
|
240
334
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
335
|
+
};
|
|
336
|
+
for (const chunk of materializePlanChunks(prepared.plan_descriptor, prepared.transfer_id)) {
|
|
337
|
+
await throwIfCancelled?.(prepared.transfer_id);
|
|
338
|
+
for (const target of chunk.destinations) {
|
|
339
|
+
const pendingRoute = (async () => {
|
|
340
|
+
await throwIfCancelled?.(prepared.transfer_id);
|
|
341
|
+
const destination = destinationsById.get(target.destination_id);
|
|
342
|
+
if (!destination)
|
|
343
|
+
throw new Error(`BeamCore returned unknown destination_id: ${target.destination_id}`);
|
|
344
|
+
const source = sourcesById.get(chunk.source_id);
|
|
345
|
+
if (!source)
|
|
346
|
+
throw new Error(`BeamCore returned unknown source_id: ${chunk.source_id}`);
|
|
347
|
+
const metadata = target.metadata ?? {};
|
|
348
|
+
const finalObjectKey = typeof metadata.final_object_key === "string" ? metadata.final_object_key : target.object_key;
|
|
349
|
+
if (!finalObjectKey)
|
|
350
|
+
throw new Error("destination signing target is missing object_key");
|
|
351
|
+
const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
|
|
352
|
+
const upload = isHippiusDestination(destination)
|
|
353
|
+
? undefined
|
|
354
|
+
: await multipartGroupWaiters.get(multipartGroupId)?.promise;
|
|
355
|
+
if (!isHippiusDestination(destination) && !upload)
|
|
356
|
+
throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
|
|
357
|
+
return this.signProviderRoute({
|
|
358
|
+
chunk,
|
|
359
|
+
target,
|
|
360
|
+
source,
|
|
361
|
+
destination,
|
|
362
|
+
expiresIn,
|
|
363
|
+
...(upload ? { upload } : {}),
|
|
364
|
+
transferId: prepared.transfer_id,
|
|
365
|
+
finalObjectKey,
|
|
366
|
+
signedUrlFlow: requestedSignedUrlFlow,
|
|
367
|
+
partNumber: typeof metadata.part_number === "number"
|
|
368
|
+
? metadata.part_number
|
|
369
|
+
: multipartPartNumber(chunk.source_chunk_index),
|
|
370
|
+
});
|
|
371
|
+
})();
|
|
372
|
+
pendingRoutes.add(pendingRoute);
|
|
373
|
+
if (pendingRoutes.size >= signingConcurrency)
|
|
374
|
+
await streamNextCompletedRoute();
|
|
254
375
|
}
|
|
255
|
-
signedInWindow = 0;
|
|
256
|
-
signingWindowStartedAt = performance.now();
|
|
257
376
|
}
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
377
|
+
while (pendingRoutes.size)
|
|
378
|
+
await streamNextCompletedRoute();
|
|
379
|
+
const multipartGroupManifest = await multipartManifestTask;
|
|
380
|
+
if (requestedSignedUrlFlow === "signed_url_v2") {
|
|
381
|
+
validateMultipartGroupManifest(multipartGroupManifest, prepared.transfer_id);
|
|
382
|
+
}
|
|
383
|
+
const attached = await routeStream.complete();
|
|
384
|
+
if (!attached.success) {
|
|
385
|
+
throw new Error(attached.error ?? attached.message ?? "route stream failed");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
if (routeStream)
|
|
390
|
+
await routeStream.abort().catch(() => undefined);
|
|
391
|
+
await Promise.allSettled(pendingRoutes);
|
|
392
|
+
if (multipartManifestTask)
|
|
393
|
+
await Promise.allSettled([multipartManifestTask]);
|
|
394
|
+
if (!recoveryReplay) {
|
|
395
|
+
if (foregroundCancelled) {
|
|
396
|
+
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
397
|
+
}
|
|
398
|
+
else if (isRecoverableRouteStreamError(error)) {
|
|
399
|
+
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
400
|
+
throw new BeamRouteRecoveryPendingError(prepared.transfer_id, error);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
this.control.releaseRecoveryLease(prepared.transfer_id);
|
|
404
|
+
await cancelAndAbortProviderFailure({
|
|
405
|
+
client: this,
|
|
287
406
|
transferId: prepared.transfer_id,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
? metadata.part_number
|
|
292
|
-
: multipartPartNumber(chunk.source_chunk_index),
|
|
407
|
+
cause: error,
|
|
408
|
+
uploads: multipartUploads,
|
|
409
|
+
abortBeforeCancel: !routeStreamBeginAttempted
|
|
293
410
|
});
|
|
294
|
-
}
|
|
295
|
-
pendingRoutes.add(pendingRoute);
|
|
296
|
-
if (pendingRoutes.size >= signingConcurrency)
|
|
297
|
-
await streamNextCompletedRoute();
|
|
411
|
+
}
|
|
298
412
|
}
|
|
413
|
+
throw error;
|
|
299
414
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
415
|
+
};
|
|
416
|
+
this.control.registerRecoveryLease({
|
|
417
|
+
transferId: prepared.transfer_id,
|
|
418
|
+
planFingerprint: prepared.plan_fingerprint,
|
|
419
|
+
coordinateChecksum: prepared.coordinate_checksum,
|
|
420
|
+
replayRoutes: async (routeGenerationId) => {
|
|
421
|
+
await routeStreamLock.run(() => streamPreparedRoutes(routeGenerationId, true));
|
|
422
|
+
},
|
|
423
|
+
disposeSecrets: () => clearRecoverySecrets(retainedRecoveryInput)
|
|
424
|
+
});
|
|
425
|
+
try {
|
|
426
|
+
try {
|
|
427
|
+
await input.onPrepared?.(prepared);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
431
|
+
throw error;
|
|
305
432
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
433
|
+
try {
|
|
434
|
+
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
438
|
+
throw error;
|
|
309
439
|
}
|
|
440
|
+
await streamPreparedRoutes(prepared.route_generation_id, false);
|
|
441
|
+
initialThrowIfCancelled = undefined;
|
|
310
442
|
}
|
|
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;
|
|
443
|
+
finally {
|
|
444
|
+
releaseInitialStream();
|
|
325
445
|
}
|
|
326
446
|
return prepared;
|
|
327
447
|
}
|
|
@@ -486,6 +606,27 @@ export class BeamClient {
|
|
|
486
606
|
}
|
|
487
607
|
}
|
|
488
608
|
const ROUTE_STREAM_BATCH_ROUTES = 2_048;
|
|
609
|
+
class AsyncMutex {
|
|
610
|
+
tail = Promise.resolve();
|
|
611
|
+
async acquire() {
|
|
612
|
+
const previous = this.tail;
|
|
613
|
+
let release;
|
|
614
|
+
this.tail = new Promise((resolve) => {
|
|
615
|
+
release = resolve;
|
|
616
|
+
});
|
|
617
|
+
await previous;
|
|
618
|
+
return release;
|
|
619
|
+
}
|
|
620
|
+
async run(operation) {
|
|
621
|
+
const release = await this.acquire();
|
|
622
|
+
try {
|
|
623
|
+
return await operation();
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
release();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
489
630
|
class RouteStreamSender {
|
|
490
631
|
control;
|
|
491
632
|
options;
|
|
@@ -505,6 +646,7 @@ class RouteStreamSender {
|
|
|
505
646
|
async begin() {
|
|
506
647
|
await this.control.request("transfer.route_stream.begin", compact({
|
|
507
648
|
transfer_id: this.options.transferId,
|
|
649
|
+
route_generation_id: this.options.routeGenerationId,
|
|
508
650
|
stream_id: this.streamId,
|
|
509
651
|
total_routes: this.options.totalRoutes,
|
|
510
652
|
total_chunks: this.options.totalChunks,
|
|
@@ -524,6 +666,7 @@ class RouteStreamSender {
|
|
|
524
666
|
const identity = await stableManifestBatchIdentity([group]);
|
|
525
667
|
await this.control.request("transfer.route_stream.manifest", {
|
|
526
668
|
transfer_id: this.options.transferId,
|
|
669
|
+
route_generation_id: this.options.routeGenerationId,
|
|
527
670
|
stream_id: this.streamId,
|
|
528
671
|
manifest_batch_id: identity,
|
|
529
672
|
groups: [group]
|
|
@@ -562,6 +705,7 @@ class RouteStreamSender {
|
|
|
562
705
|
throw this.sendError;
|
|
563
706
|
return this.control.request("transfer.route_stream.complete", {
|
|
564
707
|
transfer_id: this.options.transferId,
|
|
708
|
+
route_generation_id: this.options.routeGenerationId,
|
|
565
709
|
stream_id: this.streamId,
|
|
566
710
|
expected_batches: this.batchIndex,
|
|
567
711
|
expected_routes: this.options.totalRoutes,
|
|
@@ -580,6 +724,7 @@ class RouteStreamSender {
|
|
|
580
724
|
const routes = this.batch.splice(0, this.batch.length);
|
|
581
725
|
const chunks = this.control.splitRoutesForPayload("transfer.route_stream.batch", {
|
|
582
726
|
transfer_id: this.options.transferId,
|
|
727
|
+
route_generation_id: this.options.routeGenerationId,
|
|
583
728
|
stream_id: this.streamId,
|
|
584
729
|
batch_id: `${this.streamId}:estimate`,
|
|
585
730
|
batch_index: this.batchIndex
|
|
@@ -599,6 +744,7 @@ class RouteStreamSender {
|
|
|
599
744
|
const batchId = `${this.streamId}:${batchIndex}:${coordinateChecksum}`;
|
|
600
745
|
await this.control.request("transfer.route_stream.batch", {
|
|
601
746
|
transfer_id: this.options.transferId,
|
|
747
|
+
route_generation_id: this.options.routeGenerationId,
|
|
602
748
|
stream_id: this.streamId,
|
|
603
749
|
batch_id: batchId,
|
|
604
750
|
batch_index: batchIndex,
|
|
@@ -878,11 +1024,13 @@ async function createMultipartGroupManifest(input) {
|
|
|
878
1024
|
"beam-transfer-id": input.prepared.transfer_id,
|
|
879
1025
|
...(input.signedUrlFlow === "signed_url_v2" ? { "beam-multipart-group-id": multipartGroupId } : {})
|
|
880
1026
|
};
|
|
881
|
-
const
|
|
1027
|
+
const retainedUpload = input.multipartUploads.get(multipartGroupId);
|
|
1028
|
+
const uploadId = retainedUpload?.uploadId ?? await createMultipartUpload({
|
|
882
1029
|
destination: destinationConfig,
|
|
883
1030
|
objectKey: finalObjectKey,
|
|
884
1031
|
metadata: finalObjectMetadata
|
|
885
1032
|
});
|
|
1033
|
+
const createdUpload = !retainedUpload;
|
|
886
1034
|
try {
|
|
887
1035
|
const urlsExpiresAt = expiresAtIso(input.expiresIn);
|
|
888
1036
|
const stagingObjectPrefix = buildStagingObjectPrefix({
|
|
@@ -940,11 +1088,13 @@ async function createMultipartGroupManifest(input) {
|
|
|
940
1088
|
return { ok: true, state };
|
|
941
1089
|
}
|
|
942
1090
|
catch (error) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1091
|
+
if (createdUpload) {
|
|
1092
|
+
try {
|
|
1093
|
+
await abortMultipartUpload(destinationConfig, finalObjectKey, uploadId);
|
|
1094
|
+
}
|
|
1095
|
+
catch (abortError) {
|
|
1096
|
+
throw new AggregateError([error, abortError], `multipart group setup and cleanup failed for ${multipartGroupId}`);
|
|
1097
|
+
}
|
|
948
1098
|
}
|
|
949
1099
|
throw error;
|
|
950
1100
|
}
|
|
@@ -957,6 +1107,8 @@ async function createMultipartGroupManifest(input) {
|
|
|
957
1107
|
});
|
|
958
1108
|
const failure = results.find((result) => !result.ok);
|
|
959
1109
|
if (failure && !failure.ok) {
|
|
1110
|
+
if (input.preserveUploadsOnFailure)
|
|
1111
|
+
throw failure.error;
|
|
960
1112
|
try {
|
|
961
1113
|
await abortCreatedUploads(input.multipartUploads);
|
|
962
1114
|
}
|
|
@@ -1235,3 +1387,13 @@ function isHippiusDestination(destination) {
|
|
|
1235
1387
|
function sleep(ms) {
|
|
1236
1388
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1237
1389
|
}
|
|
1390
|
+
function clearRecoverySecrets(input) {
|
|
1391
|
+
const secretKeys = new Set(["access_key_id", "secret_access_key", "session_token", "api_token", "token", "password"]);
|
|
1392
|
+
for (const provider of [...input.sources, ...input.destinations]) {
|
|
1393
|
+
const record = provider;
|
|
1394
|
+
for (const key of secretKeys) {
|
|
1395
|
+
if (key in record)
|
|
1396
|
+
record[key] = "";
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|