@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 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 stagingObjectKey = buildStagingObjectKey({
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 buildStagingObjectKey(input) {
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;
@@ -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 retryChecksumRead(() => computeChunkGroupContentMd5({
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
- if (isS3CompatibleProvider(input.source)) {
404
- const client = createS3CompatibleClient(input.source);
405
- const response = await client.send(new GetObjectCommand({
406
- Bucket: input.source.bucket,
407
- Key: input.source.key,
408
- Range: `bytes=${sourceOffset}-${sourceOffset + totalSize - 1}`
409
- }));
410
- if (!response.Body) {
411
- 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));
412
445
  }
413
- return hashChunkGroupContentMd5(chunks, totalSize, bodyToAsyncBytes(response.Body));
414
- }
415
- const sourceRoute = await signSourceRangeRoute({
416
- source: input.source,
417
- fallbackUrl: first.source_url,
418
- sourceOffset,
419
- chunkSize: totalSize,
420
- expiresIn: input.expiresIn,
421
- 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())]);
422
464
  });
423
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
424
- const response = await fetchImpl(sourceRoute.url, { method: "GET", headers: sourceRoute.headers });
425
- if (!response.ok) {
426
- const message = `chunk checksum fetch failed with ${response.status}: ${await response.text()}`;
427
- 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]);
489
+ }
490
+ catch (error) {
491
+ if (controller.signal.aborted) {
492
+ throw timeoutError;
493
+ }
494
+ throw error;
428
495
  }
429
- if (response.body) {
430
- return hashChunkGroupContentMd5(chunks, totalSize, readableStreamToAsyncBytes(response.body));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",