@beam-network/sdk 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -98,7 +98,9 @@ For low-level raw transfer configs, use `createRawTransfer`; lifecycle transport
98
98
 
99
99
  Provider transfers use `transfer-client-control/v5`. Every reply carries Runtime and transport epochs; prepare and every route-stream message carry a UUID route generation. S3, R2, and S3-compatible destinations retain direct multipart UploadPart/ListParts/HEAD handling.
100
100
 
101
- The client signs up to 64 routes concurrently by default and emits 2,048-route logical batches, split only when encoded MessagePack requests exceed `maxPayloadBytes` (24 MiB by default). The stream ID derives from the transfer, selected flow, and immutable plan identity, and each ordered batch ID includes its route-coordinate checksum. Manual multi-destination attachment requires `delivery_index` on every route. Lifecycle mutations retry transient failures up to three times with the same request identity. `waitForTransfer` subscribes to the API-key-owned, at-most-once terminal signal before its first status read and reconciles every signal through authoritative status; subscription failure degrades to the same jittered 15-to-30-second status fallback. Call `close()` when the client is no longer needed; it also closes outstanding terminal waiters.
101
+ The client signs up to 64 routes concurrently by default and emits 2,048-route logical batches, split only when encoded MessagePack requests exceed `maxPayloadBytes` (8 MiB by default; explicit positive overrides remain supported). A single route larger than the configured limit fails before publication with both sizes in the error. Multipart create and abort requests use a separate `multipartControlConcurrency` limit of 2 by default, with explicit positive overrides supported, and S3-compatible control requests make up to five attempts for transient provider failures. This keeps URL signing throughput independent from provider control-plane pressure.
102
+
103
+ The stream ID derives from the transfer, selected flow, and immutable plan identity, and each ordered batch ID includes its route-coordinate checksum. Manual multi-destination attachment requires `delivery_index` on every route. Lifecycle mutations retry transient failures up to three times with the same request identity. A non-recoverable provider setup or route failure raises `BeamProviderTransferError`, whose `errors`, `transferCancelled`, and `multipartCleanupComplete` fields retain the original provider, cancellation, and cleanup outcomes. `waitForTransfer` subscribes to the API-key-owned, at-most-once terminal signal before its first status read and reconciles every signal through authoritative status; subscription failure degrades to the same jittered 15-to-30-second status fallback. Call `close()` when the client is no longer needed; it also closes outstanding terminal waiters.
102
104
 
103
105
  Hippius uses genuine non-multipart v2 routes, so its manifest is empty and group-level final HEAD verification is not available from that provider flow.
104
106
 
package/dist/client.d.ts CHANGED
@@ -1,6 +1,24 @@
1
1
  import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, MultipartGroupManifest, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, SignedUrlFlow, TransferCancelResponse, TransferCreateResponse, TransferPlanResponse, TransferPrepareResponse, TransferStatusInfo, TransferTerminalSignalWaiter } from "./models.js";
2
2
  import { BEAM_DEFAULT_NATS_URL } from "./nats-control.js";
3
+ export declare class BeamRouteRecoveryPendingError extends Error {
4
+ readonly transferId: string;
5
+ readonly cause: unknown;
6
+ constructor(transferId: string, cause: unknown);
7
+ }
3
8
  export { BEAM_DEFAULT_NATS_URL };
