@beam-network/sdk 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +6 -0
- package/dist/client.js +19 -8
- package/dist/models.d.ts +1 -0
- package/dist/provider-signing.d.ts +4 -0
- package/dist/provider-signing.js +106 -31
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -48,6 +48,12 @@ export declare class BeamClient {
|
|
|
48
48
|
private request;
|
|
49
49
|
}
|
|
50
50
|
export declare function calculateOptimalChunkSize(totalSize: number): number;
|
|
51
|
+
export declare function buildStagingObjectPrefix(input: {
|
|
52
|
+
finalObjectKey: string;
|
|
53
|
+
transferId: string;
|
|
54
|
+
sourceId: string;
|
|
55
|
+
destinationId: string;
|
|
56
|
+
}): string;
|
|
51
57
|
export declare function buildStagingObjectKey(input: {
|
|
52
58
|
finalObjectKey: string;
|
|
53
59
|
transferId: string;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortMultipartUpload, computeChunksContentMd5, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, prepareProviderSourceForPlan, isS3CompatibleProvider, listPartsPageMarker, listPartsVerifyMarker, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signListMultipartUpload, signUploadPartCopy } from "./provider-signing.js";
|
|
1
|
+
import { abortMultipartUpload, computeChunksContentMd5, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, prepareProviderSourceForPlan, isS3CompatibleProvider, listPartsPageMarker, listPartsVerifyMarker, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signListMultipartUpload, signListObjects, signUploadPartCopy } from "./provider-signing.js";
|
|
2
2
|
export const BEAM_DEFAULT_URL = "https://beamcore.b1m.ai";
|
|
3
3
|
export class BeamApiError extends Error {
|
|
4
4
|
status;
|
|
@@ -108,6 +108,7 @@ export class BeamClient {
|
|
|
108
108
|
fetchImpl: this.fetchImpl,
|
|
109
109
|
provider: input.checksumProvider,
|
|
110
110
|
groupSizeBytes: input.checksumGroupSizeBytes ?? 256 * 1024 * 1024,
|
|
111
|
+
readTimeoutMs: input.checksumReadTimeoutMs,
|
|
111
112
|
onEvent: input.onChecksumEvent,
|
|
112
113
|
throwIfCancelled: () => input.throwIfCancelled?.()
|
|
113
114
|
});
|
|
@@ -341,16 +342,19 @@ export class BeamClient {
|
|
|
341
342
|
fetchImpl: this.fetchImpl
|
|
342
343
|
});
|
|
343
344
|
}
|
|
344
|
-
const
|
|
345
|
+
const stagingObjectPrefix = buildStagingObjectPrefix({
|
|
345
346
|
finalObjectKey: input.finalObjectKey,
|
|
346
347
|
transferId: input.transferId,
|
|
347
348
|
sourceId: input.chunk.source_id,
|
|
348
|
-
destinationId: input.target.destination_id
|
|
349
|
-
chunkIndex: input.chunk.chunk_index,
|
|
350
|
-
partNumber: input.partNumber
|
|
349
|
+
destinationId: input.target.destination_id
|
|
351
350
|
});
|
|
351
|
+
const stagingObjectKey = [
|
|
352
|
+
stagingObjectPrefix,
|
|
353
|
+
`part-${input.partNumber}-chunk-${input.chunk.chunk_index}`
|
|
354
|
+
].join("/");
|
|
352
355
|
const copySourceRange = `bytes=0-${Math.max(0, input.chunk.chunk_size - 1)}`;
|
|
353
356
|
const { copyUrl, copySource } = await signUploadPartCopy(input.destination, input.finalObjectKey, stagingObjectKey, input.upload.uploadId, input.partNumber, input.expiresIn, copySourceRange);
|
|
357
|
+
const stagingListUrl = await signListObjects(input.destination, stagingObjectPrefix, input.expiresIn);
|
|
354
358
|
const deleteUrl = await signDeleteObject(input.destination, stagingObjectKey, input.expiresIn);
|
|
355
359
|
return signDestinationRoute({
|
|
356
360
|
chunk: input.chunk,
|
|
@@ -369,6 +373,7 @@ export class BeamClient {
|
|
|
369
373
|
multipartCreatedAt: input.upload.multipartCreatedAt,
|
|
370
374
|
commitMethod: "upload_part_copy",
|
|
371
375
|
stagingObjectKey,
|
|
376
|
+
stagingListUrl,
|
|
372
377
|
copyUrl,
|
|
373
378
|
copySource,
|
|
374
379
|
copySourceRange,
|
|
@@ -576,7 +581,8 @@ class ChunkChecksumCache {
|
|
|
576
581
|
chunks: misses,
|
|
577
582
|
expiresIn: this.options.expiresIn,
|
|
578
583
|
fetchImpl: this.options.fetchImpl,
|
|
579
|
-
groupSizeBytes: this.options.groupSizeBytes
|
|
584
|
+
groupSizeBytes: this.options.groupSizeBytes,
|
|
585
|
+
readTimeoutMs: this.options.readTimeoutMs
|
|
580
586
|
});
|
|
581
587
|
for (const chunk of misses) {
|
|
582
588
|
await this.options.throwIfCancelled?.();
|
|
@@ -716,13 +722,18 @@ function normalizeBaseUrl(value) {
|
|
|
716
722
|
function compact(input) {
|
|
717
723
|
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
718
724
|
}
|
|
719
|
-
export function
|
|
725
|
+
export function buildStagingObjectPrefix(input) {
|
|
720
726
|
const base = input.finalObjectKey.replace(/\/+$/, "") || "object";
|
|
721
727
|
return [
|
|
722
728
|
`${base}.beam-staging`,
|
|
723
729
|
safeObjectKeySegment(input.transferId),
|
|
724
730
|
safeObjectKeySegment(input.sourceId),
|
|
725
|
-
safeObjectKeySegment(input.destinationId)
|
|
731
|
+
safeObjectKeySegment(input.destinationId)
|
|
732
|
+
].join("/");
|
|
733
|
+
}
|
|
734
|
+
export function buildStagingObjectKey(input) {
|
|
735
|
+
return [
|
|
736
|
+
buildStagingObjectPrefix(input),
|
|
726
737
|
`part-${input.partNumber}-chunk-${input.chunkIndex}`
|
|
727
738
|
].join("/");
|
|
728
739
|
}
|
package/dist/models.d.ts
CHANGED
|
@@ -226,6 +226,7 @@ export interface ProviderTransferCreateInput {
|
|
|
226
226
|
checksumProvider?: ChunkChecksumProvider;
|
|
227
227
|
checksumReadConcurrency?: number;
|
|
228
228
|
checksumGroupSizeBytes?: number;
|
|
229
|
+
checksumReadTimeoutMs?: number;
|
|
229
230
|
onChecksumEvent?: (event: ChunkChecksumEvent) => void | Promise<void>;
|
|
230
231
|
}
|
|
231
232
|
export interface PlanningHttpSource {
|
|
@@ -17,6 +17,7 @@ export declare function prepareProviderDestination(destination: ProviderDestinat
|
|
|
17
17
|
export declare function createMultipartUpload(destination: ProviderDestinationConfig, objectKey: string): Promise<string>;
|
|
18
18
|
export declare function signCompleteMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
19
19
|
export declare function signAbortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
20
|
+
export declare function signListObjects(destination: ProviderDestinationConfig, prefix: string, expiresIn: number): Promise<string>;
|
|
20
21
|
export declare function signListMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number, options?: {
|
|
21
22
|
maxParts?: number;
|
|
22
23
|
partNumberMarker?: number;
|
|
@@ -49,6 +50,7 @@ export declare function signDestinationRoute(input: {
|
|
|
49
50
|
partVerifyUrl?: string;
|
|
50
51
|
listPageUrl?: string;
|
|
51
52
|
stagingObjectKey?: string;
|
|
53
|
+
stagingListUrl?: string;
|
|
52
54
|
copyUrl?: string;
|
|
53
55
|
copySource?: string;
|
|
54
56
|
copySourceRange?: string;
|
|
@@ -61,12 +63,14 @@ export declare function computeChunksContentMd5(input: {
|
|
|
61
63
|
expiresIn: number;
|
|
62
64
|
fetchImpl?: typeof fetch;
|
|
63
65
|
groupSizeBytes?: number;
|
|
66
|
+
readTimeoutMs?: number;
|
|
64
67
|
}): Promise<Map<number, string>>;
|
|
65
68
|
export declare function computeChunkContentMd5(input: {
|
|
66
69
|
source: ProviderSourceConfig;
|
|
67
70
|
chunk: ChunkSigningPlanItem;
|
|
68
71
|
expiresIn: number;
|
|
69
72
|
fetchImpl?: typeof fetch;
|
|
73
|
+
readTimeoutMs?: number;
|
|
70
74
|
}): Promise<string>;
|
|
71
75
|
export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
|
|
72
76
|
export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
|
package/dist/provider-signing.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCopyCommand, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCopyCommand, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
3
3
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
4
4
|
export function nowIso() {
|
|
5
5
|
return new Date().toISOString();
|
|
6
6
|
}
|
|
7
|
+
const DEFAULT_CHECKSUM_READ_TIMEOUT_MS = 300_000;
|
|
7
8
|
export function expiresAtIso(expiresIn) {
|
|
8
9
|
return new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
9
10
|
}
|
|
@@ -139,6 +140,13 @@ export async function signAbortMultipartUpload(destination, objectKey, uploadId,
|
|
|
139
140
|
}
|
|
140
141
|
throw new Error(`multipart abort is not supported for ${destination.provider}`);
|
|
141
142
|
}
|
|
143
|
+
export async function signListObjects(destination, prefix, expiresIn) {
|
|
144
|
+
if (isS3CompatibleProvider(destination)) {
|
|
145
|
+
const client = createS3CompatibleClient(destination);
|
|
146
|
+
return getSignedUrl(client, new ListObjectsV2Command({ Bucket: destination.bucket, Prefix: prefix }), { expiresIn });
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`multipart staging list is not supported for ${destination.provider}`);
|
|
149
|
+
}
|
|
142
150
|
export async function signListMultipartUpload(destination, objectKey, uploadId, expiresIn, options = {}) {
|
|
143
151
|
if (isS3CompatibleProvider(destination)) {
|
|
144
152
|
const client = createS3CompatibleClient(destination);
|
|
@@ -247,6 +255,7 @@ export async function signDestinationRoute(input) {
|
|
|
247
255
|
...(input.multipartCreatedAt ? { multipart_created_at: input.multipartCreatedAt } : {}),
|
|
248
256
|
...(input.commitMethod ? { commit_method: input.commitMethod } : {}),
|
|
249
257
|
...(input.stagingObjectKey ? { staging_object_key: input.stagingObjectKey } : {}),
|
|
258
|
+
...(input.stagingListUrl ? { staging_list_url: input.stagingListUrl } : {}),
|
|
250
259
|
...(input.copyUrl ? { copy_url: input.copyUrl } : {}),
|
|
251
260
|
...(input.copySource ? { copy_source: input.copySource } : {}),
|
|
252
261
|
...(input.copySourceRange ? { copy_source_range: input.copySourceRange } : {}),
|
|
@@ -339,12 +348,13 @@ export async function computeChunksContentMd5(input) {
|
|
|
339
348
|
const chunks = [...input.chunks].sort((a, b) => a.source_offset - b.source_offset || a.source_chunk_index - b.source_chunk_index);
|
|
340
349
|
const groups = groupContiguousChunks(chunks, input.groupSizeBytes ?? 256 * 1024 * 1024);
|
|
341
350
|
for (const group of groups) {
|
|
342
|
-
const groupOutput = await
|
|
351
|
+
const groupOutput = await computeChunkGroupContentMd5Adaptive({
|
|
343
352
|
source: input.source,
|
|
344
353
|
chunks: group,
|
|
345
354
|
expiresIn: input.expiresIn,
|
|
346
|
-
fetchImpl: input.fetchImpl
|
|
347
|
-
|
|
355
|
+
fetchImpl: input.fetchImpl,
|
|
356
|
+
readTimeoutMs: input.readTimeoutMs
|
|
357
|
+
});
|
|
348
358
|
for (const [sourceChunkIndex, checksum] of groupOutput) {
|
|
349
359
|
output.set(sourceChunkIndex, checksum);
|
|
350
360
|
}
|
|
@@ -357,7 +367,8 @@ export async function computeChunkContentMd5(input) {
|
|
|
357
367
|
chunks: [input.chunk],
|
|
358
368
|
expiresIn: input.expiresIn,
|
|
359
369
|
fetchImpl: input.fetchImpl,
|
|
360
|
-
groupSizeBytes: input.chunk.chunk_size
|
|
370
|
+
groupSizeBytes: input.chunk.chunk_size,
|
|
371
|
+
readTimeoutMs: input.readTimeoutMs
|
|
361
372
|
});
|
|
362
373
|
const checksum = checksums.get(input.chunk.source_chunk_index);
|
|
363
374
|
if (!checksum) {
|
|
@@ -392,6 +403,25 @@ function groupContiguousChunks(chunks, maxGroupSizeBytes) {
|
|
|
392
403
|
groups.push(current);
|
|
393
404
|
return groups;
|
|
394
405
|
}
|
|
406
|
+
async function computeChunkGroupContentMd5Adaptive(input) {
|
|
407
|
+
try {
|
|
408
|
+
return await retryChecksumRead(() => computeChunkGroupContentMd5(input));
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (input.chunks.length <= 1) {
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
const midpoint = Math.ceil(input.chunks.length / 2);
|
|
415
|
+
const output = new Map();
|
|
416
|
+
for (const chunks of [input.chunks.slice(0, midpoint), input.chunks.slice(midpoint)]) {
|
|
417
|
+
const partial = await computeChunkGroupContentMd5Adaptive({ ...input, chunks });
|
|
418
|
+
for (const [sourceChunkIndex, checksum] of partial) {
|
|
419
|
+
output.set(sourceChunkIndex, checksum);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return output;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
395
425
|
async function computeChunkGroupContentMd5(input) {
|
|
396
426
|
if (!input.chunks.length)
|
|
397
427
|
return new Map();
|
|
@@ -400,36 +430,73 @@ async function computeChunkGroupContentMd5(input) {
|
|
|
400
430
|
const last = chunks[chunks.length - 1];
|
|
401
431
|
const sourceOffset = first.source_offset;
|
|
402
432
|
const totalSize = last.source_offset + last.chunk_size - sourceOffset;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
433
|
+
return withChecksumReadTimeout(input.readTimeoutMs, async (abortSignal) => {
|
|
434
|
+
if (isS3CompatibleProvider(input.source)) {
|
|
435
|
+
const client = createS3CompatibleClient(input.source);
|
|
436
|
+
const response = await client.send(new GetObjectCommand({
|
|
437
|
+
Bucket: input.source.bucket,
|
|
438
|
+
Key: input.source.key,
|
|
439
|
+
Range: `bytes=${sourceOffset}-${sourceOffset + totalSize - 1}`
|
|
440
|
+
}), abortSignal ? { abortSignal } : undefined);
|
|
441
|
+
if (!response.Body) {
|
|
442
|
+
throw new Error(`chunk checksum read returned no body for ${input.source.bucket}/${input.source.key}`);
|
|
443
|
+
}
|
|
444
|
+
return hashChunkGroupContentMd5(chunks, totalSize, bodyToAsyncBytes(response.Body));
|
|
412
445
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
fetchImpl
|
|
446
|
+
const sourceRoute = await signSourceRangeRoute({
|
|
447
|
+
source: input.source,
|
|
448
|
+
fallbackUrl: first.source_url,
|
|
449
|
+
sourceOffset,
|
|
450
|
+
chunkSize: totalSize,
|
|
451
|
+
expiresIn: input.expiresIn,
|
|
452
|
+
fetchImpl: input.fetchImpl
|
|
453
|
+
});
|
|
454
|
+
const fetchImpl = input.fetchImpl ?? globalThis.fetch;
|
|
455
|
+
const response = await fetchImpl(sourceRoute.url, { method: "GET", headers: sourceRoute.headers, signal: abortSignal });
|
|
456
|
+
if (!response.ok) {
|
|
457
|
+
const message = `chunk checksum fetch failed with ${response.status}: ${await response.text()}`;
|
|
458
|
+
throw Object.assign(new Error(message), { status: response.status });
|
|
459
|
+
}
|
|
460
|
+
if (response.body) {
|
|
461
|
+
return hashChunkGroupContentMd5(chunks, totalSize, readableStreamToAsyncBytes(response.body));
|
|
462
|
+
}
|
|
463
|
+
return hashChunkGroupContentMd5(chunks, totalSize, [new Uint8Array(await response.arrayBuffer())]);
|
|
422
464
|
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
465
|
+
}
|
|
466
|
+
function normalizedChecksumReadTimeoutMs(value) {
|
|
467
|
+
if (value === undefined)
|
|
468
|
+
return DEFAULT_CHECKSUM_READ_TIMEOUT_MS;
|
|
469
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
470
|
+
return 0;
|
|
471
|
+
return Math.floor(value);
|
|
472
|
+
}
|
|
473
|
+
async function withChecksumReadTimeout(readTimeoutMs, task) {
|
|
474
|
+
const timeoutMs = normalizedChecksumReadTimeoutMs(readTimeoutMs);
|
|
475
|
+
if (timeoutMs <= 0) {
|
|
476
|
+
return task(undefined);
|
|
477
|
+
}
|
|
478
|
+
const controller = new AbortController();
|
|
479
|
+
const timeoutError = Object.assign(new Error(`chunk checksum read timed out after ${timeoutMs}ms`), { code: "CHECKSUM_READ_TIMEOUT" });
|
|
480
|
+
let timer;
|
|
481
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
482
|
+
timer = setTimeout(() => {
|
|
483
|
+
controller.abort(timeoutError);
|
|
484
|
+
reject(timeoutError);
|
|
485
|
+
}, timeoutMs);
|
|
486
|
+
});
|
|
487
|
+
try {
|
|
488
|
+
return await Promise.race([task(controller.signal), timeout]);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
if (controller.signal.aborted) {
|
|
492
|
+
throw timeoutError;
|
|
493
|
+
}
|
|
494
|
+
throw error;
|
|
428
495
|
}
|
|
429
|
-
|
|
430
|
-
|
|
496
|
+
finally {
|
|
497
|
+
if (timer)
|
|
498
|
+
clearTimeout(timer);
|
|
431
499
|
}
|
|
432
|
-
return hashChunkGroupContentMd5(chunks, totalSize, [new Uint8Array(await response.arrayBuffer())]);
|
|
433
500
|
}
|
|
434
501
|
async function hashChunkGroupContentMd5(chunks, totalSize, body) {
|
|
435
502
|
const output = new Map();
|
|
@@ -530,6 +597,8 @@ async function retryChecksumRead(task) {
|
|
|
530
597
|
}
|
|
531
598
|
catch (error) {
|
|
532
599
|
lastError = error;
|
|
600
|
+
if (isChecksumReadTimeout(error))
|
|
601
|
+
break;
|
|
533
602
|
const status = typeof error === "object" && error && "status" in error ? Number(error.status) : undefined;
|
|
534
603
|
if (status && status >= 400 && status < 500 && status !== 408 && status !== 429)
|
|
535
604
|
break;
|
|
@@ -539,6 +608,12 @@ async function retryChecksumRead(task) {
|
|
|
539
608
|
}
|
|
540
609
|
throw lastError;
|
|
541
610
|
}
|
|
611
|
+
function isChecksumReadTimeout(error) {
|
|
612
|
+
return Boolean(error &&
|
|
613
|
+
typeof error === "object" &&
|
|
614
|
+
"code" in error &&
|
|
615
|
+
error.code === "CHECKSUM_READ_TIMEOUT");
|
|
616
|
+
}
|
|
542
617
|
function sleep(ms) {
|
|
543
618
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
544
619
|
}
|