@beam-network/sdk 0.2.14 → 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.js +3 -1
- package/dist/models.d.ts +1 -0
- package/dist/provider-signing.d.ts +2 -0
- package/dist/provider-signing.js +97 -30
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -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
|
});
|
|
@@ -580,7 +581,8 @@ class ChunkChecksumCache {
|
|
|
580
581
|
chunks: misses,
|
|
581
582
|
expiresIn: this.options.expiresIn,
|
|
582
583
|
fetchImpl: this.options.fetchImpl,
|
|
583
|
-
groupSizeBytes: this.options.groupSizeBytes
|
|
584
|
+
groupSizeBytes: this.options.groupSizeBytes,
|
|
585
|
+
readTimeoutMs: this.options.readTimeoutMs
|
|
584
586
|
});
|
|
585
587
|
for (const chunk of misses) {
|
|
586
588
|
await this.options.throwIfCancelled?.();
|
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 {
|
|
@@ -63,12 +63,14 @@ export declare function computeChunksContentMd5(input: {
|
|
|
63
63
|
expiresIn: number;
|
|
64
64
|
fetchImpl?: typeof fetch;
|
|
65
65
|
groupSizeBytes?: number;
|
|
66
|
+
readTimeoutMs?: number;
|
|
66
67
|
}): Promise<Map<number, string>>;
|
|
67
68
|
export declare function computeChunkContentMd5(input: {
|
|
68
69
|
source: ProviderSourceConfig;
|
|
69
70
|
chunk: ChunkSigningPlanItem;
|
|
70
71
|
expiresIn: number;
|
|
71
72
|
fetchImpl?: typeof fetch;
|
|
73
|
+
readTimeoutMs?: number;
|
|
72
74
|
}): Promise<string>;
|
|
73
75
|
export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
|
|
74
76
|
export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
|
package/dist/provider-signing.js
CHANGED
|
@@ -4,6 +4,7 @@ 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
|
}
|
|
@@ -347,12 +348,13 @@ export async function computeChunksContentMd5(input) {
|
|
|
347
348
|
const chunks = [...input.chunks].sort((a, b) => a.source_offset - b.source_offset || a.source_chunk_index - b.source_chunk_index);
|
|
348
349
|
const groups = groupContiguousChunks(chunks, input.groupSizeBytes ?? 256 * 1024 * 1024);
|
|
349
350
|
for (const group of groups) {
|
|
350
|
-
const groupOutput = await
|
|
351
|
+
const groupOutput = await computeChunkGroupContentMd5Adaptive({
|
|
351
352
|
source: input.source,
|
|
352
353
|
chunks: group,
|
|
353
354
|
expiresIn: input.expiresIn,
|
|
354
|
-
fetchImpl: input.fetchImpl
|
|
355
|
-
|
|
355
|
+
fetchImpl: input.fetchImpl,
|
|
356
|
+
readTimeoutMs: input.readTimeoutMs
|
|
357
|
+
});
|
|
356
358
|
for (const [sourceChunkIndex, checksum] of groupOutput) {
|
|
357
359
|
output.set(sourceChunkIndex, checksum);
|
|
358
360
|
}
|
|
@@ -365,7 +367,8 @@ export async function computeChunkContentMd5(input) {
|
|
|
365
367
|
chunks: [input.chunk],
|
|
366
368
|
expiresIn: input.expiresIn,
|
|
367
369
|
fetchImpl: input.fetchImpl,
|
|
368
|
-
groupSizeBytes: input.chunk.chunk_size
|
|
370
|
+
groupSizeBytes: input.chunk.chunk_size,
|
|
371
|
+
readTimeoutMs: input.readTimeoutMs
|
|
369
372
|
});
|
|
370
373
|
const checksum = checksums.get(input.chunk.source_chunk_index);
|
|
371
374
|
if (!checksum) {
|
|
@@ -400,6 +403,25 @@ function groupContiguousChunks(chunks, maxGroupSizeBytes) {
|
|
|
400
403
|
groups.push(current);
|
|
401
404
|
return groups;
|
|
402
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
|
+
}
|
|
403
425
|
async function computeChunkGroupContentMd5(input) {
|
|
404
426
|
if (!input.chunks.length)
|
|
405
427
|
return new Map();
|
|
@@ -408,36 +430,73 @@ async function computeChunkGroupContentMd5(input) {
|
|
|
408
430
|
const last = chunks[chunks.length - 1];
|
|
409
431
|
const sourceOffset = first.source_offset;
|
|
410
432
|
const totalSize = last.source_offset + last.chunk_size - sourceOffset;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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));
|
|
420
445
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
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())]);
|
|
430
464
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
|
|
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]);
|
|
436
489
|
}
|
|
437
|
-
|
|
438
|
-
|
|
490
|
+
catch (error) {
|
|
491
|
+
if (controller.signal.aborted) {
|
|
492
|
+
throw timeoutError;
|
|
493
|
+
}
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
if (timer)
|
|
498
|
+
clearTimeout(timer);
|
|
439
499
|
}
|
|
440
|
-
return hashChunkGroupContentMd5(chunks, totalSize, [new Uint8Array(await response.arrayBuffer())]);
|
|
441
500
|
}
|
|
442
501
|
async function hashChunkGroupContentMd5(chunks, totalSize, body) {
|
|
443
502
|
const output = new Map();
|
|
@@ -538,6 +597,8 @@ async function retryChecksumRead(task) {
|
|
|
538
597
|
}
|
|
539
598
|
catch (error) {
|
|
540
599
|
lastError = error;
|
|
600
|
+
if (isChecksumReadTimeout(error))
|
|
601
|
+
break;
|
|
541
602
|
const status = typeof error === "object" && error && "status" in error ? Number(error.status) : undefined;
|
|
542
603
|
if (status && status >= 400 && status < 500 && status !== 408 && status !== 429)
|
|
543
604
|
break;
|
|
@@ -547,6 +608,12 @@ async function retryChecksumRead(task) {
|
|
|
547
608
|
}
|
|
548
609
|
throw lastError;
|
|
549
610
|
}
|
|
611
|
+
function isChecksumReadTimeout(error) {
|
|
612
|
+
return Boolean(error &&
|
|
613
|
+
typeof error === "object" &&
|
|
614
|
+
"code" in error &&
|
|
615
|
+
error.code === "CHECKSUM_READ_TIMEOUT");
|
|
616
|
+
}
|
|
550
617
|
function sleep(ms) {
|
|
551
618
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
552
619
|
}
|