9
+ export declare const BEAM_DEFAULT_MULTIPART_CONTROL_CONCURRENCY = 2;
10
+ export declare class BeamProviderTransferError extends AggregateError {
11
+ readonly transferId: string;
12
+ readonly transferCancelled: boolean;
13
+ readonly multipartCleanupComplete: boolean;
14
+ readonly cause: unknown;
15
+ constructor(input: {
16
+ transferId: string;
17
+ cause: unknown;
18
+ cancelError?: unknown;
19
+ cleanupError?: unknown;
20
+ });
21
+ }
4
22
  export declare class BeamApiError extends Error {
5
23
  readonly status: number;
6
24
  readonly body: string;
@@ -13,6 +31,7 @@ export declare class BeamClient {
13
31
  private readonly control;
14
32
  private readonly routeSigningConcurrency;
15
33
  private readonly routeSigningConcurrencyOverridden;
34
+ private readonly multipartControlConcurrency;
16
35
  constructor(options?: BeamClientOptions);
17
36
  close(): Promise<void>;
18
37
  openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
package/dist/client.js CHANGED
@@ -1,7 +1,39 @@
1
1
  import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signListObjects, signUploadPartCopy } from "./provider-signing.js";
2
2
  import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
3
+ export class BeamRouteRecoveryPendingError extends Error {
4
+ transferId;
5
+ cause;
6
+ constructor(transferId, cause) {
7
+ super(`Transfer ${transferId} is prepared and route recovery is continuing in the background.`);
8
+ this.name = "BeamRouteRecoveryPendingError";
9
+ this.transferId = transferId;
10
+ this.cause = cause;
11
+ }
12
+ }
3
13
  import { multipartPartNumber } from "./multipart-limits.js";
4
14
  export { BEAM_DEFAULT_NATS_URL };
15
+ export const BEAM_DEFAULT_MULTIPART_CONTROL_CONCURRENCY = 2;
16
+ export class BeamProviderTransferError extends AggregateError {
17
+ transferId;
18
+ transferCancelled;
19
+ multipartCleanupComplete;
20
+ cause;
21
+ constructor(input) {
22
+ const errors = [input.cause];
23
+ if (input.cancelError !== undefined)
24
+ errors.push(input.cancelError);
25
+ if (input.cleanupError !== undefined)
26
+ errors.push(input.cleanupError);
27
+ const transferCancelled = input.cancelError === undefined;
28
+ const multipartCleanupComplete = input.cleanupError === undefined;
29
+ super(errors, `provider transfer failed for ${input.transferId} (transfer_cancelled=${transferCancelled}, multipart_cleanup_complete=${multipartCleanupComplete})`);
30
+ this.name = "BeamProviderTransferError";
31
+ this.transferId = input.transferId;
32
+ this.transferCancelled = transferCancelled;
33
+ this.multipartCleanupComplete = multipartCleanupComplete;
34
+ this.cause = input.cause;
35
+ }
36
+ }
5
37
  export class BeamApiError extends Error {
6
38
  status;
7
39
  body;
@@ -19,6 +51,7 @@ export class BeamClient {
19
51
  control;
20
52
  routeSigningConcurrency;
21
53
  routeSigningConcurrencyOverridden;
54
+ multipartControlConcurrency;
22
55
  constructor(options = {}) {
23
56
  if (!options.apiKey?.trim()) {
24
57
  throw new Error("apiKey is required.");
@@ -27,6 +60,7 @@ export class BeamClient {
27
60
  this.natsUrl = options.natsUrl ?? options.natsWsUrl ?? BEAM_DEFAULT_NATS_URL;
28
61
  this.routeSigningConcurrency = positiveInteger(options.routeSigningConcurrency ?? 64, "routeSigningConcurrency");
29
62
  this.routeSigningConcurrencyOverridden = options.routeSigningConcurrency !== undefined;
63
+ this.multipartControlConcurrency = positiveInteger(options.multipartControlConcurrency ?? BEAM_DEFAULT_MULTIPART_CONTROL_CONCURRENCY, "multipartControlConcurrency");
30
64
  this.control = new BeamTransferControl({
31
65
  apiKey: this.apiKey,
32
66
  natsUrl: options.natsUrl,
@@ -192,10 +226,11 @@ export class BeamClient {
192
226
  return attached;
193
227
  }
194
228
  catch (error) {
195
- if (isRecoverableRouteStreamError(error))
229
+ if (isRecoverableRouteStreamError(error)) {
196
230
  this.control.continueRecoveryLease(transferId);
197
- else
198
- this.control.releaseRecoveryLease(transferId);
231
+ throw new BeamRouteRecoveryPendingError(transferId, error);
232
+ }
233
+ this.control.releaseRecoveryLease(transferId);
199
234
  throw error;
200
235
  }
201
236
  finally {
@@ -297,8 +332,7 @@ export class BeamClient {
297
332
  multipartUploads,
298
333
  expiresIn,
299
334
  signedUrlFlow: requestedSignedUrlFlow,
300
- concurrency: this.routeSigningConcurrency,
301
- preserveUploadsOnFailure: recoveryReplay,
335
+ concurrency: this.multipartControlConcurrency,
302
336
  onGroupReady: async (state) => {
303
337
  if (requestedSignedUrlFlow === "signed_url_v2") {
304
338
  validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
@@ -381,9 +415,13 @@ export class BeamClient {
381
415
  if (multipartManifestTask)
382
416
  await Promise.allSettled([multipartManifestTask]);
383
417
  if (!recoveryReplay) {
384
- if (foregroundCancelled || isRecoverableRouteStreamError(error)) {
418
+ if (foregroundCancelled) {
385
419
  this.control.continueRecoveryLease(prepared.transfer_id);
386
420
  }
421
+ else if (isRecoverableRouteStreamError(error)) {
422
+ this.control.continueRecoveryLease(prepared.transfer_id);
423
+ throw new BeamRouteRecoveryPendingError(prepared.transfer_id, error);
424
+ }
387
425
  else {
388
426
  this.control.releaseRecoveryLease(prepared.transfer_id);
389
427
  await cancelAndAbortProviderFailure({
@@ -391,7 +429,8 @@ export class BeamClient {
391
429
  transferId: prepared.transfer_id,
392
430
  cause: error,
393
431
  uploads: multipartUploads,
394
- abortBeforeCancel: !routeStreamBeginAttempted
432
+ abortBeforeCancel: !routeStreamBeginAttempted,
433
+ cleanupConcurrency: this.multipartControlConcurrency
395
434
  });
396
435
  }
397
436
  }
@@ -919,7 +958,7 @@ async function cancelAndAbortProviderFailure(input) {
919
958
  let cancelError;
920
959
  if (input.abortBeforeCancel) {
921
960
  try {
922
- await abortCreatedUploads(input.uploads);
961
+ await abortCreatedUploads(input.uploads, input.cleanupConcurrency);
923
962
  }
924
963
  catch (error) {
925
964
  cleanupError = error;
@@ -934,7 +973,7 @@ async function cancelAndAbortProviderFailure(input) {
934
973
  }
935
974
  if (!input.abortBeforeCancel || cleanupFailed) {
936
975
  try {
937
- await abortCreatedUploads(input.uploads);
976
+ await abortCreatedUploads(input.uploads, input.cleanupConcurrency);
938
977
  cleanupError = undefined;
939
978
  cleanupFailed = false;
940
979
  }
@@ -943,14 +982,12 @@ async function cancelAndAbortProviderFailure(input) {
943
982
  cleanupFailed = true;
944
983
  }
945
984
  }
946
- if (cancelError && cleanupFailed) {
947
- throw new AggregateError([input.cause, cancelError, cleanupError], `provider transfer, transfer cancellation, and multipart cleanup failed for ${input.transferId}`);
948
- }
949
- if (cancelError)
950
- throw cancelError;
951
- if (cleanupFailed) {
952
- throw new AggregateError([input.cause, cleanupError], `provider transfer failed and multipart cleanup failed for ${input.transferId}`);
953
- }
985
+ throw new BeamProviderTransferError({
986
+ transferId: input.transferId,
987
+ cause: input.cause,
988
+ ...(cancelError !== undefined ? { cancelError } : {}),
989
+ ...(cleanupFailed ? { cleanupError } : {})
990
+ });
954
991
  }
955
992
  function safeErrorCode(error) {
956
993
  if (!(error instanceof Error))
@@ -1016,6 +1053,13 @@ async function createMultipartGroupManifest(input) {
1016
1053
  metadata: finalObjectMetadata
1017
1054
  });
1018
1055
  const createdUpload = !retainedUpload;
1056
+ if (createdUpload) {
1057
+ input.multipartUploads.set(multipartGroupId, {
1058
+ destination: destinationConfig,
1059
+ objectKey: finalObjectKey,
1060
+ uploadId
1061
+ });
1062
+ }
1019
1063
  try {
1020
1064
  const urlsExpiresAt = expiresAtIso(input.expiresIn);
1021
1065
  const stagingObjectPrefix = buildStagingObjectPrefix({
@@ -1076,6 +1120,7 @@ async function createMultipartGroupManifest(input) {
1076
1120
  if (createdUpload) {
1077
1121
  try {
1078
1122
  await abortMultipartUpload(destinationConfig, finalObjectKey, uploadId);
1123
+ input.multipartUploads.delete(multipartGroupId);
1079
1124
  }
1080
1125
  catch (abortError) {
1081
1126
  throw new AggregateError([error, abortError], `multipart group setup and cleanup failed for ${multipartGroupId}`);
@@ -1092,16 +1137,6 @@ async function createMultipartGroupManifest(input) {
1092
1137
  });
1093
1138
  const failure = results.find((result) => !result.ok);
1094
1139
  if (failure && !failure.ok) {
1095
- if (input.preserveUploadsOnFailure)
1096
- throw failure.error;
1097
- try {
1098
- await abortCreatedUploads(input.multipartUploads);
1099
- }
1100
- catch (cleanupError) {
1101
- input.multipartUploads.clear();
1102
- throw new AggregateError([failure.error, cleanupError], "multipart manifest setup and cleanup failed");
1103
- }
1104
- input.multipartUploads.clear();
1105
1140
  throw failure.error;
1106
1141
  }
1107
1142
  return results.flatMap((result) => result.ok && result.state ? [result.state.manifest] : []);
@@ -1350,11 +1385,12 @@ function validateId(value, name) {
1350
1385
  throw new Error(`${name} is invalid`);
1351
1386
  }
1352
1387
  }
1353
- async function abortCreatedUploads(uploads) {
1354
- const candidates = [...uploads.values()].filter((upload) => upload.uploadId && !isHippiusDestination(upload.destination));
1355
- const results = await mapOrderedWithConcurrency(candidates, Math.min(32, Math.max(1, candidates.length)), async (upload) => {
1388
+ async function abortCreatedUploads(uploads, concurrency) {
1389
+ const candidates = [...uploads.entries()].filter(([, upload]) => upload.uploadId && !isHippiusDestination(upload.destination));
1390
+ const results = await mapOrderedWithConcurrency(candidates, Math.min(concurrency, Math.max(1, candidates.length)), async ([groupId, upload]) => {
1356
1391
  try {
1357
1392
  await abortMultipartUpload(upload.destination, upload.objectKey, upload.uploadId);
1393
+ uploads.delete(groupId);
1358
1394
  return null;
1359
1395
  }
1360
1396
  catch (error) {
package/dist/models.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface BeamClientOptions {
9
9
  requestTimeoutMs?: number;
10
10
  maxPayloadBytes?: number;
11
11
  routeSigningConcurrency?: number;
12
+ multipartControlConcurrency?: number;
12
13
  fetch?: typeof fetch;
13
14
  }
14
15
  export interface SourceConfig {
@@ -5,7 +5,7 @@ export const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
5
5
  export const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
6
6
  // NATS enforces max_payload per message. This guard splits signed-route control
7
7
  // messages before the broker rejects them; transfer bytes never flow through NATS.
8
- export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 24 * 1024 * 1024;
8
+ export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
9
9
  export function buildConnectionOptions(options) {
10
10
  const connectionOptions = {
11
11
  servers: options.natsUrl,
@@ -347,6 +347,7 @@ function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source
347
347
  region: s3CompatibleRegion(source),
348
348
  endpoint,
349
349
  forcePathStyle: s3CompatibleForcePathStyle(source, endpoint),
350
+ maxAttempts: 5,
350
351
  credentials: {
351
352
  accessKeyId: source.access_key_id,
352
353
  secretAccessKey: source.secret_access_key,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "tsc -p tsconfig.json",
22
22
  "test": "tsc -p tsconfig.json --noEmit && npm run test:functional",
23
- "test:functional": "npm run build && node --test test/client.functional.test.mjs",
23
+ "test:functional": "npm run build && node --test test/*.test.mjs",
24
24
  "lint": "tsc -p tsconfig.json --noEmit"
25
25
  },
26
26
  "publishConfig": {