@beam-network/sdk 0.2.12 → 0.2.14

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;
@@ -135,7 +135,7 @@ export class BeamClient {
135
135
  const totalWarmupBytes = [...chunksBySource.values()].reduce((sum, chunks) => sum + chunks.reduce((chunkSum, chunk) => chunkSum + chunk.chunk_size, 0), 0);
136
136
  if (totalWarmupChunks > 0) {
137
137
  const startedAt = Date.now();
138
- emitChecksumEvent(input, {
138
+ await emitChecksumEvent(input, {
139
139
  event: "checksum_warmup_started",
140
140
  sourceId: "all",
141
141
  chunkCount: totalWarmupChunks,
@@ -149,7 +149,7 @@ export class BeamClient {
149
149
  }
150
150
  await checksumCache.ensureMany(source, chunks);
151
151
  }
152
- emitChecksumEvent(input, {
152
+ await emitChecksumEvent(input, {
153
153
  event: "checksum_warmup_completed",
154
154
  sourceId: "all",
155
155
  chunkCount: totalWarmupChunks,
@@ -158,6 +158,7 @@ export class BeamClient {
158
158
  });
159
159
  }
160
160
  await input.throwIfCancelled?.();
161
+ checksumCache.seal();
161
162
  preparedSources = await Promise.all(input.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
162
163
  await input.onBeforeTransferPrepare?.();
163
164
  prepared = await this.prepareTransfer({
@@ -201,11 +202,7 @@ export class BeamClient {
201
202
  const signedUrlFlow = prepared.signed_url_flow ?? requestedSignedUrlFlow;
202
203
  if (signedUrlFlow === "signed_url_v1") {
203
204
  const chunksBySource = checksumChunksBySource(prepared.chunk_plan, destinationsById);
204
- for (const [sourceId, chunks] of chunksBySource) {
205
- const source = sourcesById.get(sourceId);
206
- if (source)
207
- checksumCache.preloadMany(source, chunks);
208
- }
205
+ checksumCache.assertManyWarmed(chunksBySource);
209
206
  }
210
207
  const routes = [];
211
208
  const multipartUploads = new Map();
@@ -344,16 +341,19 @@ export class BeamClient {
344
341
  fetchImpl: this.fetchImpl
345
342
  });
346
343
  }
347
- const stagingObjectKey = buildStagingObjectKey({
344
+ const stagingObjectPrefix = buildStagingObjectPrefix({
348
345
  finalObjectKey: input.finalObjectKey,
349
346
  transferId: input.transferId,
350
347
  sourceId: input.chunk.source_id,
351
- destinationId: input.target.destination_id,
352
- chunkIndex: input.chunk.chunk_index,
353
- partNumber: input.partNumber
348
+ destinationId: input.target.destination_id
354
349
  });
350
+ const stagingObjectKey = [
351
+ stagingObjectPrefix,
352
+ `part-${input.partNumber}-chunk-${input.chunk.chunk_index}`
353
+ ].join("/");
355
354
  const copySourceRange = `bytes=0-${Math.max(0, input.chunk.chunk_size - 1)}`;
356
355
  const { copyUrl, copySource } = await signUploadPartCopy(input.destination, input.finalObjectKey, stagingObjectKey, input.upload.uploadId, input.partNumber, input.expiresIn, copySourceRange);
356
+ const stagingListUrl = await signListObjects(input.destination, stagingObjectPrefix, input.expiresIn);
357
357
  const deleteUrl = await signDeleteObject(input.destination, stagingObjectKey, input.expiresIn);
358
358
  return signDestinationRoute({
359
359
  chunk: input.chunk,
@@ -372,6 +372,7 @@ export class BeamClient {
372
372
  multipartCreatedAt: input.upload.multipartCreatedAt,
373
373
  commitMethod: "upload_part_copy",
374
374
  stagingObjectKey,
375
+ stagingListUrl,
375
376
  copyUrl,
376
377
  copySource,
377
378
  copySourceRange,
@@ -501,20 +502,28 @@ function planSignature(chunks) {
501
502
  })
502
503
  })));
503
504
  }
504
- function emitChecksumEvent(input, event) {
505
+ async function emitChecksumEvent(input, event) {
505
506
  if (!input.onChecksumEvent)
506
507
  return;
507
- void Promise.resolve(input.onChecksumEvent(event)).catch(() => undefined);
508
+ await input.onChecksumEvent(event);
508
509
  }
509
510
  class ChunkChecksumCache {
510
511
  options;
511
512
  queue = [];
512
513
  cache = new Map();
513
514
  active = 0;
515
+ sealed = false;
514
516
  constructor(options) {
515
517
  this.options = options;
516
518
  }
517
519
  preloadMany(source, chunks) {
520
+ if (this.sealed) {
521
+ const missing = this.uniqueChunks(chunks).filter((chunk) => !this.cache.has(this.key(chunk)));
522
+ if (missing.length) {
523
+ throw new Error(`checksum cache missing ${missing.length} chunk(s) after preflight warmup; refusing post-prepare source reads`);
524
+ }
525
+ return;
526
+ }
518
527
  const pending = this.uniqueChunks(chunks).filter((chunk) => !this.cache.has(this.key(chunk)));
519
528
  for (const chunk of pending) {
520
529
  this.cache.set(this.key(chunk), deferredChecksum());
@@ -532,6 +541,9 @@ class ChunkChecksumCache {
532
541
  const key = this.key(chunk);
533
542
  let existing = this.cache.get(key);
534
543
  if (!existing) {
544
+ if (this.sealed) {
545
+ return Promise.reject(new Error(`checksum cache missing ${key} after preflight warmup; refusing post-prepare source read`));
546
+ }
535
547
  existing = deferredChecksum();
536
548
  this.cache.set(key, existing);
537
549
  void this.run(() => this.fillGroup(source, [chunk])).catch(() => undefined);
@@ -550,12 +562,12 @@ class ChunkChecksumCache {
550
562
  continue;
551
563
  const cached = await this.options.provider?.getContentMd5({ source, chunk, algorithm: "content-md5" }).catch(() => undefined);
552
564
  if (cached) {
553
- this.emit({ event: "checksum_cache_hit", sourceId: chunk.source_id, sourceChunkIndex: chunk.source_chunk_index, bytes: chunk.chunk_size });
565
+ await this.emit({ event: "checksum_cache_hit", sourceId: chunk.source_id, sourceChunkIndex: chunk.source_chunk_index, bytes: chunk.chunk_size });
554
566
  deferred.resolve(cached);
555
567
  unresolved.delete(key);
556
568
  }
557
569
  else {
558
- this.emit({ event: "checksum_cache_miss", sourceId: chunk.source_id, sourceChunkIndex: chunk.source_chunk_index, bytes: chunk.chunk_size });
570
+ await this.emit({ event: "checksum_cache_miss", sourceId: chunk.source_id, sourceChunkIndex: chunk.source_chunk_index, bytes: chunk.chunk_size });
559
571
  misses.push(chunk);
560
572
  }
561
573
  }
@@ -579,9 +591,9 @@ class ChunkChecksumCache {
579
591
  const deferred = this.cache.get(this.key(chunk));
580
592
  deferred?.resolve(checksum);
581
593
  unresolved.delete(this.key(chunk));
582
- this.emit({ event: "checksum_cold_chunk_cached", sourceId: chunk.source_id, sourceChunkIndex: chunk.source_chunk_index, bytes: chunk.chunk_size });
594
+ await this.emit({ event: "checksum_cold_chunk_cached", sourceId: chunk.source_id, sourceChunkIndex: chunk.source_chunk_index, bytes: chunk.chunk_size });
583
595
  }
584
- this.emit({
596
+ await this.emit({
585
597
  event: "checksum_cold_group_completed",
586
598
  sourceId: misses[0]?.source_id ?? source.id ?? "source",
587
599
  chunkCount: misses.length,
@@ -637,6 +649,21 @@ class ChunkChecksumCache {
637
649
  key(chunk) {
638
650
  return `${chunk.source_id}:${chunk.source_chunk_index}`;
639
651
  }
652
+ seal() {
653
+ this.sealed = true;
654
+ }
655
+ assertManyWarmed(chunksBySource) {
656
+ const missing = [];
657
+ for (const chunks of chunksBySource.values()) {
658
+ for (const chunk of this.uniqueChunks(chunks)) {
659
+ if (!this.cache.has(this.key(chunk)))
660
+ missing.push(this.key(chunk));
661
+ }
662
+ }
663
+ if (missing.length) {
664
+ throw new Error(`checksum cache missing ${missing.length} chunk(s) after preflight warmup; refusing post-prepare source reads`);
665
+ }
666
+ }
640
667
  run(task) {
641
668
  return new Promise((resolve, reject) => {
642
669
  const start = () => {
@@ -652,10 +679,10 @@ class ChunkChecksumCache {
652
679
  this.queue.push(start);
653
680
  });
654
681
  }
655
- emit(event) {
682
+ async emit(event) {
656
683
  if (!this.options.onEvent)
657
684
  return;
658
- void Promise.resolve(this.options.onEvent(event)).catch(() => undefined);
685
+ await this.options.onEvent(event);
659
686
  }
660
687
  }
661
688
  function deferredChecksum() {
@@ -693,13 +720,18 @@ function normalizeBaseUrl(value) {
693
720
  function compact(input) {
694
721
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
695
722
  }
696
- export function buildStagingObjectKey(input) {
723
+ export function buildStagingObjectPrefix(input) {
697
724
  const base = input.finalObjectKey.replace(/\/+$/, "") || "object";
698
725
  return [
699
726
  `${base}.beam-staging`,
700
727
  safeObjectKeySegment(input.transferId),
701
728
  safeObjectKeySegment(input.sourceId),
702
- safeObjectKeySegment(input.destinationId),
729
+ safeObjectKeySegment(input.destinationId)
730
+ ].join("/");
731
+ }
732
+ export function buildStagingObjectKey(input) {
733
+ return [
734
+ buildStagingObjectPrefix(input),
703
735
  `part-${input.partNumber}-chunk-${input.chunkIndex}`
704
736
  ].join("/");
705
737
  }
@@ -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;
@@ -1,5 +1,5 @@
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();
@@ -139,6 +139,13 @@ export async function signAbortMultipartUpload(destination, objectKey, uploadId,
139
139
  }
140
140
  throw new Error(`multipart abort is not supported for ${destination.provider}`);
141
141
  }
142
+ export async function signListObjects(destination, prefix, expiresIn) {
143
+ if (isS3CompatibleProvider(destination)) {
144
+ const client = createS3CompatibleClient(destination);
145
+ return getSignedUrl(client, new ListObjectsV2Command({ Bucket: destination.bucket, Prefix: prefix }), { expiresIn });
146
+ }
147
+ throw new Error(`multipart staging list is not supported for ${destination.provider}`);
148
+ }
142
149
  export async function signListMultipartUpload(destination, objectKey, uploadId, expiresIn, options = {}) {
143
150
  if (isS3CompatibleProvider(destination)) {
144
151
  const client = createS3CompatibleClient(destination);
@@ -247,6 +254,7 @@ export async function signDestinationRoute(input) {
247
254
  ...(input.multipartCreatedAt ? { multipart_created_at: input.multipartCreatedAt } : {}),
248
255
  ...(input.commitMethod ? { commit_method: input.commitMethod } : {}),
249
256
  ...(input.stagingObjectKey ? { staging_object_key: input.stagingObjectKey } : {}),
257
+ ...(input.stagingListUrl ? { staging_list_url: input.stagingListUrl } : {}),
250
258
  ...(input.copyUrl ? { copy_url: input.copyUrl } : {}),
251
259
  ...(input.copySource ? { copy_source: input.copySource } : {}),
252
260
  ...(input.copySourceRange ? { copy_source_range: input.copySourceRange } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",