@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.
package/dist/client.js CHANGED
@@ -1,4 +1,5 @@
1
- import { signMultipartRecovery, abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, isHuggingFaceProvider } from "./provider-signing.js";
1
+ import { SdkPerformanceCollector, SourceSignatureHistory, currentSdkPerformance, measureSdkPhase } from "./performance.js";
2
+ import { signMultipartRecovery, abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, releaseProviderClients, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, signSourceChunk, boundedGrantExpiry, isHuggingFaceProvider } from "./provider-signing.js";
2
3
  import { describe as describeHuggingFace, hashSourceStream, huggingFaceCommit, huggingFaceCompleteLfsUpload, huggingFaceLfsBatch, huggingFacePreupload, huggingFaceVerifyLfsUpload, readSourceSample } from "./huggingface.js";
3
4
  import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
4
5
  export class BeamRouteRecoveryPendingError extends Error {
@@ -53,11 +54,15 @@ export class BeamClient {
53
54
  routeSigningConcurrency;
54
55
  routeSigningConcurrencyOverridden;
55
56
  multipartControlConcurrency;
57
+ onDiagnostics;
58
+ diagnosticPending = false;
56
59
  recoverySigners = new Map();
57
60
  integrityAuditSigners = new Map();
61
+ integrityGrantCache = new Map();
58
62
  integrityAuditSubmissions = new Map();
59
63
  huggingFaceUploads = new Map();
60
64
  constructor(options = {}) {
65
+ this.onDiagnostics = options.onDiagnostics;
61
66
  if (!options.apiKey?.trim()) {
62
67
  throw new Error("apiKey is required.");
63
68
  }
@@ -237,7 +242,7 @@ export class BeamClient {
237
242
  };
238
243
  const routeStreamLock = new AsyncMutex();
239
244
  const releaseInitialStream = await routeStreamLock.acquire();
240
- this.control.registerRecoveryLease({
245
+ const recoveryLease = {
241
246
  transferId,
242
247
  planFingerprint: input.planFingerprint,
243
248
  coordinateChecksum: input.coordinateChecksum,
@@ -249,19 +254,20 @@ export class BeamClient {
249
254
  throw new Error(attached.error ?? attached.message ?? "route replay failed");
250
255
  });
251
256
  }
252
- });
257
+ };
258
+ this.control.registerRecoveryLease(recoveryLease);
253
259
  try {
254
260
  const attached = await streamRoutes(input.chunkRoutes, input.multipartGroupManifest, input.routeGenerationId, input.urlsExpiresAt);
255
261
  if (!attached.success)
256
- this.control.releaseRecoveryLease(transferId);
262
+ this.control.releaseRecoveryLease(transferId, recoveryLease);
257
263
  return attached;
258
264
  }
259
265
  catch (error) {
260
266
  if (isRecoverableRouteStreamError(error)) {
261
- this.control.continueRecoveryLease(transferId);
267
+ this.control.continueRecoveryLease(transferId, recoveryLease);
262
268
  throw new BeamRouteRecoveryPendingError(transferId, error);
263
269
  }
264
- this.control.releaseRecoveryLease(transferId);
270
+ this.control.releaseRecoveryLease(transferId, recoveryLease);
265
271
  throw error;
266
272
  }
267
273
  finally {
@@ -294,7 +300,8 @@ export class BeamClient {
294
300
  destinationsById.set(preparedDestination.destination_id, destination);
295
301
  });
296
302
  await input.throwIfCancelled?.();
297
- const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal })));
303
+ const discoveryTelemetry = new SdkPerformanceCollector();
304
+ const preparedSources = await discoveryTelemetry.measure("sdk.discovery", () => Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal }))));
298
305
  await input.throwIfCancelled?.();
