@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 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;
@@ -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 retryChecksumRead(() => computeChunkGroupContentMd5({
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
- if (isS3CompatibleProvider(input.source)) {
412
- const client = createS3CompatibleClient(input.source);
413
- const response = await client.send(new GetObjectCommand({
414
- Bucket: input.source.bucket,
415
- Key: input.source.key,
416
- Range: `bytes=${sourceOffset}-${sourceOffset + totalSize - 1}`
417
- }));
418
- if (!response.Body) {
419
- throw new Error(`chunk checksum read returned no body for ${input.source.bucket}/${input.source.key}`);
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
- return hashChunkGroupContentMd5(chunks, totalSize, bodyToAsyncBytes(response.Body));
422
- }
423
- const sourceRoute = await signSourceRangeRoute({
424
- source: input.source,
425
- fallbackUrl: first.source_url,
426
- sourceOffset,
427
- chunkSize: totalSize,
428
- expiresIn: input.expiresIn,
429
- fetchImpl: input.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
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
432
- const response = await fetchImpl(sourceRoute.url, { method: "GET", headers: sourceRoute.headers });
433
- if (!response.ok) {
434
- const message = `chunk checksum fetch failed with ${response.status}: ${await response.text()}`;
435
- throw Object.assign(new Error(message), { status: response.status });
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
- if (response.body) {
438
- return hashChunkGroupContentMd5(chunks, totalSize, readableStreamToAsyncBytes(response.body));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",