@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 +6 -0
- package/dist/client.js +54 -22
- package/dist/provider-signing.d.ts +2 -0
- package/dist/provider-signing.js +9 -1
- 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;
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/provider-signing.js
CHANGED
|
@@ -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 } : {}),
|