299
306
  const huggingFace = await this.planHuggingFaceUploads({
300
307
  sources: retainedRecoveryInput.sources,
@@ -345,10 +352,18 @@ export class BeamClient {
345
352
  const assertOwnership = () => input.signal?.throwIfAborted();
346
353
  const routeStreamLock = new AsyncMutex();
347
354
  const releaseInitialStream = await routeStreamLock.acquire();
355
+ const sourceSignatureHistory = new SourceSignatureHistory(!resume);
348
356
  const streamPreparedRoutes = async (routeGenerationId, recoveryReplay) => {
357
+ const telemetry = new SdkPerformanceCollector(sourceSignatureHistory);
358
+ telemetry.seedDiscovery(discoveryTelemetry);
359
+ telemetry.gauge("signing_configured_limit", this.routeSigningConcurrency);
360
+ telemetry.gauge("multipart_configured_limit", this.multipartControlConcurrency);
361
+ telemetry.gauge("signing_limit", this.routeSigningConcurrency);
362
+ telemetry.gauge("multipart_limit", this.multipartControlConcurrency);
349
363
  const pendingRoutes = new Set();
350
364
  let signingConcurrency = this.routeSigningConcurrency;
351
365
  let signedInWindow = 0;
366
+ let activeSigning = 0;
352
367
  let signingWindowStartedAt = performance.now();
353
368
  let routeStream = null;
354
369
  let routeStreamBeginAttempted = false;
@@ -376,6 +391,15 @@ export class BeamClient {
376
391
  signedUrlFlow: requestedSignedUrlFlow
377
392
  });
378
393
  routeStream = new RouteStreamSender(this.control, {
394
+ telemetry,
395
+ onDiagnostics: (summary) => {
396
+ if (!this.onDiagnostics || this.diagnosticPending)
397
+ return;
398
+ this.diagnosticPending = true;
399
+ setTimeout(() => {
400
+ void Promise.resolve().then(() => this.onDiagnostics?.(summary)).catch(() => { }).finally(() => { this.diagnosticPending = false; });
401
+ }, 0);
402
+ },
379
403
  streamId,
380
404
  transferId: prepared.transfer_id,
381
405
  routeGenerationId,
@@ -388,7 +412,7 @@ export class BeamClient {
388
412
  routeStreamBeginAttempted = true;
389
413
  await routeStream.begin();
390
414
  multipartGroupWaiters = createMultipartGroupWaiters(prepared, destinationsById);
391
- multipartManifestTask = createMultipartGroupManifest({
415
+ multipartManifestTask = telemetry.measure("sdk.multipart_create", () => createMultipartGroupManifest({
392
416
  prepared,
393
417
  destinationsById,
394
418
  multipartUploads,
@@ -400,14 +424,14 @@ export class BeamClient {
400
424
  assertOwnership();
401
425
  recoveryMultipartUploads.set(state.manifest.multipart_group_id, state);
402
426
  validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
403
- await input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest));
427
+ await telemetry.measure("sdk.multipart_callback", async () => input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest)));
404
428
  await routeStream.addManifestGroups([state.manifest]);
405
429
  multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
406
430
  },
407
431
  onGroupFailed: (groupId, error) => multipartGroupWaiters.get(groupId)?.reject(error)
408
- });
432
+ }));
409
433
  const streamNextCompletedRoute = async () => {
410
- const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
434
+ const settled = await telemetry.measure("sdk.producer_wait", () => Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route })))));
411
435
  pendingRoutes.delete(settled.pending);
412
436
  assertOwnership();
413
437
  await routeStream.addRoute(settled.route);
