@beam-network/sdk 0.2.23 → 0.3.0
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 -1
- package/dist/client.d.ts +5 -6
- package/dist/client.js +679 -279
- package/dist/models.d.ts +50 -7
- package/dist/multipart-limits.d.ts +3 -4
- package/dist/multipart-limits.js +8 -9
- package/dist/nats-control.d.ts +26 -43
- package/dist/nats-control.js +262 -124
- package/dist/provider-signing.d.ts +10 -15
- package/dist/provider-signing.js +45 -55
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { abortMultipartUpload, createMultipartUpload,
|
|
1
|
+
import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signListObjects, signUploadPartCopy } from "./provider-signing.js";
|
|
2
2
|
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes } from "./nats-control.js";
|
|
3
|
-
import {
|
|
3
|
+
import { multipartPartNumber } from "./multipart-limits.js";
|
|
4
4
|
export { BEAM_DEFAULT_NATS_URL };
|
|
5
5
|
export class BeamApiError extends Error {
|
|
6
6
|
status;
|
|
@@ -18,7 +18,6 @@ export class BeamClient {
|
|
|
18
18
|
fetchImpl;
|
|
19
19
|
control;
|
|
20
20
|
routeSigningConcurrency;
|
|
21
|
-
recoverySigners = new Map();
|
|
22
21
|
constructor(options = {}) {
|
|
23
22
|
if (!options.apiKey?.trim()) {
|
|
24
23
|
throw new Error("apiKey is required.");
|
|
@@ -42,12 +41,16 @@ export class BeamClient {
|
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
async close() {
|
|
45
|
-
this.stopAllRecoverySigners();
|
|
46
44
|
return this.control.close();
|
|
47
45
|
}
|
|
48
|
-
|
|
46
|
+
openTransferTerminalWaiter(transferId) {
|
|
47
|
+
validateId(transferId, "transferId");
|
|
48
|
+
return this.control.openTerminalSignalWaiter(transferId);
|
|
49
|
+
}
|
|
50
|
+
async createRawTransfer(input) {
|
|
51
|
+
const transferId = await transferIdForIdempotencyKey(input.idempotencyKey);
|
|
49
52
|
const body = compact({
|
|
50
|
-
transfer_id:
|
|
53
|
+
transfer_id: transferId,
|
|
51
54
|
sources: input.sources,
|
|
52
55
|
destinations: input.destinations,
|
|
53
56
|
total_size: input.totalSize,
|
|
@@ -58,71 +61,105 @@ export class BeamClient {
|
|
|
58
61
|
callbacks: input.callbacks,
|
|
59
62
|
test_mode: input.testMode || undefined,
|
|
60
63
|
progressive_mode: input.progressiveMode || undefined,
|
|
61
|
-
signed_url_flow:
|
|
64
|
+
signed_url_flow: "signed_url_v2"
|
|
65
|
+
});
|
|
66
|
+
return this.control.request("transfer.create", body, {
|
|
67
|
+
transferId,
|
|
68
|
+
idempotencyKey: `transfer:${transferId}:create`
|
|
62
69
|
});
|
|
63
|
-
return this.control.request("transfer.create", body);
|
|
64
70
|
}
|
|
65
71
|
createTransfer(input) {
|
|
66
72
|
return this.prepareProviderTransfer(input);
|
|
67
73
|
}
|
|
68
74
|
transferStatus(transferId) {
|
|
69
75
|
validateId(transferId, "transferId");
|
|
70
|
-
return this.control.request("transfer.status", { transfer_id: transferId }, transferId);
|
|
76
|
+
return this.control.request("transfer.status", { transfer_id: transferId }, { transferId });
|
|
71
77
|
}
|
|
72
78
|
distributeTransfer(transferId) {
|
|
73
79
|
validateId(transferId, "transferId");
|
|
74
|
-
return this.control.request("transfer.distribute", { transfer_id: transferId },
|
|
80
|
+
return this.control.request("transfer.distribute", { transfer_id: transferId }, {
|
|
81
|
+
transferId,
|
|
82
|
+
idempotencyKey: `transfer:${transferId}:distribute`
|
|
83
|
+
});
|
|
75
84
|
}
|
|
76
85
|
async cancelTransfer(transferId) {
|
|
77
86
|
validateId(transferId, "transferId");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
87
|
+
const result = await this.control.request("transfer.cancel", { transfer_id: transferId }, {
|
|
88
|
+
transferId,
|
|
89
|
+
idempotencyKey: `transfer:${transferId}:cancel`
|
|
90
|
+
});
|
|
91
|
+
return result;
|
|
84
92
|
}
|
|
85
|
-
planTransfer(input) {
|
|
86
|
-
|
|
93
|
+
async planTransfer(input) {
|
|
94
|
+
const result = await this.control.request("transfer.plan", compact({
|
|
87
95
|
sources: input.sources,
|
|
88
96
|
destinations: input.destinations,
|
|
89
97
|
name: input.name,
|
|
90
98
|
test_mode: input.testMode || undefined,
|
|
91
99
|
urls_expires_at: input.urlsExpiresAt,
|
|
92
|
-
signed_url_flow:
|
|
100
|
+
signed_url_flow: "signed_url_v2"
|
|
93
101
|
}));
|
|
102
|
+
if (result.success)
|
|
103
|
+
validateCompactTransferPlan(result.plan_descriptor, result.signed_url_flow);
|
|
104
|
+
return result;
|
|
94
105
|
}
|
|
95
|
-
prepareTransfer(input) {
|
|
96
|
-
const transferId =
|
|
97
|
-
|
|
106
|
+
async prepareTransfer(input) {
|
|
107
|
+
const transferId = await transferIdForIdempotencyKey(input.idempotencyKey);
|
|
108
|
+
const result = await this.control.request("transfer.prepare", compact({
|
|
98
109
|
transfer_id: transferId,
|
|
99
110
|
sources: input.sources,
|
|
100
111
|
destinations: input.destinations,
|
|
101
112
|
name: input.name,
|
|
102
113
|
test_mode: input.testMode || undefined,
|
|
103
114
|
urls_expires_at: input.urlsExpiresAt,
|
|
104
|
-
signed_url_flow:
|
|
105
|
-
}),
|
|
115
|
+
signed_url_flow: "signed_url_v2"
|
|
116
|
+
}), {
|
|
117
|
+
transferId,
|
|
118
|
+
idempotencyKey: `transfer:${transferId}:prepare`
|
|
119
|
+
});
|
|
120
|
+
if (result.success)
|
|
121
|
+
validateCompactTransferPlan(result.plan_descriptor, result.signed_url_flow);
|
|
122
|
+
return result;
|
|
106
123
|
}
|
|
107
124
|
async attachSignedUrls(transferId, input) {
|
|
108
125
|
validateId(transferId, "transferId");
|
|
126
|
+
validateMultipartGroupManifest(input.multipartGroupManifest, transferId);
|
|
127
|
+
validateSignedRouteManifestContract(input.chunkRoutes, input.multipartGroupManifest);
|
|
128
|
+
const destinationCount = new Set(input.chunkRoutes.map((route) => route.destination_id)).size;
|
|
129
|
+
const orderedRoutes = input.chunkRoutes
|
|
130
|
+
.map((route) => {
|
|
131
|
+
if (signedRouteDeliveryIndex(route) !== undefined)
|
|
132
|
+
return route;
|
|
133
|
+
if (destinationCount !== 1) {
|
|
134
|
+
throw new Error("delivery_index is required when manually attaching routes for multiple destinations");
|
|
135
|
+
}
|
|
136
|
+
return { ...route, delivery_index: route.chunk_index };
|
|
137
|
+
})
|
|
138
|
+
.sort(compareSignedRoutesByDelivery);
|
|
139
|
+
const streamId = await stableRouteStreamId({
|
|
140
|
+
transferId,
|
|
141
|
+
planIdentity: await routeCoordinateChecksum(orderedRoutes),
|
|
142
|
+
totalRoutes: orderedRoutes.length,
|
|
143
|
+
totalChunks: countDistinctRouteChunks(orderedRoutes)
|
|
144
|
+
});
|
|
109
145
|
const stream = new RouteStreamSender(this.control, {
|
|
146
|
+
streamId,
|
|
110
147
|
transferId,
|
|
111
|
-
totalRoutes:
|
|
112
|
-
totalChunks: countDistinctRouteChunks(
|
|
148
|
+
totalRoutes: orderedRoutes.length,
|
|
149
|
+
totalChunks: countDistinctRouteChunks(orderedRoutes),
|
|
113
150
|
urlsExpiresAt: input.urlsExpiresAt,
|
|
114
|
-
|
|
151
|
+
multipartGroupManifest: input.multipartGroupManifest,
|
|
115
152
|
autoDistribute: input.autoDistribute ?? true
|
|
116
153
|
});
|
|
117
154
|
await stream.begin();
|
|
118
|
-
for (const route of
|
|
155
|
+
for (const route of orderedRoutes) {
|
|
119
156
|
stream.addRoute(route);
|
|
120
157
|
}
|
|
121
158
|
return stream.complete();
|
|
122
159
|
}
|
|
123
160
|
async prepareProviderTransfer(input) {
|
|
124
161
|
const expiresIn = input.expiresIn ?? 3600;
|
|
125
|
-
const requestedSignedUrlFlow =
|
|
162
|
+
const requestedSignedUrlFlow = "signed_url_v2";
|
|
126
163
|
const preparedDestinations = input.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
|
|
127
164
|
const destinationsById = new Map();
|
|
128
165
|
preparedDestinations.forEach((preparedDestination, index) => {
|
|
@@ -141,7 +178,8 @@ export class BeamClient {
|
|
|
141
178
|
destinations: preparedDestinations,
|
|
142
179
|
name: input.name,
|
|
143
180
|
testMode: input.testMode,
|
|
144
|
-
signedUrlFlow: requestedSignedUrlFlow
|
|
181
|
+
signedUrlFlow: requestedSignedUrlFlow,
|
|
182
|
+
idempotencyKey: input.idempotencyKey
|
|
145
183
|
});
|
|
146
184
|
if (!prepared.success) {
|
|
147
185
|
return prepared;
|
|
@@ -156,38 +194,44 @@ export class BeamClient {
|
|
|
156
194
|
}
|
|
157
195
|
sourcesById.set(preparedSource.source_id, source);
|
|
158
196
|
});
|
|
159
|
-
const signedUrlFlow = prepared.signed_url_flow ?? requestedSignedUrlFlow;
|
|
160
|
-
const routeStream = new RouteStreamSender(this.control, {
|
|
161
|
-
transferId: prepared.transfer_id,
|
|
162
|
-
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
163
|
-
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
164
|
-
signedUrlFlow,
|
|
165
|
-
autoDistribute: input.distribute !== false
|
|
166
|
-
});
|
|
167
197
|
const multipartUploads = new Map();
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
sourcesById,
|
|
172
|
-
destinationsById,
|
|
173
|
-
multipartUploads,
|
|
174
|
-
expiresIn,
|
|
175
|
-
signedUrlFlow
|
|
176
|
-
});
|
|
177
|
-
let routeStreamStarted = false;
|
|
198
|
+
const pendingRoutes = new Set();
|
|
199
|
+
let routeStream = null;
|
|
200
|
+
let routeStreamBeginAttempted = false;
|
|
178
201
|
try {
|
|
202
|
+
const multipartGroupManifest = await createMultipartGroupManifest({
|
|
203
|
+
prepared,
|
|
204
|
+
destinationsById,
|
|
205
|
+
multipartUploads,
|
|
206
|
+
expiresIn,
|
|
207
|
+
concurrency: this.routeSigningConcurrency
|
|
208
|
+
});
|
|
209
|
+
validateMultipartGroupManifest(multipartGroupManifest, prepared.transfer_id);
|
|
210
|
+
const streamId = await stableRouteStreamId({
|
|
211
|
+
transferId: prepared.transfer_id,
|
|
212
|
+
planIdentity: prepared.plan_descriptor.plan_nonce,
|
|
213
|
+
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
214
|
+
totalChunks: prepared.plan_descriptor.logical_chunk_count
|
|
215
|
+
});
|
|
216
|
+
routeStream = new RouteStreamSender(this.control, {
|
|
217
|
+
streamId,
|
|
218
|
+
transferId: prepared.transfer_id,
|
|
219
|
+
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
220
|
+
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
221
|
+
multipartGroupManifest,
|
|
222
|
+
autoDistribute: input.distribute !== false
|
|
223
|
+
});
|
|
224
|
+
routeStreamBeginAttempted = true;
|
|
179
225
|
await routeStream.begin();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (pending)
|
|
185
|
-
routeStream.addRoute(await pending);
|
|
226
|
+
const streamNextCompletedRoute = async () => {
|
|
227
|
+
const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
|
|
228
|
+
pendingRoutes.delete(settled.pending);
|
|
229
|
+
routeStream.addRoute(settled.route);
|
|
186
230
|
};
|
|
187
|
-
for (const chunk of materializePlanChunks(prepared.plan_descriptor)) {
|
|
231
|
+
for (const chunk of materializePlanChunks(prepared.plan_descriptor, prepared.transfer_id)) {
|
|
188
232
|
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
189
233
|
for (const target of chunk.destinations) {
|
|
190
|
-
|
|
234
|
+
const pendingRoute = (async () => {
|
|
191
235
|
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
192
236
|
const destination = destinationsById.get(target.destination_id);
|
|
193
237
|
if (!destination)
|
|
@@ -199,74 +243,47 @@ export class BeamClient {
|
|
|
199
243
|
const finalObjectKey = typeof metadata.final_object_key === "string" ? metadata.final_object_key : target.object_key;
|
|
200
244
|
if (!finalObjectKey)
|
|
201
245
|
throw new Error("destination signing target is missing object_key");
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
uploadPromise = (async () => {
|
|
206
|
-
const multipartCreatedAt = nowIso();
|
|
207
|
-
const upload = isHippiusDestination(destination)
|
|
208
|
-
? {
|
|
209
|
-
destination,
|
|
210
|
-
objectKey: finalObjectKey,
|
|
211
|
-
uploadId: "",
|
|
212
|
-
completeUrl: "",
|
|
213
|
-
abortUrl: "",
|
|
214
|
-
listUrl: "",
|
|
215
|
-
multipartGroupId: multipartKey,
|
|
216
|
-
multipartCreatedAt
|
|
217
|
-
}
|
|
218
|
-
: await (async () => {
|
|
219
|
-
const uploadId = await createMultipartUpload(destination, finalObjectKey);
|
|
220
|
-
return {
|
|
221
|
-
destination,
|
|
222
|
-
objectKey: finalObjectKey,
|
|
223
|
-
uploadId,
|
|
224
|
-
completeUrl: await signCompleteMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
225
|
-
abortUrl: await signAbortMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
226
|
-
listUrl: await signListMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
227
|
-
multipartGroupId: multipartKey,
|
|
228
|
-
multipartCreatedAt
|
|
229
|
-
};
|
|
230
|
-
})();
|
|
231
|
-
multipartUploads.set(multipartKey, upload);
|
|
232
|
-
return upload;
|
|
233
|
-
})();
|
|
234
|
-
multipartUploadPromises.set(multipartKey, uploadPromise);
|
|
246
|
+
const upload = multipartUploads.get(multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey));
|
|
247
|
+
if (!isHippiusDestination(destination) && !upload) {
|
|
248
|
+
throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
|
|
235
249
|
}
|
|
236
|
-
const upload = await uploadPromise;
|
|
237
250
|
return this.signProviderRoute({
|
|
238
251
|
chunk,
|
|
239
252
|
target,
|
|
240
253
|
source,
|
|
241
254
|
destination,
|
|
242
255
|
expiresIn,
|
|
243
|
-
upload,
|
|
256
|
+
...(upload ? { upload } : {}),
|
|
244
257
|
transferId: prepared.transfer_id,
|
|
245
258
|
finalObjectKey,
|
|
246
259
|
partNumber: typeof metadata.part_number === "number"
|
|
247
260
|
? metadata.part_number
|
|
248
|
-
:
|
|
249
|
-
signedUrlFlow
|
|
261
|
+
: multipartPartNumber(chunk.source_chunk_index),
|
|
250
262
|
});
|
|
251
|
-
})()
|
|
252
|
-
|
|
253
|
-
|
|
263
|
+
})();
|
|
264
|
+
pendingRoutes.add(pendingRoute);
|
|
265
|
+
if (pendingRoutes.size >= this.routeSigningConcurrency)
|
|
266
|
+
await streamNextCompletedRoute();
|
|
254
267
|
}
|
|
255
268
|
}
|
|
256
|
-
while (pendingRoutes.
|
|
257
|
-
await
|
|
269
|
+
while (pendingRoutes.size)
|
|
270
|
+
await streamNextCompletedRoute();
|
|
258
271
|
const attached = await routeStream.complete();
|
|
259
272
|
if (!attached.success) {
|
|
260
273
|
throw new Error(attached.error ?? attached.message ?? "route stream failed");
|
|
261
274
|
}
|
|
262
275
|
}
|
|
263
276
|
catch (error) {
|
|
264
|
-
routeStream
|
|
265
|
-
|
|
266
|
-
await
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
if (routeStream)
|
|
278
|
+
await routeStream.abort();
|
|
279
|
+
await Promise.allSettled(pendingRoutes);
|
|
280
|
+
await cancelAndAbortProviderFailure({
|
|
281
|
+
client: this,
|
|
282
|
+
transferId: prepared.transfer_id,
|
|
283
|
+
cause: error,
|
|
284
|
+
uploads: multipartUploads,
|
|
285
|
+
abortBeforeCancel: !routeStreamBeginAttempted
|
|
286
|
+
});
|
|
270
287
|
throw error;
|
|
271
288
|
}
|
|
272
289
|
return prepared;
|
|
@@ -275,43 +292,17 @@ export class BeamClient {
|
|
|
275
292
|
if (isHippiusDestination(input.destination)) {
|
|
276
293
|
return signDestinationRoute({
|
|
277
294
|
chunk: input.chunk,
|
|
278
|
-
target: input.target,
|
|
279
|
-
source: input.source,
|
|
280
|
-
destination: input.destination,
|
|
281
|
-
expiresIn: input.expiresIn,
|
|
282
|
-
transferId: input.transferId,
|
|
283
|
-
multipartGroupId: input.upload.multipartGroupId,
|
|
284
|
-
multipartCreatedAt: input.upload.multipartCreatedAt,
|
|
285
|
-
fetchImpl: this.fetchImpl
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
if (!input.target.object_key) {
|
|
289
|
-
throw new Error("destination signing target is missing object_key");
|
|
290
|
-
}
|
|
291
|
-
if (input.signedUrlFlow === "signed_url_v1") {
|
|
292
|
-
const partVerifyUrl = await signListMultipartUpload(input.destination, input.finalObjectKey, input.upload.uploadId, input.expiresIn, { maxParts: 1, partNumberMarker: listPartsVerifyMarker(input.partNumber) });
|
|
293
|
-
const listPageUrl = await signListMultipartUpload(input.destination, input.finalObjectKey, input.upload.uploadId, input.expiresIn, { maxParts: 1000, partNumberMarker: listPartsPageMarker(input.partNumber) });
|
|
294
|
-
return signDestinationRoute({
|
|
295
|
-
chunk: input.chunk,
|
|
296
|
-
target: input.target,
|
|
295
|
+
target: { ...input.target, metadata: partRouteMetadata(input.target.metadata) },
|
|
297
296
|
source: input.source,
|
|
298
297
|
destination: input.destination,
|
|
299
298
|
expiresIn: input.expiresIn,
|
|
300
|
-
uploadId: input.upload.uploadId,
|
|
301
|
-
partNumber: input.partNumber,
|
|
302
|
-
multipartObjectKey: input.finalObjectKey,
|
|
303
|
-
completeUrl: input.upload.completeUrl,
|
|
304
|
-
abortUrl: input.upload.abortUrl,
|
|
305
|
-
listUrl: input.upload.listUrl,
|
|
306
|
-
partVerifyUrl,
|
|
307
|
-
listPageUrl,
|
|
308
|
-
transferId: input.transferId,
|
|
309
|
-
multipartGroupId: input.upload.multipartGroupId,
|
|
310
|
-
multipartCreatedAt: input.upload.multipartCreatedAt,
|
|
311
|
-
commitMethod: "upload_part",
|
|
312
299
|
fetchImpl: this.fetchImpl
|
|
313
300
|
});
|
|
314
301
|
}
|
|
302
|
+
const upload = input.upload;
|
|
303
|
+
if (!upload)
|
|
304
|
+
throw new Error("signed_url_v2 route is missing its multipart group");
|
|
305
|
+
validateMultipartPartNumber(input.partNumber, upload.manifest);
|
|
315
306
|
const stagingObjectPrefix = buildStagingObjectPrefix({
|
|
316
307
|
finalObjectKey: input.finalObjectKey,
|
|
317
308
|
transferId: input.transferId,
|
|
@@ -323,27 +314,20 @@ export class BeamClient {
|
|
|
323
314
|
`part-${input.partNumber}-chunk-${input.chunk.chunk_index}`
|
|
324
315
|
].join("/");
|
|
325
316
|
const copySourceRange = `bytes=0-${Math.max(0, input.chunk.chunk_size - 1)}`;
|
|
326
|
-
const { copyUrl, copySource } = await
|
|
327
|
-
|
|
328
|
-
|
|
317
|
+
const [{ copyUrl, copySource }, deleteUrl] = await Promise.all([
|
|
318
|
+
signUploadPartCopy(input.destination, input.finalObjectKey, stagingObjectKey, upload.uploadId, input.partNumber, input.expiresIn, copySourceRange),
|
|
319
|
+
signDeleteObject(input.destination, stagingObjectKey, input.expiresIn)
|
|
320
|
+
]);
|
|
329
321
|
return signDestinationRoute({
|
|
330
322
|
chunk: input.chunk,
|
|
331
|
-
target: input.target,
|
|
323
|
+
target: { ...input.target, metadata: partRouteMetadata(input.target.metadata) },
|
|
332
324
|
source: input.source,
|
|
333
325
|
destination: input.destination,
|
|
334
326
|
expiresIn: input.expiresIn,
|
|
335
|
-
uploadId: input.upload.uploadId,
|
|
336
327
|
partNumber: input.partNumber,
|
|
337
|
-
|
|
338
|
-
completeUrl: input.upload.completeUrl,
|
|
339
|
-
abortUrl: input.upload.abortUrl,
|
|
340
|
-
listUrl: input.upload.listUrl,
|
|
341
|
-
transferId: input.transferId,
|
|
342
|
-
multipartGroupId: input.upload.multipartGroupId,
|
|
343
|
-
multipartCreatedAt: input.upload.multipartCreatedAt,
|
|
328
|
+
multipartGroupId: upload.manifest.multipart_group_id,
|
|
344
329
|
commitMethod: "upload_part_copy",
|
|
345
330
|
stagingObjectKey,
|
|
346
|
-
stagingListUrl,
|
|
347
331
|
copyUrl,
|
|
348
332
|
copySource,
|
|
349
333
|
copySourceRange,
|
|
@@ -351,79 +335,6 @@ export class BeamClient {
|
|
|
351
335
|
fetchImpl: this.fetchImpl
|
|
352
336
|
});
|
|
353
337
|
}
|
|
354
|
-
async startProviderRouteRecoverySigner(input) {
|
|
355
|
-
this.stopRecoverySigner(input.prepared.transfer_id);
|
|
356
|
-
const stop = await this.control.serveRouteRecoverySigner(input.prepared.transfer_id, async (payload) => {
|
|
357
|
-
const routes = await mapOrderedWithConcurrency(payload.chunks, this.routeSigningConcurrency, async (requested) => {
|
|
358
|
-
const chunk = materializePlanChunk(input.prepared.plan_descriptor, requested.source_id, requested.chunk_index);
|
|
359
|
-
if (!chunk) {
|
|
360
|
-
throw new Error(`recovery signing chunk not found: ${requested.source_id}:${requested.chunk_index}`);
|
|
361
|
-
}
|
|
362
|
-
const target = chunk.destinations.find((candidate) => candidate.destination_id === requested.destination_id);
|
|
363
|
-
if (!target) {
|
|
364
|
-
throw new Error(`recovery signing destination not found: ${requested.destination_id}`);
|
|
365
|
-
}
|
|
366
|
-
const source = input.sourcesById.get(requested.source_id);
|
|
367
|
-
const destination = input.destinationsById.get(requested.destination_id);
|
|
368
|
-
if (!source || !destination) {
|
|
369
|
-
throw new Error("recovery signing provider configuration missing");
|
|
370
|
-
}
|
|
371
|
-
const upload = input.multipartUploads.get(requested.multipart_group_id) ?? input.multipartUploads.get(`${input.prepared.transfer_id}:${requested.destination_id}:${requested.source_id}:${requested.final_object_key}`);
|
|
372
|
-
if (!upload) {
|
|
373
|
-
throw new Error(`recovery signing multipart upload state missing for ${requested.multipart_group_id}`);
|
|
374
|
-
}
|
|
375
|
-
const metadata = {
|
|
376
|
-
...(target.metadata ?? {}),
|
|
377
|
-
...(requested.destination_metadata ?? {}),
|
|
378
|
-
transfer_id: input.prepared.transfer_id,
|
|
379
|
-
final_object_key: requested.final_object_key,
|
|
380
|
-
upload_id: requested.upload_id ?? upload.uploadId,
|
|
381
|
-
multipart_group_id: requested.multipart_group_id,
|
|
382
|
-
multipart_created_at: requested.multipart_created_at ?? upload.multipartCreatedAt,
|
|
383
|
-
delivery_index: requested.delivery_index,
|
|
384
|
-
part_number: requested.part_number,
|
|
385
|
-
logical_attempt_index: requested.logical_attempt_index,
|
|
386
|
-
attempt_slot: requested.attempt_slot,
|
|
387
|
-
route_generation_id: requested.route_generation_id
|
|
388
|
-
};
|
|
389
|
-
return this.signProviderRoute({
|
|
390
|
-
chunk,
|
|
391
|
-
target: {
|
|
392
|
-
...target,
|
|
393
|
-
object_key: target.object_key ?? requested.final_object_key,
|
|
394
|
-
metadata
|
|
395
|
-
},
|
|
396
|
-
source,
|
|
397
|
-
destination,
|
|
398
|
-
expiresIn: input.expiresIn,
|
|
399
|
-
upload,
|
|
400
|
-
transferId: input.prepared.transfer_id,
|
|
401
|
-
finalObjectKey: requested.final_object_key,
|
|
402
|
-
partNumber: requested.part_number,
|
|
403
|
-
signedUrlFlow: input.signedUrlFlow
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
return {
|
|
407
|
-
transfer_id: payload.transfer_id,
|
|
408
|
-
route_generation_id: payload.route_generation_id,
|
|
409
|
-
signed_at: nowIso(),
|
|
410
|
-
route_batch: compactSignedRoutes(routes)
|
|
411
|
-
};
|
|
412
|
-
});
|
|
413
|
-
this.recoverySigners.set(input.prepared.transfer_id, stop);
|
|
414
|
-
}
|
|
415
|
-
stopRecoverySigner(transferId) {
|
|
416
|
-
const stop = this.recoverySigners.get(transferId);
|
|
417
|
-
if (!stop)
|
|
418
|
-
return;
|
|
419
|
-
stop();
|
|
420
|
-
this.recoverySigners.delete(transferId);
|
|
421
|
-
}
|
|
422
|
-
stopAllRecoverySigners() {
|
|
423
|
-
for (const transferId of [...this.recoverySigners.keys()]) {
|
|
424
|
-
this.stopRecoverySigner(transferId);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
338
|
async createAndDistribute(input) {
|
|
428
339
|
const transfer = await this.createRawTransfer(input);
|
|
429
340
|
if (transfer.success) {
|
|
@@ -433,37 +344,95 @@ export class BeamClient {
|
|
|
433
344
|
}
|
|
434
345
|
async waitForTransfer(transferId, options = {}) {
|
|
435
346
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
436
|
-
const pollIntervalMs = options.pollIntervalMs ??
|
|
347
|
+
const pollIntervalMs = options.pollIntervalMs ?? 15_000;
|
|
348
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
349
|
+
throw new Error("timeoutMs must be a positive number.");
|
|
350
|
+
}
|
|
351
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
|
|
352
|
+
throw new Error("pollIntervalMs must be a positive number.");
|
|
353
|
+
}
|
|
354
|
+
const configuredMaxPollIntervalMs = options.maxPollIntervalMs ?? 30_000;
|
|
355
|
+
if (!Number.isFinite(configuredMaxPollIntervalMs) || configuredMaxPollIntervalMs <= 0) {
|
|
356
|
+
throw new Error("maxPollIntervalMs must be a positive number.");
|
|
357
|
+
}
|
|
358
|
+
const maxPollIntervalMs = Math.max(pollIntervalMs, configuredMaxPollIntervalMs);
|
|
437
359
|
const startedAt = Date.now();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
360
|
+
let currentPollIntervalMs = pollIntervalMs;
|
|
361
|
+
let terminalWaiter = null;
|
|
362
|
+
try {
|
|
363
|
+
terminalWaiter = await this.openTransferTerminalWaiter(transferId);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Terminal delivery is advisory; authoritative status remains available.
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
while (true) {
|
|
370
|
+
const status = await this.transferStatus(transferId);
|
|
371
|
+
if (status.status === "completed") {
|
|
372
|
+
return status;
|
|
373
|
+
}
|
|
374
|
+
if (status.status === "failed") {
|
|
375
|
+
throw new Error(`Transfer failed: ${status.error_message ?? "unknown error"}`);
|
|
376
|
+
}
|
|
377
|
+
if (status.status === "cancelled") {
|
|
378
|
+
throw new Error("Transfer cancelled");
|
|
379
|
+
}
|
|
380
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
381
|
+
throw new Error(`Transfer ${transferId} did not complete within ${timeoutMs}ms`);
|
|
382
|
+
}
|
|
383
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
384
|
+
const waitMs = Math.min(Math.max(1, remainingMs), jitteredDelay(currentPollIntervalMs));
|
|
385
|
+
let terminalEvent = null;
|
|
386
|
+
if (terminalWaiter) {
|
|
387
|
+
const waitStartedAt = Date.now();
|
|
388
|
+
try {
|
|
389
|
+
terminalEvent = await terminalWaiter.wait(waitMs);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
try {
|
|
393
|
+
await terminalWaiter.close();
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// The status fallback remains authoritative even if unsubscribe fails.
|
|
397
|
+
}
|
|
398
|
+
terminalWaiter = null;
|
|
399
|
+
const remainingWaitMs = Math.max(0, waitMs - (Date.now() - waitStartedAt));
|
|
400
|
+
if (remainingWaitMs > 0) {
|
|
401
|
+
await new Promise((resolve) => setTimeout(resolve, remainingWaitMs));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
407
|
+
}
|
|
408
|
+
if (terminalEvent)
|
|
409
|
+
currentPollIntervalMs = pollIntervalMs;
|
|
410
|
+
else
|
|
411
|
+
currentPollIntervalMs = Math.min(maxPollIntervalMs, Math.ceil(currentPollIntervalMs * 1.5));
|
|
451
412
|
}
|
|
452
|
-
|
|
453
|
-
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
if (terminalWaiter) {
|
|
416
|
+
try {
|
|
417
|
+
await terminalWaiter.close();
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Terminal delivery is advisory; closing the client remains the final cleanup boundary.
|
|
421
|
+
}
|
|
454
422
|
}
|
|
455
|
-
await sleep(pollIntervalMs);
|
|
456
423
|
}
|
|
457
424
|
}
|
|
458
425
|
}
|
|
459
|
-
const ROUTE_STREAM_BOOTSTRAP_BATCH_ROUTES =
|
|
460
|
-
const ROUTE_STREAM_STEADY_BATCH_ROUTES =
|
|
426
|
+
const ROUTE_STREAM_BOOTSTRAP_BATCH_ROUTES = 32;
|
|
427
|
+
const ROUTE_STREAM_STEADY_BATCH_ROUTES = 256;
|
|
461
428
|
class RouteStreamSender {
|
|
462
429
|
control;
|
|
463
430
|
options;
|
|
464
|
-
streamId
|
|
431
|
+
streamId;
|
|
465
432
|
checksum = new RouteKeysChecksum();
|
|
466
433
|
batch = [];
|
|
434
|
+
pendingRoutes = new Map();
|
|
435
|
+
nextDeliveryIndex = 0;
|
|
467
436
|
batchIndex = 0;
|
|
468
437
|
routeCount = 0;
|
|
469
438
|
sendTail = Promise.resolve();
|
|
@@ -471,6 +440,7 @@ class RouteStreamSender {
|
|
|
471
440
|
constructor(control, options) {
|
|
472
441
|
this.control = control;
|
|
473
442
|
this.options = options;
|
|
443
|
+
this.streamId = options.streamId;
|
|
474
444
|
}
|
|
475
445
|
async begin() {
|
|
476
446
|
await this.control.request("transfer.route_stream.begin", compact({
|
|
@@ -478,25 +448,48 @@ class RouteStreamSender {
|
|
|
478
448
|
stream_id: this.streamId,
|
|
479
449
|
total_routes: this.options.totalRoutes,
|
|
480
450
|
total_chunks: this.options.totalChunks,
|
|
451
|
+
route_contract_version: "signed_url_v2",
|
|
452
|
+
multipart_group_manifest: this.options.multipartGroupManifest,
|
|
481
453
|
urls_expires_at: this.options.urlsExpiresAt,
|
|
482
|
-
signed_url_flow:
|
|
454
|
+
signed_url_flow: "signed_url_v2",
|
|
483
455
|
auto_distribute: this.options.autoDistribute
|
|
484
|
-
}),
|
|
456
|
+
}), {
|
|
457
|
+
transferId: this.options.transferId,
|
|
458
|
+
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:begin`
|
|
459
|
+
});
|
|
485
460
|
}
|
|
486
461
|
addRoute(route) {
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
462
|
+
const deliveryIndex = signedRouteDeliveryIndex(route);
|
|
463
|
+
if (deliveryIndex === undefined) {
|
|
464
|
+
throw new Error("route delivery_index is required");
|
|
465
|
+
}
|
|
466
|
+
if (!Number.isSafeInteger(deliveryIndex) || deliveryIndex < 0 || deliveryIndex >= this.options.totalRoutes) {
|
|
467
|
+
throw new Error(`route delivery_index ${deliveryIndex} is outside the declared route stream`);
|
|
468
|
+
}
|
|
469
|
+
if (deliveryIndex < this.nextDeliveryIndex || this.pendingRoutes.has(deliveryIndex)) {
|
|
470
|
+
throw new Error(`duplicate route delivery_index ${deliveryIndex}`);
|
|
471
|
+
}
|
|
472
|
+
this.pendingRoutes.set(deliveryIndex, route);
|
|
490
473
|
this.routeCount += 1;
|
|
491
|
-
this.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
:
|
|
495
|
-
|
|
496
|
-
this.
|
|
474
|
+
while (this.pendingRoutes.has(this.nextDeliveryIndex)) {
|
|
475
|
+
const next = this.pendingRoutes.get(this.nextDeliveryIndex);
|
|
476
|
+
this.pendingRoutes.delete(this.nextDeliveryIndex);
|
|
477
|
+
this.batch.push({ ...next, delivery_index: this.nextDeliveryIndex });
|
|
478
|
+
this.nextDeliveryIndex += 1;
|
|
479
|
+
const target = this.batchIndex === 0
|
|
480
|
+
? ROUTE_STREAM_BOOTSTRAP_BATCH_ROUTES
|
|
481
|
+
: ROUTE_STREAM_STEADY_BATCH_ROUTES;
|
|
482
|
+
if (this.batch.length >= target) {
|
|
483
|
+
this.enqueueFlush();
|
|
484
|
+
}
|
|
497
485
|
}
|
|
498
486
|
}
|
|
499
487
|
async complete() {
|
|
488
|
+
if (this.routeCount !== this.options.totalRoutes ||
|
|
489
|
+
this.pendingRoutes.size !== 0 ||
|
|
490
|
+
this.nextDeliveryIndex !== this.options.totalRoutes) {
|
|
491
|
+
throw new Error(`route stream has non-contiguous delivery indices: received ${this.routeCount} of ${this.options.totalRoutes} routes`);
|
|
492
|
+
}
|
|
500
493
|
this.enqueueFlush();
|
|
501
494
|
await this.sendTail;
|
|
502
495
|
if (this.sendError)
|
|
@@ -507,7 +500,10 @@ class RouteStreamSender {
|
|
|
507
500
|
expected_batches: this.batchIndex,
|
|
508
501
|
expected_routes: this.options.totalRoutes,
|
|
509
502
|
route_keys_checksum: this.checksum.value()
|
|
510
|
-
},
|
|
503
|
+
}, {
|
|
504
|
+
transferId: this.options.transferId,
|
|
505
|
+
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:complete`
|
|
506
|
+
});
|
|
511
507
|
}
|
|
512
508
|
enqueueFlush() {
|
|
513
509
|
if (!this.batch.length)
|
|
@@ -529,15 +525,21 @@ class RouteStreamSender {
|
|
|
529
525
|
const chunkChecksum = new RouteKeysChecksum();
|
|
530
526
|
await chunkChecksum.addMany(chunk.map(routeKeyForSignedRoute));
|
|
531
527
|
this.checksum.merge(chunkChecksum);
|
|
528
|
+
const routeChecksum = chunkChecksum.value();
|
|
529
|
+
const coordinateChecksum = await routeCoordinateChecksum(chunk);
|
|
530
|
+
const batchId = `${this.streamId}:${batchIndex}:${coordinateChecksum}`;
|
|
532
531
|
await this.control.request("transfer.route_stream.batch", {
|
|
533
532
|
transfer_id: this.options.transferId,
|
|
534
533
|
stream_id: this.streamId,
|
|
535
|
-
batch_id:
|
|
534
|
+
batch_id: batchId,
|
|
536
535
|
batch_index: batchIndex,
|
|
537
536
|
route_batch: compactSignedRoutes(chunk),
|
|
538
537
|
route_count: chunk.length,
|
|
539
|
-
route_keys_checksum:
|
|
540
|
-
},
|
|
538
|
+
route_keys_checksum: routeChecksum
|
|
539
|
+
}, {
|
|
540
|
+
transferId: this.options.transferId,
|
|
541
|
+
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:batch:${batchIndex}:${coordinateChecksum}`
|
|
542
|
+
});
|
|
541
543
|
}
|
|
542
544
|
catch (error) {
|
|
543
545
|
this.sendError = error;
|
|
@@ -545,8 +547,9 @@ class RouteStreamSender {
|
|
|
545
547
|
});
|
|
546
548
|
}
|
|
547
549
|
}
|
|
548
|
-
abort() {
|
|
550
|
+
async abort() {
|
|
549
551
|
this.sendError ??= new Error("route_stream_aborted");
|
|
552
|
+
await this.sendTail;
|
|
550
553
|
}
|
|
551
554
|
}
|
|
552
555
|
class RouteKeysChecksum {
|
|
@@ -574,13 +577,40 @@ class RouteKeysChecksum {
|
|
|
574
577
|
function routeKeyForSignedRoute(route) {
|
|
575
578
|
return `${route.source_id}:${route.destination_id}:${route.chunk_index}`;
|
|
576
579
|
}
|
|
580
|
+
function signedRouteDeliveryIndex(route) {
|
|
581
|
+
const metadataIndex = route.metadata?.delivery_index;
|
|
582
|
+
if (typeof metadataIndex === "number")
|
|
583
|
+
return metadataIndex;
|
|
584
|
+
return route.delivery_index;
|
|
585
|
+
}
|
|
586
|
+
function compareSignedRoutesByDelivery(left, right) {
|
|
587
|
+
const leftIndex = signedRouteDeliveryIndex(left);
|
|
588
|
+
const rightIndex = signedRouteDeliveryIndex(right);
|
|
589
|
+
if (leftIndex !== undefined && rightIndex !== undefined && leftIndex !== rightIndex) {
|
|
590
|
+
return leftIndex - rightIndex;
|
|
591
|
+
}
|
|
592
|
+
return routeKeyForSignedRoute(left).localeCompare(routeKeyForSignedRoute(right));
|
|
593
|
+
}
|
|
594
|
+
async function routeCoordinateChecksum(routes) {
|
|
595
|
+
const checksum = new RouteKeysChecksum();
|
|
596
|
+
await checksum.addMany(routes.map((route) => `${routeKeyForSignedRoute(route)}:${signedRouteDeliveryIndex(route) ?? "missing"}`));
|
|
597
|
+
return checksum.value();
|
|
598
|
+
}
|
|
599
|
+
async function stableRouteStreamId(input) {
|
|
600
|
+
const digest = await sha256Bytes(`beam:route-stream:signed_url_v2:${input.transferId}:${input.planIdentity}:${input.totalRoutes}:${input.totalChunks}`);
|
|
601
|
+
const bytes = digest.slice(0, 16);
|
|
602
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
603
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
604
|
+
const value = hex(bytes);
|
|
605
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
606
|
+
}
|
|
577
607
|
function countDistinctRouteChunks(routes) {
|
|
578
608
|
return new Set(routes.map((route) => `${route.source_id}:${route.chunk_index}`)).size;
|
|
579
609
|
}
|
|
580
|
-
function* materializePlanChunks(descriptor) {
|
|
610
|
+
function* materializePlanChunks(descriptor, transferId) {
|
|
581
611
|
for (const source of descriptor.sources) {
|
|
582
612
|
for (let sourceChunkIndex = 0; sourceChunkIndex < source.chunk_count; sourceChunkIndex += 1) {
|
|
583
|
-
yield materializePlanChunk(descriptor, source.source_id, source.global_chunk_start + sourceChunkIndex);
|
|
613
|
+
yield materializePlanChunk(descriptor, transferId, source.source_id, source.global_chunk_start + sourceChunkIndex);
|
|
584
614
|
}
|
|
585
615
|
}
|
|
586
616
|
}
|
|
@@ -604,7 +634,7 @@ function positiveInteger(value, label) {
|
|
|
604
634
|
throw new Error(`${label} must be a positive integer.`);
|
|
605
635
|
return value;
|
|
606
636
|
}
|
|
607
|
-
function materializePlanChunk(descriptor, sourceId, chunkIndex) {
|
|
637
|
+
function materializePlanChunk(descriptor, transferId, sourceId, chunkIndex) {
|
|
608
638
|
const source = descriptor.sources.find((candidate) => candidate.source_id === sourceId);
|
|
609
639
|
if (!source)
|
|
610
640
|
throw new Error(`plan source not found: ${sourceId}`);
|
|
@@ -626,15 +656,26 @@ function materializePlanChunk(descriptor, sourceId, chunkIndex) {
|
|
|
626
656
|
if (!finalObjectKey)
|
|
627
657
|
throw new Error(`plan final object key not found: ${sourceId}:${destination.destination_id}`);
|
|
628
658
|
const deliveryIndex = chunkIndex * descriptor.destinations.length + destination.destination_index;
|
|
659
|
+
const partNumber = multipartPartNumber(sourceChunkIndex);
|
|
660
|
+
const objectKey = destination.provider.toLowerCase() === "hippius"
|
|
661
|
+
? `${finalObjectKey}/${descriptor.plan_nonce}/chunk-${String(sourceChunkIndex).padStart(6, "0")}`
|
|
662
|
+
: buildStagingObjectKey({
|
|
663
|
+
finalObjectKey,
|
|
664
|
+
transferId,
|
|
665
|
+
sourceId,
|
|
666
|
+
destinationId: destination.destination_id,
|
|
667
|
+
chunkIndex,
|
|
668
|
+
partNumber
|
|
669
|
+
});
|
|
629
670
|
return {
|
|
630
671
|
destination_id: destination.destination_id,
|
|
631
672
|
delivery_index: deliveryIndex,
|
|
632
673
|
provider: destination.provider,
|
|
633
|
-
object_key:
|
|
674
|
+
object_key: objectKey,
|
|
634
675
|
metadata: {
|
|
635
676
|
...(destination.metadata ?? {}),
|
|
636
677
|
final_object_key: finalObjectKey,
|
|
637
|
-
part_number:
|
|
678
|
+
part_number: partNumber,
|
|
638
679
|
logical_attempt_index: 0,
|
|
639
680
|
attempt_slot: 0,
|
|
640
681
|
route_generation_id: `initial-${chunkIndex}-${destination.destination_id}`,
|
|
@@ -655,33 +696,387 @@ async function sha256Bytes(value) {
|
|
|
655
696
|
function hex(bytes) {
|
|
656
697
|
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
657
698
|
}
|
|
658
|
-
async function
|
|
699
|
+
async function cancelTransferAfterProviderFailure(client, transferId, cause) {
|
|
659
700
|
try {
|
|
660
|
-
await client.cancelTransfer(transferId);
|
|
701
|
+
const result = await client.cancelTransfer(transferId);
|
|
702
|
+
if (!result.success) {
|
|
703
|
+
throw new Error(result.message ?? `Beam rejected cancellation for ${transferId}`);
|
|
704
|
+
}
|
|
661
705
|
}
|
|
662
706
|
catch (cancelError) {
|
|
663
|
-
throw new Error(`
|
|
707
|
+
throw new Error(`provider transfer failed (${errorMessage(cause)}) and transfer cancellation failed (${errorMessage(cancelError)})`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function cancelAndAbortProviderFailure(input) {
|
|
711
|
+
let cleanupError;
|
|
712
|
+
let cleanupFailed = false;
|
|
713
|
+
let cancelError;
|
|
714
|
+
if (input.abortBeforeCancel) {
|
|
715
|
+
try {
|
|
716
|
+
await abortCreatedUploads(input.uploads);
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
cleanupError = error;
|
|
720
|
+
cleanupFailed = true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
await cancelTransferAfterProviderFailure(input.client, input.transferId, input.cause);
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
cancelError = error;
|
|
728
|
+
}
|
|
729
|
+
if (!input.abortBeforeCancel || cleanupFailed) {
|
|
730
|
+
try {
|
|
731
|
+
await abortCreatedUploads(input.uploads);
|
|
732
|
+
cleanupError = undefined;
|
|
733
|
+
cleanupFailed = false;
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
cleanupError = error;
|
|
737
|
+
cleanupFailed = true;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (cancelError && cleanupFailed) {
|
|
741
|
+
throw new AggregateError([input.cause, cancelError, cleanupError], `provider transfer, transfer cancellation, and multipart cleanup failed for ${input.transferId}`);
|
|
742
|
+
}
|
|
743
|
+
if (cancelError)
|
|
744
|
+
throw cancelError;
|
|
745
|
+
if (cleanupFailed) {
|
|
746
|
+
throw new AggregateError([input.cause, cleanupError], `provider transfer failed and multipart cleanup failed for ${input.transferId}`);
|
|
664
747
|
}
|
|
665
748
|
}
|
|
666
749
|
function errorMessage(error) {
|
|
667
750
|
return error instanceof Error ? error.message : String(error);
|
|
668
751
|
}
|
|
669
|
-
function
|
|
752
|
+
async function createMultipartGroupManifest(input) {
|
|
753
|
+
const groups = input.prepared.plan_descriptor.sources.flatMap((source) => input.prepared.plan_descriptor.destinations.map((destination) => ({ source, destination })));
|
|
754
|
+
const results = await mapOrderedWithConcurrency(groups, input.concurrency, async ({ source, destination }) => {
|
|
755
|
+
try {
|
|
756
|
+
const destinationConfig = input.destinationsById.get(destination.destination_id);
|
|
757
|
+
if (!destinationConfig) {
|
|
758
|
+
throw new Error(`BeamCore returned unknown destination_id: ${destination.destination_id}`);
|
|
759
|
+
}
|
|
760
|
+
if (isHippiusDestination(destinationConfig)) {
|
|
761
|
+
return { ok: true, state: null };
|
|
762
|
+
}
|
|
763
|
+
const finalObjectKey = destination.final_object_keys[source.source_id];
|
|
764
|
+
if (!finalObjectKey) {
|
|
765
|
+
throw new Error(`plan final object key not found: ${source.source_id}:${destination.destination_id}`);
|
|
766
|
+
}
|
|
767
|
+
const multipartGroupId = multipartGroupStateKey(input.prepared.transfer_id, destination.destination_id, source.source_id, finalObjectKey);
|
|
768
|
+
const finalObjectMetadata = {
|
|
769
|
+
"beam-transfer-id": input.prepared.transfer_id,
|
|
770
|
+
"beam-multipart-group-id": multipartGroupId
|
|
771
|
+
};
|
|
772
|
+
const uploadId = await createMultipartUpload({
|
|
773
|
+
destination: destinationConfig,
|
|
774
|
+
objectKey: finalObjectKey,
|
|
775
|
+
metadata: finalObjectMetadata
|
|
776
|
+
});
|
|
777
|
+
try {
|
|
778
|
+
const urlsExpiresAt = expiresAtIso(input.expiresIn);
|
|
779
|
+
const stagingObjectPrefix = buildStagingObjectPrefix({
|
|
780
|
+
finalObjectKey,
|
|
781
|
+
transferId: input.prepared.transfer_id,
|
|
782
|
+
sourceId: source.source_id,
|
|
783
|
+
destinationId: destination.destination_id
|
|
784
|
+
});
|
|
785
|
+
const stagingListPrefix = `${stagingObjectPrefix}/`;
|
|
786
|
+
const maxPartNumber = multipartMaxPartNumber(source.chunk_count);
|
|
787
|
+
const listPageMarkers = multipartListPageMarkers(maxPartNumber);
|
|
788
|
+
const stagingObjectKeys = multipartStagingObjectKeys({
|
|
789
|
+
finalObjectKey,
|
|
790
|
+
transferId: input.prepared.transfer_id,
|
|
791
|
+
sourceId: source.source_id,
|
|
792
|
+
destinationId: destination.destination_id,
|
|
793
|
+
globalChunkStart: source.global_chunk_start,
|
|
794
|
+
chunkCount: source.chunk_count
|
|
795
|
+
});
|
|
796
|
+
const stagingListStartAfter = pagedListStartAfter(stagingObjectKeys);
|
|
797
|
+
const [completeUrl, abortUrl, listPageUrls, stagingListUrls, finalHeadUrl] = await Promise.all([
|
|
798
|
+
signCompleteMultipartUpload(destinationConfig, finalObjectKey, uploadId, input.expiresIn),
|
|
799
|
+
signAbortMultipartUpload(destinationConfig, finalObjectKey, uploadId, input.expiresIn),
|
|
800
|
+
Promise.all(listPageMarkers.map((partNumberMarker) => signListMultipartUpload(destinationConfig, finalObjectKey, uploadId, input.expiresIn, { maxParts: 1_000, ...(partNumberMarker ? { partNumberMarker } : {}) }))),
|
|
801
|
+
Promise.all(stagingListStartAfter.map((startAfter) => signListObjects(destinationConfig, stagingListPrefix, input.expiresIn, startAfter ? { startAfter } : {}))),
|
|
802
|
+
signFinalObjectHead(destinationConfig, finalObjectKey, input.expiresIn)
|
|
803
|
+
]);
|
|
804
|
+
const manifest = {
|
|
805
|
+
multipart_group_id: multipartGroupId,
|
|
806
|
+
source_id: source.source_id,
|
|
807
|
+
destination_id: destination.destination_id,
|
|
808
|
+
final_object_key: finalObjectKey,
|
|
809
|
+
upload_id: uploadId,
|
|
810
|
+
expected_object_size: source.size,
|
|
811
|
+
expected_part_count: source.chunk_count,
|
|
812
|
+
max_part_number: maxPartNumber,
|
|
813
|
+
complete_url: completeUrl,
|
|
814
|
+
abort_url: abortUrl,
|
|
815
|
+
list_page_urls: listPageUrls,
|
|
816
|
+
staging_list_urls: stagingListUrls,
|
|
817
|
+
final_head_url: finalHeadUrl,
|
|
818
|
+
final_object_metadata: finalObjectMetadata,
|
|
819
|
+
urls_expires_at: urlsExpiresAt
|
|
820
|
+
};
|
|
821
|
+
const state = {
|
|
822
|
+
destination: destinationConfig,
|
|
823
|
+
objectKey: finalObjectKey,
|
|
824
|
+
uploadId,
|
|
825
|
+
manifest
|
|
826
|
+
};
|
|
827
|
+
input.multipartUploads.set(multipartGroupId, state);
|
|
828
|
+
return { ok: true, state };
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
try {
|
|
832
|
+
await abortMultipartUpload(destinationConfig, finalObjectKey, uploadId);
|
|
833
|
+
}
|
|
834
|
+
catch (abortError) {
|
|
835
|
+
throw new AggregateError([error, abortError], `multipart group setup and cleanup failed for ${multipartGroupId}`);
|
|
836
|
+
}
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
return { ok: false, error };
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
const failure = results.find((result) => !result.ok);
|
|
845
|
+
if (failure && !failure.ok) {
|
|
846
|
+
try {
|
|
847
|
+
await abortCreatedUploads(input.multipartUploads);
|
|
848
|
+
}
|
|
849
|
+
catch (cleanupError) {
|
|
850
|
+
input.multipartUploads.clear();
|
|
851
|
+
throw new AggregateError([failure.error, cleanupError], "multipart manifest setup and cleanup failed");
|
|
852
|
+
}
|
|
853
|
+
input.multipartUploads.clear();
|
|
854
|
+
throw failure.error;
|
|
855
|
+
}
|
|
856
|
+
return results.flatMap((result) => result.ok && result.state ? [result.state.manifest] : []);
|
|
857
|
+
}
|
|
858
|
+
function multipartListPageMarkers(maxPartNumber) {
|
|
859
|
+
const markers = [];
|
|
860
|
+
for (let marker = 0; marker < maxPartNumber; marker += 1_000)
|
|
861
|
+
markers.push(marker);
|
|
862
|
+
return markers;
|
|
863
|
+
}
|
|
864
|
+
function multipartStagingObjectKeys(input) {
|
|
865
|
+
const encoder = new TextEncoder();
|
|
866
|
+
const keys = Array.from({ length: input.chunkCount }, (_, sourceChunkIndex) => {
|
|
867
|
+
const key = buildStagingObjectKey({
|
|
868
|
+
finalObjectKey: input.finalObjectKey,
|
|
869
|
+
transferId: input.transferId,
|
|
870
|
+
sourceId: input.sourceId,
|
|
871
|
+
destinationId: input.destinationId,
|
|
872
|
+
chunkIndex: input.globalChunkStart + sourceChunkIndex,
|
|
873
|
+
partNumber: multipartPartNumber(sourceChunkIndex)
|
|
874
|
+
});
|
|
875
|
+
return { key, bytes: encoder.encode(key) };
|
|
876
|
+
});
|
|
877
|
+
keys.sort((left, right) => compareBytes(left.bytes, right.bytes));
|
|
878
|
+
return keys.map(({ key }) => key);
|
|
879
|
+
}
|
|
880
|
+
function pagedListStartAfter(sortedKeys) {
|
|
881
|
+
const boundaries = [];
|
|
882
|
+
for (let pageStart = 0; pageStart < sortedKeys.length; pageStart += 1_000) {
|
|
883
|
+
boundaries.push(pageStart === 0 ? undefined : sortedKeys[pageStart - 1]);
|
|
884
|
+
}
|
|
885
|
+
return boundaries;
|
|
886
|
+
}
|
|
887
|
+
function compareBytes(leftBytes, rightBytes) {
|
|
888
|
+
const sharedLength = Math.min(leftBytes.length, rightBytes.length);
|
|
889
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
890
|
+
const difference = (leftBytes[index] ?? 0) - (rightBytes[index] ?? 0);
|
|
891
|
+
if (difference !== 0)
|
|
892
|
+
return difference;
|
|
893
|
+
}
|
|
894
|
+
return leftBytes.length - rightBytes.length;
|
|
895
|
+
}
|
|
896
|
+
function multipartMaxPartNumber(chunkCount) {
|
|
897
|
+
return multipartPartNumber(positiveInteger(chunkCount, "source chunk_count") - 1);
|
|
898
|
+
}
|
|
899
|
+
function validateMultipartPartNumber(partNumber, manifest) {
|
|
900
|
+
if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > manifest.max_part_number) {
|
|
901
|
+
throw new Error(`multipart part_number ${partNumber} is outside group ${manifest.multipart_group_id} range 1-${manifest.max_part_number}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function validateSignedRouteManifestContract(routes, manifest) {
|
|
905
|
+
const groups = new Map(manifest.map((group) => [group.multipart_group_id, group]));
|
|
906
|
+
for (const route of routes) {
|
|
907
|
+
const groupId = typeof route.metadata?.multipart_group_id === "string"
|
|
908
|
+
? route.metadata.multipart_group_id
|
|
909
|
+
: undefined;
|
|
910
|
+
if (!groupId)
|
|
911
|
+
continue;
|
|
912
|
+
const group = groups.get(groupId);
|
|
913
|
+
if (!group)
|
|
914
|
+
throw new Error(`signed route references unknown multipart group ${groupId}`);
|
|
915
|
+
if (route.source_id !== group.source_id || route.destination_id !== group.destination_id) {
|
|
916
|
+
throw new Error(`signed route identity does not match multipart group ${groupId}`);
|
|
917
|
+
}
|
|
918
|
+
const partNumber = route.metadata?.part_number;
|
|
919
|
+
if (typeof partNumber !== "number") {
|
|
920
|
+
throw new Error(`signed route for multipart group ${groupId} is missing part_number`);
|
|
921
|
+
}
|
|
922
|
+
validateMultipartPartNumber(partNumber, group);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const COMPACT_TRANSFER_PLAN_KEYS = new Set([
|
|
926
|
+
"version",
|
|
927
|
+
"plan_nonce",
|
|
928
|
+
"chunk_size",
|
|
929
|
+
"sources",
|
|
930
|
+
"destinations",
|
|
931
|
+
"logical_chunk_count",
|
|
932
|
+
"delivery_route_count",
|
|
933
|
+
"formulas"
|
|
934
|
+
]);
|
|
935
|
+
const COMPACT_TRANSFER_PLAN_FORMULAS = {
|
|
936
|
+
source_offset: "source_chunk_index * chunk_size",
|
|
937
|
+
delivery_index: "chunk_index * destination_count + destination_index",
|
|
938
|
+
part_number: "source_chunk_index + 1",
|
|
939
|
+
route_generation_id: "initial-{chunk_index}-{destination_id}",
|
|
940
|
+
staging_object_key: "{final_object_key}.beam-staging/{transfer_id}/{source_id}/{destination_id}/part-{part_number}-chunk-{chunk_index}"
|
|
941
|
+
};
|
|
942
|
+
function validateCompactTransferPlan(descriptor, signedUrlFlow) {
|
|
943
|
+
if (signedUrlFlow !== "signed_url_v2") {
|
|
944
|
+
throw new Error(`BeamCore returned unsupported signed_url_flow: ${String(signedUrlFlow)}`);
|
|
945
|
+
}
|
|
946
|
+
const keys = Object.keys(descriptor);
|
|
947
|
+
if (keys.length !== COMPACT_TRANSFER_PLAN_KEYS.size || keys.some((key) => !COMPACT_TRANSFER_PLAN_KEYS.has(key))) {
|
|
948
|
+
throw new Error("BeamCore returned invalid compact transfer plan fields");
|
|
949
|
+
}
|
|
950
|
+
if (descriptor.version !== "compact-transfer-plan/v1") {
|
|
951
|
+
throw new Error(`BeamCore returned unsupported plan version: ${descriptor.version}`);
|
|
952
|
+
}
|
|
953
|
+
const formulaKeys = Object.keys(descriptor.formulas);
|
|
954
|
+
if (formulaKeys.length !== Object.keys(COMPACT_TRANSFER_PLAN_FORMULAS).length
|
|
955
|
+
|| Object.entries(COMPACT_TRANSFER_PLAN_FORMULAS).some(([key, value]) => descriptor.formulas[key] !== value)) {
|
|
956
|
+
throw new Error("BeamCore returned unsupported compact transfer plan formulas");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const MULTIPART_GROUP_MANIFEST_KEYS = new Set([
|
|
960
|
+
"multipart_group_id",
|
|
961
|
+
"source_id",
|
|
962
|
+
"destination_id",
|
|
963
|
+
"final_object_key",
|
|
964
|
+
"upload_id",
|
|
965
|
+
"expected_object_size",
|
|
966
|
+
"expected_part_count",
|
|
967
|
+
"max_part_number",
|
|
968
|
+
"complete_url",
|
|
969
|
+
"abort_url",
|
|
970
|
+
"list_page_urls",
|
|
971
|
+
"staging_list_urls",
|
|
972
|
+
"final_head_url",
|
|
973
|
+
"final_object_metadata",
|
|
974
|
+
"urls_expires_at"
|
|
975
|
+
]);
|
|
976
|
+
function validateMultipartGroupManifest(manifest, transferId) {
|
|
977
|
+
const groupIds = new Set();
|
|
978
|
+
for (const group of manifest) {
|
|
979
|
+
const keys = Object.keys(group);
|
|
980
|
+
if (keys.length !== MULTIPART_GROUP_MANIFEST_KEYS.size || keys.some((key) => !MULTIPART_GROUP_MANIFEST_KEYS.has(key))) {
|
|
981
|
+
throw new Error(`multipart group ${group.multipart_group_id || "<unknown>"} has invalid manifest fields`);
|
|
982
|
+
}
|
|
983
|
+
if (!group.multipart_group_id || groupIds.has(group.multipart_group_id)) {
|
|
984
|
+
throw new Error(`multipart_group_id must be non-empty and unique: ${group.multipart_group_id}`);
|
|
985
|
+
}
|
|
986
|
+
groupIds.add(group.multipart_group_id);
|
|
987
|
+
for (const [field, value] of Object.entries({
|
|
988
|
+
source_id: group.source_id,
|
|
989
|
+
destination_id: group.destination_id,
|
|
990
|
+
final_object_key: group.final_object_key,
|
|
991
|
+
upload_id: group.upload_id,
|
|
992
|
+
complete_url: group.complete_url,
|
|
993
|
+
abort_url: group.abort_url,
|
|
994
|
+
final_head_url: group.final_head_url,
|
|
995
|
+
urls_expires_at: group.urls_expires_at
|
|
996
|
+
})) {
|
|
997
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
998
|
+
throw new Error(`multipart group ${group.multipart_group_id} requires ${field}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (!Number.isInteger(group.expected_part_count) || group.expected_part_count < 1 || group.expected_part_count > 10_000) {
|
|
1002
|
+
throw new Error(`multipart group ${group.multipart_group_id} has invalid expected_part_count`);
|
|
1003
|
+
}
|
|
1004
|
+
if (group.max_part_number !== group.expected_part_count) {
|
|
1005
|
+
throw new Error(`multipart group ${group.multipart_group_id} max_part_number must equal expected_part_count`);
|
|
1006
|
+
}
|
|
1007
|
+
if (!Number.isSafeInteger(group.expected_object_size) || group.expected_object_size <= 0) {
|
|
1008
|
+
throw new Error(`multipart group ${group.multipart_group_id} has invalid expected_object_size`);
|
|
1009
|
+
}
|
|
1010
|
+
const expectedListPageCount = Math.ceil(group.max_part_number / 1_000);
|
|
1011
|
+
if (!Array.isArray(group.list_page_urls)
|
|
1012
|
+
|| group.list_page_urls.length !== expectedListPageCount
|
|
1013
|
+
|| new Set(group.list_page_urls).size !== expectedListPageCount
|
|
1014
|
+
|| group.list_page_urls.some((url) => typeof url !== "string" || url.length === 0)) {
|
|
1015
|
+
throw new Error(`multipart group ${group.multipart_group_id} requires ${expectedListPageCount} unique list_page_urls`);
|
|
1016
|
+
}
|
|
1017
|
+
if (!Array.isArray(group.staging_list_urls)
|
|
1018
|
+
|| group.staging_list_urls.length !== expectedListPageCount
|
|
1019
|
+
|| new Set(group.staging_list_urls).size !== expectedListPageCount
|
|
1020
|
+
|| group.staging_list_urls.some((url) => typeof url !== "string" || url.length === 0)) {
|
|
1021
|
+
throw new Error(`multipart group ${group.multipart_group_id} requires ${expectedListPageCount} unique staging_list_urls`);
|
|
1022
|
+
}
|
|
1023
|
+
const metadataKeys = Object.keys(group.final_object_metadata);
|
|
1024
|
+
if (metadataKeys.length !== 2
|
|
1025
|
+
|| group.final_object_metadata["beam-transfer-id"] !== transferId
|
|
1026
|
+
|| group.final_object_metadata["beam-multipart-group-id"] !== group.multipart_group_id) {
|
|
1027
|
+
throw new Error(`multipart group ${group.multipart_group_id} has invalid final_object_metadata`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function multipartGroupStateKey(transferId, destinationId, sourceId, finalObjectKey) {
|
|
1032
|
+
return `${transferId}:${destinationId}:${sourceId}:${finalObjectKey}`;
|
|
1033
|
+
}
|
|
1034
|
+
const PART_ROUTE_METADATA_KEYS = new Set([
|
|
1035
|
+
"part_number",
|
|
1036
|
+
"logical_attempt_index",
|
|
1037
|
+
"attempt_slot",
|
|
1038
|
+
"route_generation_id",
|
|
1039
|
+
"delivery_index"
|
|
1040
|
+
]);
|
|
1041
|
+
function partRouteMetadata(metadata) {
|
|
1042
|
+
return Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => PART_ROUTE_METADATA_KEYS.has(key)));
|
|
1043
|
+
}
|
|
1044
|
+
function randomUuid() {
|
|
670
1045
|
const cryptoApi = globalThis.crypto;
|
|
671
1046
|
if (cryptoApi?.randomUUID)
|
|
672
1047
|
return cryptoApi.randomUUID();
|
|
673
1048
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
674
1049
|
}
|
|
1050
|
+
async function transferIdForIdempotencyKey(idempotencyKey) {
|
|
1051
|
+
if (!idempotencyKey?.trim())
|
|
1052
|
+
return randomUuid();
|
|
1053
|
+
const input = new TextEncoder().encode(`beam-transfer:${idempotencyKey.trim()}`);
|
|
1054
|
+
let digest;
|
|
1055
|
+
if (globalThis.crypto?.subtle) {
|
|
1056
|
+
digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", input));
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
const { createHash } = await import("node:crypto");
|
|
1060
|
+
digest = new Uint8Array(createHash("sha256").update(input).digest());
|
|
1061
|
+
}
|
|
1062
|
+
const bytes = digest.slice(0, 16);
|
|
1063
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50;
|
|
1064
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
1065
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1066
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1067
|
+
}
|
|
1068
|
+
function jitteredDelay(baseMs) {
|
|
1069
|
+
return Math.max(1, Math.round(baseMs * (0.8 + Math.random() * 0.4)));
|
|
1070
|
+
}
|
|
675
1071
|
function compact(input) {
|
|
676
1072
|
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
677
1073
|
}
|
|
678
1074
|
export function buildStagingObjectPrefix(input) {
|
|
679
|
-
const base = input.finalObjectKey.replace(/\/+$/, "") || "object";
|
|
680
1075
|
return [
|
|
681
|
-
`${
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1076
|
+
`${input.finalObjectKey}.beam-staging`,
|
|
1077
|
+
input.transferId,
|
|
1078
|
+
input.sourceId,
|
|
1079
|
+
input.destinationId
|
|
685
1080
|
].join("/");
|
|
686
1081
|
}
|
|
687
1082
|
export function buildStagingObjectKey(input) {
|
|
@@ -690,21 +1085,26 @@ export function buildStagingObjectKey(input) {
|
|
|
690
1085
|
`part-${input.partNumber}-chunk-${input.chunkIndex}`
|
|
691
1086
|
].join("/");
|
|
692
1087
|
}
|
|
693
|
-
function safeObjectKeySegment(value) {
|
|
694
|
-
return encodeURIComponent(value).replace(/%2F/gi, "_");
|
|
695
|
-
}
|
|
696
1088
|
function validateId(value, name) {
|
|
697
1089
|
if (!value || value.includes("/") || value.includes("..")) {
|
|
698
1090
|
throw new Error(`${name} is invalid`);
|
|
699
1091
|
}
|
|
700
1092
|
}
|
|
701
1093
|
async function abortCreatedUploads(uploads) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1094
|
+
const candidates = [...uploads.values()].filter((upload) => upload.uploadId && !isHippiusDestination(upload.destination));
|
|
1095
|
+
const results = await mapOrderedWithConcurrency(candidates, Math.min(32, Math.max(1, candidates.length)), async (upload) => {
|
|
1096
|
+
try {
|
|
1097
|
+
await abortMultipartUpload(upload.destination, upload.objectKey, upload.uploadId);
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
return error;
|
|
705
1102
|
}
|
|
706
|
-
|
|
707
|
-
|
|
1103
|
+
});
|
|
1104
|
+
const failures = results.filter((result) => result !== null);
|
|
1105
|
+
if (failures.length) {
|
|
1106
|
+
throw new AggregateError(failures, `failed to abort ${failures.length} multipart upload(s)`);
|
|
1107
|
+
}
|
|
708
1108
|
}
|
|
709
1109
|
function isHippiusDestination(destination) {
|
|
710
1110
|
return destination.provider === "hippius" && "api_token" in destination;
|