@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 +3 -1
- package/dist/client.d.ts +14 -0
- package/dist/client.js +47 -26
- package/dist/models.d.ts +1 -0
- package/dist/nats-control.js +6 -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
|
|
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.
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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.
|
|
1370
|
-
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]) => {
|
|
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
package/dist/nats-control.js
CHANGED
|
@@ -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
|
-
|
|
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) }
|
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.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
|
|
23
|
+
"test:functional": "npm run build && node --test test/*.test.mjs",
|
|
24
24
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|