@@ -416,6 +440,8 @@ export class BeamClient {
416
440
  const elapsedMs = performance.now() - signingWindowStartedAt;
417
441
  if (!this.routeSigningConcurrencyOverridden && elapsedMs > 4_000 && signingConcurrency < 256) {
418
442
  signingConcurrency = Math.min(256, signingConcurrency * 2);
443
+ telemetry.increment("concurrency_changes");
444
+ telemetry.gauge("signing_limit", signingConcurrency);
419
445
  }
420
446
  signedInWindow = 0;
421
447
  signingWindowStartedAt = performance.now();
@@ -423,6 +449,11 @@ export class BeamClient {
423
449
  };
424
450
  for (const chunk of materializePlanChunks(prepared.plan_descriptor, prepared.transfer_id)) {
425
451
  await throwIfCancelled?.(prepared.transfer_id);
452
+ // This promise belongs to this chunk and signing generation only. Its
453
+ // immutable credential/range identity and actual expiry cannot be renewed.
454
+ let sourceUses = 0;
455
+ const sourceGrant = telemetry.measure("sdk.source_signing", () => signSourceChunk({ source: sourcesById.get(chunk.source_id), fallbackUrl: chunk.source_url, chunk, expiresIn, fetchImpl: this.fetchImpl })).then(grant => { telemetry.sourceCreated(chunk.chunk_index); return grant; });
456
+ void sourceGrant.catch(() => { });
426
457
  for (const target of chunk.destinations) {
427
458
  const pendingRoute = (async () => {
428
459
  await throwIfCancelled?.(prepared.transfer_id);
@@ -439,13 +470,16 @@ export class BeamClient {
439
470
  const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
440
471
  const upload = isDirectPutDestination(destination)
441
472
  ? undefined
442
- : await multipartGroupWaiters.get(multipartGroupId)?.promise;
473
+ : await telemetry.measure("sdk.multipart_ready_wait", async () => multipartGroupWaiters.get(multipartGroupId)?.promise);
443
474
  if (!isDirectPutDestination(destination) && !upload)
444
475
  throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
445
- return this.signProviderRoute({
476
+ telemetry.gauge("signing_active_peak", ++activeSigning);
477
+ return telemetry.measure("sdk.signing", () => this.signProviderRoute({
446
478
  chunk,
447
479
  target,
448
480
  source,
481
+ sourceGrant: telemetry.measure("sdk.source_grant_wait", () => sourceGrant).then(grant => { if (sourceUses++ > 0)
482
+ telemetry.counters.source_reuses++; return grant; }),
449
483
  destination,
450
484
  expiresIn,
451
485
  ...(upload ? { upload } : {}),
@@ -456,11 +490,12 @@ export class BeamClient {
456
490
  partNumber: typeof metadata.part_number === "number"
457
491
  ? metadata.part_number
458
492
  : multipartPartNumber(chunk.source_chunk_index),
459
- });
493
+ })).finally(() => { activeSigning--; });
460
494
  })();
461
495
  pendingRoutes.add(pendingRoute);
496
+ telemetry.gauge("signing_pending_peak", pendingRoutes.size);
462
497
  if (pendingRoutes.size >= signingConcurrency)
463
- await streamNextCompletedRoute();
498
+ await telemetry.measure("sdk.signing_queue", streamNextCompletedRoute);
464
499
  }
465
500
  }
466
501
  while (pendingRoutes.size)
@@ -475,8 +510,8 @@ export class BeamClient {
475
510
  }
476
511
  catch (error) {
477
512
  if (input.signal?.aborted) {
478
- this.stopRecoverySigner(prepared.transfer_id);
479
- this.control.releaseRecoveryLease(prepared.transfer_id);
513
+ this.stopRecoverySigner(prepared.transfer_id, recoveryLease);
514
+ this.control.releaseRecoveryLease(prepared.transfer_id, recoveryLease);
480
515
  await Promise.allSettled(pendingRoutes);
481
516
  if (multipartManifestTask)
482
517
  await Promise.allSettled([multipartManifestTask]);
@@ -489,10 +524,10 @@ export class BeamClient {
489
524
  await Promise.allSettled([multipartManifestTask]);
490
525
  if (!recoveryReplay) {
491
526
  if (foregroundCancelled) {
492
- this.control.continueRecoveryLease(prepared.transfer_id);
527
+ this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
493
528
  }
494
529
  else if (isRecoverableRouteStreamError(error)) {
495
- this.control.continueRecoveryLease(prepared.transfer_id);
530
+ this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
496
531
  throw new BeamRouteRecoveryPendingError(prepared.transfer_id, error);
497
532
  }
498
533
  else {
@@ -510,27 +545,16 @@ export class BeamClient {
510
545
  // The retained recovery input owns the same destination objects used by
511
546
  // multipart cleanup. Releasing it earlier scrubs their scoped credentials
512
547
  // before abortMultipartUpload can clean up already-created uploads.
513
- this.control.releaseRecoveryLease(prepared.transfer_id);
548
+ this.control.releaseRecoveryLease(prepared.transfer_id, recoveryLease);
514
549
  }
515
550
  }
516
551
  }
517
552
  throw error;
518
553
  }
519
554
  };
520
- await this.startProviderRouteRecoverySigner({
521
- prepared,
522
- sourcesById,
523
- destinationsById,
524
- multipartUploads: recoveryMultipartUploads,
525
- signal: input.signal,
526
- expiresIn
527
- });
528
- const stopOwnedRecovery = () => {
529
- this.stopRecoverySigner(prepared.transfer_id);
530
- this.control.releaseRecoveryLease(prepared.transfer_id);
531
- };
532
- input.signal?.addEventListener("abort", stopOwnedRecovery, { once: true });
533
- this.control.registerRecoveryLease({
555
+ // The lease object is this owner's identity: releases, recovery requests, and
556
+ // signer shutdowns below only act while it is still the registered lease.
557
+ const recoveryLease = {
534
558
  transferId: prepared.transfer_id,
535
559
  planFingerprint: prepared.plan_fingerprint,
536
560
  coordinateChecksum: prepared.coordinate_checksum,
@@ -539,23 +563,39 @@ export class BeamClient {
539
563
  },
540
564
  disposeSecrets: () => {
541
565
  input.signal?.removeEventListener("abort", stopOwnedRecovery);
566
+ releaseProviderClients([...retainedRecoveryInput.sources, ...retainedRecoveryInput.destinations]);
542
567
  clearRecoverySecrets(retainedRecoveryInput);
543
568
  }
569
+ };
570
+ await this.startProviderRouteRecoverySigner({
571
+ owner: recoveryLease,
572
+ prepared,
573
+ sourcesById,
574
+ destinationsById,
575
+ multipartUploads: recoveryMultipartUploads,
576
+ signal: input.signal,
577
+ expiresIn
544
578
  });
579
+ const stopOwnedRecovery = () => {
580
+ this.stopRecoverySigner(prepared.transfer_id, recoveryLease);
581
+ this.control.releaseRecoveryLease(prepared.transfer_id, recoveryLease);
582
+ };
583
+ input.signal?.addEventListener("abort", stopOwnedRecovery, { once: true });
584
+ this.control.registerRecoveryLease(recoveryLease);
545
585
  try {
546
586
  try {
547
587
  assertOwnership();
548
588
  await input.onPrepared?.(prepared);
549
589
  }
550
590
  catch (error) {
551
- this.control.continueRecoveryLease(prepared.transfer_id);
591
+ this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
552
592
  throw error;
553
593
  }
554
594
  try {
555
595
  await input.throwIfCancelled?.(prepared.transfer_id);
556
596
  }
557
597
  catch (error) {
558
- this.control.continueRecoveryLease(prepared.transfer_id);
598
+ this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
559
599
  throw error;
560
600
  }
561
601
  await streamPreparedRoutes(prepared.route_generation_id, false);
@@ -740,6 +780,7 @@ export class BeamClient {
740
780
  metadata: directPutRouteMetadata(input.target.metadata)
741
781
  },
742
782
  source: input.source,
783
+ sourceGrant: input.sourceGrant,
743
784
  destination: input.destination,
744
785
  expiresIn: input.expiresIn,
745
786
  destUrl,
@@ -751,6 +792,7 @@ export class BeamClient {
751
792
  chunk: input.chunk,
752
793
  target: { ...input.target, metadata: partRouteMetadata(input.target.metadata) },
753
794
  source: input.source,
795
+ sourceGrant: input.sourceGrant,
754
796
  destination: input.destination,
755
797
  expiresIn: input.expiresIn,
756
798
  fetchImpl: this.fetchImpl
@@ -768,6 +810,7 @@ export class BeamClient {
768
810
  metadata: partRouteMetadata(input.target.metadata)
769
811
  },
770
812
  source: input.source,
813
+ sourceGrant: input.sourceGrant,
771
814
  destination: input.destination,
772
815
  expiresIn: input.expiresIn,
773
816
  uploadId: upload.uploadId,
@@ -791,6 +834,7 @@ export class BeamClient {
791
834
  input.signal?.throwIfAborted();
792
835
  if (payload.route_generation_id.length === 0)
793
836
  throw new Error("route recovery generation is required");
837
+ const sourceGrants = new Map();
794
838
  const routes = await mapOrderedWithConcurrency(payload.chunks, this.routeSigningConcurrency, async (requested) => {
795
839
  input.signal?.throwIfAborted();
796
840
  if (requested.route_generation_id !== payload.route_generation_id) {
@@ -821,11 +865,22 @@ export class BeamClient {
821
865
  multipartUploads: input.multipartUploads,
822
866
  expiresIn: input.expiresIn
823
867
  });
868
+ const sourceKey = JSON.stringify([chunk.source_id, chunk.chunk_index]);
869
+ let sourceGrant = sourceGrants.get(sourceKey);
870
+ if (!sourceGrant) {
871
+ sourceGrant = signSourceChunk({ source, chunk, fallbackUrl: chunk.source_url, expiresIn: input.expiresIn, fetchImpl: this.fetchImpl });
872
+ sourceGrants.set(sourceKey, sourceGrant);
873
+ void sourceGrant.catch(() => { });
874
+ }
824
875
  const route = await this.signProviderRoute({
876
+ sourceGrant,
825
877
  chunk,
826
878
  target: {
827
879
  ...target,
828
- object_key: requested.final_object_key,
880
+ // Direct-PUT destinations (Hippius, Hugging Face) keep the planned
881
+ // per-chunk key exactly as initial materialization does; only
882
+ // multipart destinations sign parts against the final object key.
883
+ object_key: isDirectPutDestination(destination) ? target.object_key : requested.final_object_key,
829
884
  metadata: {
830
885
  ...(target.metadata ?? {}),
831
886
  ...(requested.destination_metadata ?? {}),
@@ -861,16 +916,33 @@ export class BeamClient {
861
916
  chunk_routes: routes
862
917
  };
863
918
  });
864
- this.recoverySigners.set(input.prepared.transfer_id, stop);
865
- this.integrityAuditSigners.set(input.prepared.transfer_id, async (challenge) => {
866
- await this.submitProviderIntegrityAuditGrants({
867
- challenge,
868
- prepared: input.prepared,
869
- sourcesById: input.sourcesById,
870
- destinationsById: input.destinationsById,
871
- expiresIn: input.expiresIn
919
+ if (input.signal?.aborted) {
920
+ stop();
921
+ return;
922
+ }
923
+ this.stopRecoverySigner(input.prepared.transfer_id);
924
+ this.integrityAuditSigners.set(input.prepared.transfer_id, (challenge) => this.buildProviderIntegrityAuditGrants({
925
+ challenge, prepared: input.prepared, sourcesById: input.sourcesById, destinationsById: input.destinationsById, expiresIn: input.expiresIn
926
+ }));
927
+ const registration = { owner: input.owner, stop };
928
+ this.recoverySigners.set(input.prepared.transfer_id, registration);
929
+ try {
930
+ const stopIntegrity = await this.control.serveIntegritySigner(input.prepared.transfer_id, challenge => {
931
+ input.signal?.throwIfAborted();
932
+ return this.signedIntegrityGrants(challenge);
872
933
  });
873
- });
934
+ if (input.signal?.aborted || this.recoverySigners.get(input.prepared.transfer_id) !== registration) {
935
+ stop();
936
+ stopIntegrity();
937
+ return;
938
+ }
939
+ registration.stop = () => { stop(); stopIntegrity(); };
940
+ }
941
+ catch {
942
+ // Status-driven signing remains available if the optional subscription fails.
943
+ if (input.signal?.aborted)
944
+ this.stopRecoverySigner(input.prepared.transfer_id, input.owner);
945
+ }
874
946
  }
875
947
  async recoveryMultipartUploadState(input) {
876
948
  const existing = input.multipartUploads.get(input.requested.multipart_group_id);
@@ -937,21 +1009,56 @@ export class BeamClient {
937
1009
  if (!signer)
938
1010
  throw new Error("integrity audit signer unavailable");
939
1011
  const existing = this.integrityAuditSubmissions.get(challenge.audit_id);
940
- if (existing) {
1012
+ const cached = this.integrityGrantCache.get(challenge.audit_id);
1013
+ if (cached && cached.fingerprint !== JSON.stringify(challenge))
1014
+ throw new Error("integrity audit identity changed");
1015
+ if (existing && cached && cached.expiresAt > Date.now() + 30_000) {
941
1016
  await existing;
942
1017
  return;
943
1018
  }
944
- const submission = signer(challenge);
1019
+ const submission = this.signedIntegrityGrants(challenge).then(async (payload) => {
1020
+ const result = await this.control.request("transfer.integrity_audit_grants", payload, {
1021
+ transferId: challenge.transfer_id, idempotencyKey: `transfer:${challenge.transfer_id}:integrity-audit:${challenge.audit_id}:${payload.submitted_at}`
1022
+ });
1023
+ if (result.published !== true)
1024
+ throw new Error("integrity audit delivery unavailable");
1025
+ });
945
1026
  this.integrityAuditSubmissions.set(challenge.audit_id, submission);
946
1027
  try {
947
1028
  await submission;
948
1029
  }
949
1030
  catch (error) {
950
- this.integrityAuditSubmissions.delete(challenge.audit_id);
1031
+ if (this.integrityAuditSubmissions.get(challenge.audit_id) === submission)
1032
+ this.integrityAuditSubmissions.delete(challenge.audit_id);
951
1033
  throw error;
952
1034
  }
953
1035
  }
954
- async submitProviderIntegrityAuditGrants(input) {
1036
+ signedIntegrityGrants(challenge) {
1037
+ const fingerprint = JSON.stringify(challenge);
1038
+ const cached = this.integrityGrantCache.get(challenge.audit_id);
1039
+ if (cached && cached.fingerprint !== fingerprint)
1040
+ return Promise.reject(new Error("integrity audit identity changed"));
1041
+ if (cached && cached.expiresAt > Date.now() + 30_000)
1042
+ return cached.payload;
1043
+ if (!cached && this.integrityGrantCache.size >= 1024)
1044
+ return Promise.reject(new Error("integrity signer capacity unavailable"));
1045
+ const signer = this.integrityAuditSigners.get(challenge.transfer_id);
1046
+ if (!signer)
1047
+ return Promise.reject(new Error("integrity audit signer unavailable"));
1048
+ this.integrityAuditSubmissions.delete(challenge.audit_id);
1049
+ const entry = { transferId: challenge.transfer_id, expiresAt: 0, fingerprint, payload: Promise.resolve({}) };
1050
+ entry.payload = signer(challenge).then(payload => {
1051
+ const chunks = payload.chunks;
1052
+ entry.expiresAt = Math.min(...chunks.flatMap(chunk => [Date.parse(chunk.source.expires_at), Date.parse(chunk.destination.expires_at)]));
1053
+ return payload;
1054
+ }).catch(error => { if (this.integrityGrantCache.get(challenge.audit_id) === entry)
1055
+ this.integrityGrantCache.delete(challenge.audit_id); throw error; });
1056
+ // Pending grants are shared too. Actual expiry replaces this sentinel on completion.
1057
+ entry.expiresAt = Infinity;
1058
+ this.integrityGrantCache.set(challenge.audit_id, entry);
1059
+ return entry.payload;
1060
+ }
1061
+ async buildProviderIntegrityAuditGrants(input) {
955
1062
  if (input.challenge.transfer_id !== input.prepared.transfer_id) {
956
1063
  throw new Error("integrity audit challenge transfer mismatch");
957
1064
  }
@@ -978,6 +1085,7 @@ export class BeamClient {
978
1085
  }
979
1086
  const sourceEtag = plannedSource?.metadata?.etag;
980
1087
  const sourceVersionId = plannedSource?.metadata?.version_id;
1088
+ const grantExpiresAt = new Date(Math.floor(Date.now() / 1000) * 1000 + input.expiresIn * 1000).toISOString();
981
1089
  const [sourceGrant, destinationGrant] = await Promise.all([
982
1090
  signSourceReadRange({
983
1091
  source,
@@ -998,31 +1106,30 @@ export class BeamClient {
998
1106
  fetchImpl: this.fetchImpl
999
1107
  })
1000
1108
  ]);
1001
- const grantExpiresAt = expiresAtIso(input.expiresIn);
1002
1109
  const { orchestrator_id: _orchestratorId, orchestrator_hotkey: _orchestratorHotkey, worker_id: _workerId, ...grantChunk } = chunk;
1003
1110
  return {
1004
1111
  ...grantChunk,
1005
- source: { ...sourceGrant, expires_at: grantExpiresAt },
1006
- destination: { ...destinationGrant, expires_at: grantExpiresAt }
1112
+ source: { ...sourceGrant, expires_at: boundedGrantExpiry(sourceGrant.url, grantExpiresAt) },
1113
+ destination: { ...destinationGrant, expires_at: boundedGrantExpiry(destinationGrant.url, grantExpiresAt) }
1007
1114
  };
1008
1115
  });
1009
- await this.control.request("transfer.integrity_audit_grants", {
1010
- transfer_id: input.challenge.transfer_id,
1011
- audit_id: input.challenge.audit_id,
1012
- submitted_at: submittedAt,
1013
- chunks
1014
- }, {
1015
- transferId: input.challenge.transfer_id,
1016
- idempotencyKey: `transfer:${input.challenge.transfer_id}:integrity-audit:${input.challenge.audit_id}`
1017
- });
1116
+ return { transfer_id: input.challenge.transfer_id, audit_id: input.challenge.audit_id, submitted_at: submittedAt, chunks };
1018
1117
  }
1019
- stopRecoverySigner(transferId) {
1020
- const stop = this.recoverySigners.get(transferId);
1021
- if (stop) {
1022
- stop();
1118
+ /** Stop a transfer's recovery and integrity signers; with `owner`, only if that owner installed them. */
1119
+ stopRecoverySigner(transferId, owner) {
1120
+ const signer = this.recoverySigners.get(transferId);
1121
+ if (owner && signer?.owner !== owner)
1122
+ return;
1123
+ if (signer) {
1124
+ signer.stop();
1023
1125
  this.recoverySigners.delete(transferId);
1024
1126
  }
1025
1127
  this.integrityAuditSigners.delete(transferId);
1128
+ for (const [auditId, entry] of this.integrityGrantCache)
1129
+ if (entry.transferId === transferId) {
1130
+ this.integrityGrantCache.delete(auditId);
1131
+ this.integrityAuditSubmissions.delete(auditId);
1132
+ }
1026
1133
  }
1027
1134
  stopAllRecoverySigners() {
1028
1135
  for (const transferId of [...this.recoverySigners.keys()])
@@ -1118,7 +1225,7 @@ export class BeamClient {
1118
1225
  }
1119
1226
  }
1120
1227
  }
1121
- const ROUTE_STREAM_BATCH_ROUTES = 2_048;
1228
+ const ROUTE_STREAM_BATCH_ROUTES = 1_024;
1122
1229
  class AsyncMutex {
1123
1230
  tail = Promise.resolve();
1124
1231
  async acquire() {
@@ -1143,6 +1250,8 @@ class AsyncMutex {
1143
1250
  class RouteStreamSender {
1144
1251
  control;
1145
1252
  options;
1253
+ started = performance.now();
1254
+ telemetry;
1146
1255
  streamId;
1147
1256
  checksum = new RouteKeysChecksum();
1148
1257
  batch = [];
@@ -1151,12 +1260,15 @@ class RouteStreamSender {
1151
1260
  routeCount = 0;
1152
1261
  sendTail = Promise.resolve();
1153
1262
  sendError = null;
1263
+ sending = false;
1154
1264
  constructor(control, options) {
1155
1265
  this.control = control;
1156
1266
  this.options = options;
1157
1267
  this.streamId = options.streamId;
1268
+ this.telemetry = options.telemetry ?? new SdkPerformanceCollector();
1158
1269
  }
1159
1270
  async begin() {
1271
+ this.telemetry.startDelaySampling();
1160
1272
  await this.control.request("transfer.route_stream.begin", compact({
1161
1273
  transfer_id: this.options.transferId,
1162
1274
  route_generation_id: this.options.routeGenerationId,
@@ -1173,6 +1285,7 @@ class RouteStreamSender {
1173
1285
  });
1174
1286
  }
1175
1287
  async addManifestGroups(groups) {
1288
+ const started = performance.now();
1176
1289
  if (!groups.length)
1177
1290
  return;
1178
1291
  await Promise.all(groups.map(async (group) => {
@@ -1188,8 +1301,12 @@ class RouteStreamSender {
1188
1301
  idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:manifest:${identity}`
1189
1302
  });
1190
1303
  }));
1304
+ this.telemetry.observe("sdk.manifest_ack", performance.now() - started);
1305
+ this.telemetry.mark("first_manifest_ms");
1191
1306
  }
1192
1307
  async addRoute(route) {
1308
+ this.telemetry.mark("first_route_ms");
1309
+ const assemblyStarted = performance.now();
1193
1310
  const deliveryIndex = signedRouteDeliveryIndex(route);
1194
1311
  if (deliveryIndex === undefined) {
1195
1312
  throw new Error("route delivery_index is required");
@@ -1203,6 +1320,8 @@ class RouteStreamSender {
1203
1320
  this.seenDeliveryIndices.add(deliveryIndex);
1204
1321
  this.routeCount += 1;
1205
1322
  this.batch.push({ ...route, delivery_index: deliveryIndex });
1323
+ this.telemetry.gauge("buffered_batches_peak", 1 + Number(this.sending));
1324
+ this.telemetry.observe("sdk.route_assembly", performance.now() - assemblyStarted);
1206
1325
  if (this.batch.length >= ROUTE_STREAM_BATCH_ROUTES) {
1207
1326
  await this.enqueueFlush();
1208
1327
  }
@@ -1212,11 +1331,17 @@ class RouteStreamSender {
1212
1331
  this.seenDeliveryIndices.size !== this.options.totalRoutes) {
1213
1332
  throw new Error(`route stream has incomplete or duplicate delivery indices: received ${this.routeCount} of ${this.options.totalRoutes} routes`);
1214
1333
  }
1334
+ this.telemetry.mark("final_flush_ms");
1215
1335
  await this.enqueueFlush();
1216
1336
  await this.sendTail;
1217
1337
  if (this.sendError)
1218
1338
  throw this.sendError;
1339
+ this.telemetry.observe("sdk.preparation", performance.now() - this.started);
1340
+ this.telemetry.finish();
1341
+ const sdkPerformance = this.telemetry.snapshot(this.control.supportsPerformanceV2?.(this.options.transferId) ?? false);
1342
+ this.options.onDiagnostics?.(sdkPerformance);
1219
1343
  return this.control.request("transfer.route_stream.complete", {
1344
+ sdk_performance: sdkPerformance,
1220
1345
  transfer_id: this.options.transferId,
1221
1346
  route_generation_id: this.options.routeGenerationId,
1222
1347
  stream_id: this.streamId,
@@ -1231,10 +1356,11 @@ class RouteStreamSender {
1231
1356
  async enqueueFlush() {
1232
1357
  if (!this.batch.length)
1233
1358
  return;
1234
- await this.sendTail;
1359
+ await this.telemetry.measure("sdk.buffer_wait", () => this.sendTail);
1235
1360
  if (this.sendError)
1236
1361
  throw this.sendError;
1237
1362
  const routes = this.batch.splice(0, this.batch.length);
1363
+ const encodingStarted = performance.now();
1238
1364
  const chunks = this.control.splitRoutesForPayload("transfer.route_stream.batch", {
1239
1365
  transfer_id: this.options.transferId,
1240
1366
  route_generation_id: this.options.routeGenerationId,
@@ -1242,20 +1368,29 @@ class RouteStreamSender {
1242
1368
  batch_id: `${this.streamId}:estimate`,
1243
1369
  batch_index: this.batchIndex
1244
1370
  }, routes);
1371
+ this.telemetry.observe("sdk.batch_encode", performance.now() - encodingStarted);
1372
+ this.sending = true;
1245
1373
  for (const chunk of chunks) {
1374
+ const queuedAt = performance.now();
1246
1375
  const batchIndex = this.batchIndex;
1247
1376
  this.batchIndex += 1;
1248
1377
  this.sendTail = this.sendTail.then(async () => {
1378
+ this.telemetry.observe("sdk.batch_queue", performance.now() - queuedAt);
1249
1379
  if (this.sendError)
1250
1380
  return;
1251
1381
  try {
1382
+ const checksumStarted = performance.now();
1252
1383
  const chunkChecksum = new RouteKeysChecksum();
1253
1384
  await chunkChecksum.addMany(chunk.map(routeKeyForSignedRoute));
1254
1385
  this.checksum.merge(chunkChecksum);
1255
1386
  const routeChecksum = chunkChecksum.value();
1256
1387
  const coordinateChecksum = await routeCoordinateChecksum(chunk);
1257
1388
  const batchId = `${this.streamId}:${batchIndex}:${coordinateChecksum}`;
1258
- await this.control.request("transfer.route_stream.batch", {
1389
+ this.telemetry.observe("sdk.batch_checksum", performance.now() - checksumStarted);
1390
+ this.telemetry.gauge("batch_routes_max", chunk.length);
1391
+ this.telemetry.mark("first_batch_ms");
1392
+ this.telemetry.counters.route_batches++;
1393
+ await this.telemetry.measure("sdk.batch_ack", () => this.control.request("transfer.route_stream.batch", {
1259
1394
  transfer_id: this.options.transferId,
1260
1395
  route_generation_id: this.options.routeGenerationId,
1261
1396
  stream_id: this.streamId,
@@ -1267,15 +1402,17 @@ class RouteStreamSender {
1267
1402
  }, {
1268
1403
  transferId: this.options.transferId,
1269
1404
  idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:batch:${batchIndex}:${coordinateChecksum}`
1270
- });
1405
+ }));
1271
1406
  }
1272
1407
  catch (error) {
1273
1408
  this.sendError = error;
1274
1409
  }
1275
1410
  });
1276
1411
  }
1412
+ this.sendTail = this.sendTail.finally(() => { this.sending = false; });
1277
1413
  }
1278
1414
  async abort() {
1415
+ this.telemetry.finish();
1279
1416
  this.sendError ??= new Error("route_stream_aborted");
1280
1417
  await this.sendTail;
1281
1418
  }
@@ -1511,6 +1648,9 @@ function createDeferred() {
1511
1648
  resolve = resolvePromise;
1512
1649
  reject = rejectPromise;
1513
1650
  });
1651
+ // A waiter may be rejected after its route loop already stopped (for example when
1652
+ // ownership is fenced mid-stream); awaiting callers still observe the rejection.
1653
+ promise.catch(() => undefined);
1514
1654
  return { promise, resolve, reject };
1515
1655
  }
1516
1656
  function createMultipartGroupWaiters(prepared, destinationsById) {
@@ -1562,7 +1702,12 @@ function restoreProviderMultipartIdentities(prepared, destinations, identities,
1562
1702
  }
1563
1703
  async function createMultipartGroupManifest(input) {
1564
1704
  const groups = input.prepared.plan_descriptor.sources.flatMap((source) => input.prepared.plan_descriptor.destinations.map((destination) => ({ source, destination })));
1705
+ const queuedAt = performance.now();
1706
+ const telemetry = currentSdkPerformance();
1707
+ let active = 0;
1565
1708
  const results = await mapOrderedWithConcurrency(groups, input.concurrency, async ({ source, destination }) => {
1709
+ telemetry?.observe("sdk.multipart_queue", performance.now() - queuedAt);
1710
+ telemetry?.gauge("multipart_active_peak", ++active);
1566
1711
  let multipartGroupId = null;
1567
1712
  try {
1568
1713
  const destinationConfig = input.destinationsById.get(destination.destination_id);
@@ -1580,12 +1725,12 @@ async function createMultipartGroupManifest(input) {
1580
1725
  const finalObjectMetadata = { "beam-transfer-id": input.prepared.transfer_id };
1581
1726
  input.signal?.throwIfAborted();
1582
1727
  const retainedUpload = input.multipartUploads.get(multipartGroupId);
1583
- const uploadId = retainedUpload?.uploadId ?? await createMultipartUpload({
1728
+ const uploadId = retainedUpload?.uploadId ?? await measureSdkPhase("sdk.multipart_provider", () => createMultipartUpload({
1584
1729
  destination: destinationConfig,
1585
1730
  objectKey: finalObjectKey,
1586
1731
  metadata: finalObjectMetadata,
1587
1732
  signal: input.signal
1588
- });
1733
+ }));
1589
1734
  const createdUpload = !retainedUpload;
1590
1735
  if (createdUpload) {
1591
1736
  input.multipartUploads.set(multipartGroupId, {
@@ -1648,6 +1793,9 @@ async function createMultipartGroupManifest(input) {
1648
1793
  input.onGroupFailed(multipartGroupId, error);
1649
1794
  return { ok: false, error };
1650
1795
  }
1796
+ finally {
1797
+ active--;
1798
+ }
1651
1799
  });
1652
1800
  const failure = results.find((result) => !result.ok);
1653
1801
  if (failure && !failure.ok) {