@beam-network/sdk 0.6.0 → 0.7.0

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.
@@ -1,3 +1,4 @@
1
+ import { currentSdkPerformance, measureSdkPhase } from "./performance.js";
1
2
  import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
2
3
  import { UploadPartCopyCommand, DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
3
4
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -69,7 +70,7 @@ export function nowIso() {
69
70
  return new Date().toISOString();
70
71
  }
71
72
  export function expiresAtIso(expiresIn) {
72
- return new Date(Date.now() + expiresIn * 1000).toISOString();
73
+ return new Date((Math.floor(Date.now() / 1000) + expiresIn) * 1000).toISOString();
73
74
  }
74
75
  export async function prepareProviderSource(source, options = {}) {
75
76
  const index = options.index ?? 0;
@@ -82,7 +83,7 @@ export async function prepareProviderSource(source, options = {}) {
82
83
  if (isS3CompatibleProvider(source)) {
83
84
  const endpoint = s3CompatibleEndpoint(source);
84
85
  const client = createS3CompatibleClient(source, endpoint);
85
- const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }), { abortSignal: options.signal });
86
+ const head = await measureSdkPhase("sdk.metadata_request", () => client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }), { abortSignal: options.signal }));
86
87
  const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: source.bucket, Key: source.key }), { expiresIn });
87
88
  return {
88
89
  source_id: source.id ?? `src_${index}`,
@@ -113,7 +114,7 @@ export async function prepareProviderSource(source, options = {}) {
113
114
  size,
114
115
  filename: filename(source.key),
115
116
  expires_at: expiresAtIso(expiresIn),
116
- metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
117
+ metadata: compactMetadata({ bucket: source.bucket, key: source.key, base_url: baseUrl, storage_location: source.storage_location })
117
118
  };
118
119
  }
119
120
  if (isHuggingFaceProvider(source)) {
@@ -161,7 +162,7 @@ export async function prepareProviderSourceForPlan(source, options = {}) {
161
162
  provider: "hippius",
162
163
  size,
163
164
  filename: filename(source.key),
164
- metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
165
+ metadata: compactMetadata({ bucket: source.bucket, key: source.key, base_url: baseUrl, storage_location: source.storage_location })
165
166
  };
166
167
  }
