@beam-network/sdk 0.5.2 → 0.5.4

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` (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. 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 approach `maxPayloadBytes` (8 MiB by default; explicit positive overrides remain supported). The splitter reserves control-envelope headroom for the live auth token and stable request identity so the final encoded request remains below the configured guard. 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
@@ -6,6 +6,19 @@ export declare class BeamRouteRecoveryPendingError extends Error {
6
6
  constructor(transferId: string, cause: unknown);
7
7
  }
8
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
+ }
9
22
  export declare class BeamApiError extends Error {
10
23
  readonly status: number;
11
24
  readonly body: string;
@@ -18,6 +31,7 @@ export declare class BeamClient {
18
31
  private readonly control;
19
32
  private readonly routeSigningConcurrency;
20
33
  private readonly routeSigningConcurrencyOverridden;
34
+ private readonly multipartControlConcurrency;
21
35
  constructor(options?: BeamClientOptions);
22
36
  close(): Promise<void>;
23
37
  openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
package/dist/client.js CHANGED
@@ -12,6 +12,28 @@ export class BeamRouteRecoveryPendingError extends Error {
12
12
  }
13
13
  import { multipartPartNumber } from "./multipart-limits.js";
14
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
+ }
15
37
  export class BeamApiError extends Error {
16
38
  status;
17
39
  body;
@@ -29,6 +51,7 @@ export class BeamClient {
29
51
  control;
30
52
  routeSigningConcurrency;
31
53
  routeSigningConcurrencyOverridden;
54
+ multipartControlConcurrency;
32
55
  constructor(options = {}) {
33
56
  if (!options.apiKey?.trim()) {
34
57
  throw new Error("apiKey is required.");
@@ -37,6 +60,7 @@ export class BeamClient {
37
60
  this.natsUrl = options.natsUrl ?? options.natsWsUrl ?? BEAM_DEFAULT_NATS_URL;
38
61
  this.routeSigningConcurrency = positiveInteger(options.routeSigningConcurrency ?? 64, "routeSigningConcurrency");
39
62
  this.routeSigningConcurrencyOverridden = options.routeSigningConcurrency !== undefined;
63
+ this.multipartControlConcurrency = positiveInteger(options.multipartControlConcurrency ?? BEAM_DEFAULT_MULTIPART_CONTROL_CONCURRENCY, "multipartControlConcurrency");
40
64
  this.control = new BeamTransferControl({
41
65
  apiKey: this.apiKey,
42
66
  natsUrl: options.natsUrl,
@@ -308,8 +332,7 @@ export class BeamClient {
308
332
  multipartUploads,
309
333
  expiresIn,
310
334
  signedUrlFlow: requestedSignedUrlFlow,
311
- concurrency: this.routeSigningConcurrency,
312
- preserveUploadsOnFailure: recoveryReplay,
335
+ concurrency: this.multipartControlConcurrency,
313
336
  onGroupReady: async (state) => {
314
337
  if (requestedSignedUrlFlow === "signed_url_v2") {
315
338
  validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
@@ -406,7 +429,8 @@ export class BeamClient {
406
429
  transferId: prepared.transfer_id,
407
430
  cause: error,
408
431
  uploads: multipartUploads,
409
- abortBeforeCancel: !routeStreamBeginAttempted
432
+ abortBeforeCancel: !routeStreamBeginAttempted,
433
+ cleanupConcurrency: this.multipartControlConcurrency
410
434
  });
411
435
  }
412
436
  }
@@ -934,7 +958,7 @@ async function cancelAndAbortProviderFailure(input) {
934
958
  let cancelError;
935
959
  if (input.abortBeforeCancel) {
936
960
  try {
937
- await abortCreatedUploads(input.uploads);
961
+ await abortCreatedUploads(input.uploads, input.cleanupConcurrency);
938
962
  }
939
963
  catch (error) {
940
964
  cleanupError = error;
@@ -949,7 +973,7 @@ async function cancelAndAbortProviderFailure(input) {
949
973
  }
950
974
  if (!input.abortBeforeCancel || cleanupFailed) {
951
975
  try {
952
- await abortCreatedUploads(input.uploads);
976
+ await abortCreatedUploads(input.uploads, input.cleanupConcurrency);
953
977
  cleanupError = undefined;
954
978
  cleanupFailed = false;
955
979
  }
@@ -958,14 +982,12 @@ async function cancelAndAbortProviderFailure(input) {
958
982
  cleanupFailed = true;
959
983
  }
960
984
  }
961
- if (cancelError && cleanupFailed) {
962
- throw new AggregateError([input.cause, cancelError, cleanupError], `provider transfer, transfer cancellation, and multipart cleanup failed for ${input.transferId}`);
963
- }
964
- if (cancelError)
965
- throw cancelError;
966
- if (cleanupFailed) {
967
- throw new AggregateError([input.cause, cleanupError], `provider transfer failed and multipart cleanup failed for ${input.transferId}`);
968
- }
985
+ throw new BeamProviderTransferError({
986
+ transferId: input.transferId,
987
+ cause: input.cause,
988
+ ...(cancelError !== undefined ? { cancelError } : {}),
989
+ ...(cleanupFailed ? { cleanupError } : {})
990
+ });
969
991
  }
970
992
  function safeErrorCode(error) {
971
993
  if (!(error instanceof Error))
@@ -1031,6 +1053,13 @@ async function createMultipartGroupManifest(input) {
1031
1053
  metadata: finalObjectMetadata
1032
1054
  });
1033
1055
  const createdUpload = !retainedUpload;
1056
+ if (createdUpload) {
1057
+ input.multipartUploads.set(multipartGroupId, {
1058
+ destination: destinationConfig,
1059
+ objectKey: finalObjectKey,
1060
+ uploadId
1061
+ });
1062
+ }
1034
1063
  try {
1035
1064
  const urlsExpiresAt = expiresAtIso(input.expiresIn);
1036
1065
  const stagingObjectPrefix = buildStagingObjectPrefix({
@@ -1091,6 +1120,7 @@ async function createMultipartGroupManifest(input) {
1091
1120
  if (createdUpload) {
1092
1121
  try {
1093
1122
  await abortMultipartUpload(destinationConfig, finalObjectKey, uploadId);
1123
+ input.multipartUploads.delete(multipartGroupId);
1094
1124
  }
1095
1125
  catch (abortError) {
1096
1126
  throw new AggregateError([error, abortError], `multipart group setup and cleanup failed for ${multipartGroupId}`);
@@ -1107,16 +1137,6 @@ async function createMultipartGroupManifest(input) {
1107
1137
  });
1108
1138
  const failure = results.find((result) => !result.ok);
1109
1139
  if (failure && !failure.ok) {
1110
- if (input.preserveUploadsOnFailure)
1111
- throw failure.error;
1112
- try {
1113
- await abortCreatedUploads(input.multipartUploads);
1114
- }
1115
- catch (cleanupError) {
1116
- input.multipartUploads.clear();
1117
- throw new AggregateError([failure.error, cleanupError], "multipart manifest setup and cleanup failed");
1118
- }
1119
- input.multipartUploads.clear();
1120
1140
  throw failure.error;
1121
1141
  }
1122
1142
  return results.flatMap((result) => result.ok && result.state ? [result.state.manifest] : []);
@@ -1365,11 +1385,12 @@ function validateId(value, name) {
1365
1385
  throw new Error(`${name} is invalid`);
1366
1386
  }
1367
1387
  }
1368
- async function abortCreatedUploads(uploads) {
1369
- const candidates = [...uploads.values()].filter((upload) => upload.uploadId && !isHippiusDestination(upload.destination));
1370
- 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]) => {
1371
1391
  try {
1372
1392
  await abortMultipartUpload(upload.destination, upload.objectKey, upload.uploadId);
1393
+ uploads.delete(groupId);
1373
1394
  return null;
1374
1395
  }
1375
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 {
@@ -6,6 +6,7 @@ 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
8
  export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
9
+ const ROUTE_BATCH_AUTH_TOKEN_ESTIMATE_BYTES = 64 * 1024;
9
10
  export function buildConnectionOptions(options) {
10
11
  const connectionOptions = {
11
12
  servers: options.natsUrl,
@@ -284,6 +285,7 @@ export class BeamTransferControl {
284
285
  }
285
286
  }
286
287
  splitRoutesForPayload(messageType, basePayload, routes) {
288
+ const authTokenEstimateBytes = Math.min(ROUTE_BATCH_AUTH_TOKEN_ESTIMATE_BYTES, Math.max(512, Math.floor(this.maxPayloadBytes / 128)));
287
289
  const encodedBytes = (candidate) => {
288
290
  const encoded = encode({
289
291
  schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
@@ -292,7 +294,10 @@ export class BeamTransferControl {
292
294
  shard_id: 0,
293
295
  message_type: messageType,
294
296
  request_id: "00000000-0000-4000-8000-000000000000",
295
- auth_token: "x".repeat(512),
297
+ // The live JWT is larger than the compact placeholder once claims and
298
+ // signatures are encoded. Reserve enough space for token and envelope
299
+ // growth so a batch accepted by the estimator stays below the guard.
300
+ auth_token: "x".repeat(authTokenEstimateBytes),
296
301
  occurred_at: new Date().toISOString(),
297
302
  producer: "sdk",
298
303
  payload: { ...basePayload, route_batch: compactSignedRoutes(candidate) }
@@ -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.2",
3
+ "version": "0.5.4",
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": {