@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 +3 -1
- package/dist/client.d.ts +19 -0
- package/dist/client.js +66 -30
- package/dist/models.d.ts +1 -0
- package/dist/nats-control.js +1 -1
- package/dist/provider-signing.js +1 -0
- package/package.json +2 -2
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` (
|
|
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
|
-
|
|
198
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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.
|
|
1355
|
-
const results = await mapOrderedWithConcurrency(candidates, Math.min(
|
|
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
package/dist/nats-control.js
CHANGED
|
@@ -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 =
|
|
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,
|
package/dist/provider-signing.js
CHANGED
|
@@ -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.
|
|
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
|
|
23
|
+
"test:functional": "npm run build && node --test test/*.test.mjs",
|
|
24
24
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|