167
168
  if (isHuggingFaceProvider(source)) {
@@ -199,6 +200,7 @@ export function prepareProviderDestination(destination, options = {}) {
199
200
  metadata: {
200
201
  bucket: destination.bucket,
201
202
  key: destination.key,
203
+ storage_location: destination.storage_location,
202
204
  base_url: destination.base_url ?? "https://api.hippius.com"
203
205
  }
204
206
  };
@@ -307,21 +309,44 @@ export async function inspectDestinationObject(destination, objectKey, signal) {
307
309
  const result = await createS3CompatibleClient(destination).send(new HeadObjectCommand({ Bucket: destination.bucket, Key: objectKey }), { abortSignal: signal });
308
310
  return { size: result.ContentLength, etag: result.ETag, versionId: result.VersionId, metadata: result.Metadata ?? {} };
309
311
  }
312
+ // Never advertise a renewed lifetime for a previously issued provider URL.
313
+ export function boundedGrantExpiry(url, upperBound) {
314
+ let expiry = Date.parse(upperBound);
315
+ const query = new URL(url).searchParams;
316
+ for (const prefix of ["X-Amz", "X-Goog"]) {
317
+ const date = query.get(`${prefix}-Date`), duration = Number(query.get(`${prefix}-Expires`));
318
+ if (date && /^\d{8}T\d{6}Z$/.test(date) && Number.isFinite(duration) && duration > 0) {
319
+ const start = Date.parse(`${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}T${date.slice(9, 11)}:${date.slice(11, 13)}:${date.slice(13, 15)}Z`);
320
+ if (Number.isFinite(start))
321
+ expiry = Math.min(expiry, start + duration * 1000);
322
+ }
323
+ }
324
+ const epoch = Number(query.get("Expires"));
325
+ if (Number.isFinite(epoch) && epoch > 0)
326
+ expiry = Math.min(expiry, epoch * 1000);
327
+ return new Date(expiry).toISOString();
328
+ }
329
+ export async function signSourceChunk(input) {
330
+ const expiresAt = expiresAtIso(input.expiresIn);
331
+ const grant = await signSourceRoute(input);
332
+ return { ...grant, expiresAt: boundedGrantExpiry(grant.url, expiresAt) };
333
+ }
310
334
  export async function signDestinationRoute(input) {
311
335
  const targetObjectKey = input.target.object_key;
312
336
  if (!targetObjectKey) {
313
337
  throw new Error("destination signing target is missing object_key");
314
338
  }
339
+ const destinationExpiresAt = expiresAtIso(input.expiresIn);
315
340
  const [destUrl, sourceRoute] = await Promise.all([
316
- input.destUrl ?? signDestinationUrl({
341
+ input.destUrl ?? measureSdkPhase("sdk.destination_signing", () => signDestinationUrl({
317
342
  destination: input.destination,
318
343
  objectKey: targetObjectKey,
319
344
  uploadId: input.uploadId,
320
345
  partNumber: input.partNumber,
321
346
  expiresIn: input.expiresIn,
322
347
  fetchImpl: input.fetchImpl,
323
- }),
324
- signSourceRoute({
348
+ })),
349
+ input.sourceGrant ?? signSourceChunk({
325
350
  source: input.source,
326
351
  fallbackUrl: input.chunk.source_url,
327
352
  chunk: input.chunk,
@@ -337,8 +362,8 @@ export async function signDestinationRoute(input) {
337
362
  dest_url: destUrl,
338
363
  source_offset: input.chunk.source_offset,
339
364
  chunk_size: input.chunk.chunk_size,
340
- expires_at: expiresAtIso(input.expiresIn),
341
- headers: sourceRoute.headers,
365
+ expires_at: boundedGrantExpiry(destUrl, new Date(Math.min(Date.parse(destinationExpiresAt), Date.parse(sourceRoute.expiresAt))).toISOString()),
366
+ headers: { ...sourceRoute.headers },
342
367
  metadata: {
343
368
  ...(input.target.metadata ?? {}),
344
369
  ...(input.multipartGroupId ? { multipart_group_id: input.multipartGroupId } : {}),
@@ -475,7 +500,16 @@ export async function signDestinationUrl(input) {
475
500
  }
476
501
  return unsupportedProviderConfig(input.destination);
477
502
  }
478
- const s3ClientCache = new Map();
503
+ const s3ClientCache = new WeakMap();
504
+ export function releaseProviderClients(configs) {
505
+ for (const config of configs) {
506
+ if (!isS3CompatibleProvider(config))
507
+ continue;
508
+ const cached = s3ClientCache.get(config);
509
+ s3ClientCache.delete(config);
510
+ cached?.client.destroy();
511
+ }
512
+ }
479
513
  function s3ClientCacheKey(source, endpoint) {
480
514
  return JSON.stringify({
481
515
  provider: source.provider,
@@ -489,9 +523,12 @@ function s3ClientCacheKey(source, endpoint) {
489
523
  }
490
524
  function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source)) {
491
525
  const cacheKey = s3ClientCacheKey(source, endpoint);
492
- const cached = s3ClientCache.get(cacheKey);
493
- if (cached)
494
- return cached;
526
+ const cached = s3ClientCache.get(source);
527
+ if (cached?.identity === cacheKey) {
528
+ currentSdkPerformance()?.increment("provider_clients_reused");
529
+ return cached.client;
530
+ }
531
+ const setupStarted = performance.now();
495
532
  const client = new S3Client({
496
533
  region: s3CompatibleRegion(source),
497
534
  endpoint,
@@ -503,7 +540,9 @@ function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source
503
540
  sessionToken: "session_token" in source ? source.session_token : undefined
504
541
  }
505
542
  });
506
- s3ClientCache.set(cacheKey, client);
543
+ s3ClientCache.set(source, { identity: cacheKey, client });
544
+ currentSdkPerformance()?.increment("provider_clients_created");
545
+ currentSdkPerformance()?.observe("sdk.provider_client_setup", performance.now() - setupStarted);
507
546
  return client;
508
547
  }
509
548
  export function isS3CompatibleProvider(config) {
@@ -554,6 +593,7 @@ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
554
593
  bucket: source.bucket,
555
594
  key: source.key,
556
595
  region: s3CompatibleRegion(source),
596
+ storage_location: source.storage_location,
557
597
  endpoint_url: endpoint,
558
598
  account_id: "account_id" in source ? source.account_id : undefined
559
599
  });
@@ -567,6 +607,7 @@ export function isHuggingFaceProvider(config) {
567
607
  function huggingFaceMetadata(config, etag, commitHash) {
568
608
  return compactMetadata({
569
609
  driver: "huggingface",
610
+ storage_location: config.storage_location,
570
611
  repo_id: config.repo_id,
571
612
  repo_type: huggingFaceRepoType(config),
572
613
  revision: huggingFaceRevision(config),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",