@beam-network/sdk 0.3.0 → 0.4.1
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.d.ts +1 -0
- package/dist/client.js +123 -44
- package/dist/models.d.ts +15 -7
- package/dist/nats-control.d.ts +8 -3
- package/dist/nats-control.js +26 -10
- package/dist/provider-signing.js +5 -4
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class BeamClient {
|
|
|
12
12
|
private readonly fetchImpl;
|
|
13
13
|
private readonly control;
|
|
14
14
|
private readonly routeSigningConcurrency;
|
|
15
|
+
private readonly routeSigningConcurrencyOverridden;
|
|
15
16
|
constructor(options?: BeamClientOptions);
|
|
16
17
|
close(): Promise<void>;
|
|
17
18
|
openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
|
package/dist/client.js
CHANGED
|
@@ -18,6 +18,7 @@ export class BeamClient {
|
|
|
18
18
|
fetchImpl;
|
|
19
19
|
control;
|
|
20
20
|
routeSigningConcurrency;
|
|
21
|
+
routeSigningConcurrencyOverridden;
|
|
21
22
|
constructor(options = {}) {
|
|
22
23
|
if (!options.apiKey?.trim()) {
|
|
23
24
|
throw new Error("apiKey is required.");
|
|
@@ -25,6 +26,7 @@ export class BeamClient {
|
|
|
25
26
|
this.apiKey = options.apiKey;
|
|
26
27
|
this.natsUrl = options.natsUrl ?? options.natsWsUrl ?? BEAM_DEFAULT_NATS_URL;
|
|
27
28
|
this.routeSigningConcurrency = positiveInteger(options.routeSigningConcurrency ?? 64, "routeSigningConcurrency");
|
|
29
|
+
this.routeSigningConcurrencyOverridden = options.routeSigningConcurrency !== undefined;
|
|
28
30
|
this.control = new BeamTransferControl({
|
|
29
31
|
apiKey: this.apiKey,
|
|
30
32
|
natsUrl: options.natsUrl,
|
|
@@ -148,12 +150,14 @@ export class BeamClient {
|
|
|
148
150
|
totalRoutes: orderedRoutes.length,
|
|
149
151
|
totalChunks: countDistinctRouteChunks(orderedRoutes),
|
|
150
152
|
urlsExpiresAt: input.urlsExpiresAt,
|
|
151
|
-
multipartGroupManifest: input.multipartGroupManifest,
|
|
152
153
|
autoDistribute: input.autoDistribute ?? true
|
|
153
154
|
});
|
|
154
155
|
await stream.begin();
|
|
156
|
+
if (input.multipartGroupManifest.length) {
|
|
157
|
+
await stream.addManifestGroups(input.multipartGroupManifest);
|
|
158
|
+
}
|
|
155
159
|
for (const route of orderedRoutes) {
|
|
156
|
-
stream.addRoute(route);
|
|
160
|
+
await stream.addRoute(route);
|
|
157
161
|
}
|
|
158
162
|
return stream.complete();
|
|
159
163
|
}
|
|
@@ -196,17 +200,14 @@ export class BeamClient {
|
|
|
196
200
|
});
|
|
197
201
|
const multipartUploads = new Map();
|
|
198
202
|
const pendingRoutes = new Set();
|
|
203
|
+
let signingConcurrency = this.routeSigningConcurrency;
|
|
204
|
+
let signedInWindow = 0;
|
|
205
|
+
let signingWindowStartedAt = performance.now();
|
|
199
206
|
let routeStream = null;
|
|
200
207
|
let routeStreamBeginAttempted = false;
|
|
208
|
+
let multipartManifestTask = null;
|
|
209
|
+
let multipartGroupWaiters = new Map();
|
|
201
210
|
try {
|
|
202
|
-
const multipartGroupManifest = await createMultipartGroupManifest({
|
|
203
|
-
prepared,
|
|
204
|
-
destinationsById,
|
|
205
|
-
multipartUploads,
|
|
206
|
-
expiresIn,
|
|
207
|
-
concurrency: this.routeSigningConcurrency
|
|
208
|
-
});
|
|
209
|
-
validateMultipartGroupManifest(multipartGroupManifest, prepared.transfer_id);
|
|
210
211
|
const streamId = await stableRouteStreamId({
|
|
211
212
|
transferId: prepared.transfer_id,
|
|
212
213
|
planIdentity: prepared.plan_descriptor.plan_nonce,
|
|
@@ -218,15 +219,37 @@ export class BeamClient {
|
|
|
218
219
|
transferId: prepared.transfer_id,
|
|
219
220
|
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
220
221
|
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
221
|
-
multipartGroupManifest,
|
|
222
222
|
autoDistribute: input.distribute !== false
|
|
223
223
|
});
|
|
224
224
|
routeStreamBeginAttempted = true;
|
|
225
225
|
await routeStream.begin();
|
|
226
|
+
multipartGroupWaiters = createMultipartGroupWaiters(prepared, destinationsById);
|
|
227
|
+
multipartManifestTask = createMultipartGroupManifest({
|
|
228
|
+
prepared,
|
|
229
|
+
destinationsById,
|
|
230
|
+
multipartUploads,
|
|
231
|
+
expiresIn,
|
|
232
|
+
concurrency: this.routeSigningConcurrency,
|
|
233
|
+
onGroupReady: async (state) => {
|
|
234
|
+
validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
|
|
235
|
+
await routeStream.addManifestGroups([state.manifest]);
|
|
236
|
+
multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
|
|
237
|
+
},
|
|
238
|
+
onGroupFailed: (groupId, error) => multipartGroupWaiters.get(groupId)?.reject(error)
|
|
239
|
+
});
|
|
226
240
|
const streamNextCompletedRoute = async () => {
|
|
227
241
|
const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
|
|
228
242
|
pendingRoutes.delete(settled.pending);
|
|
229
|
-
routeStream.addRoute(settled.route);
|
|
243
|
+
await routeStream.addRoute(settled.route);
|
|
244
|
+
signedInWindow += 1;
|
|
245
|
+
if (signedInWindow === ROUTE_STREAM_BATCH_ROUTES) {
|
|
246
|
+
const elapsedMs = performance.now() - signingWindowStartedAt;
|
|
247
|
+
if (!this.routeSigningConcurrencyOverridden && elapsedMs > 4_000 && signingConcurrency < 256) {
|
|
248
|
+
signingConcurrency = Math.min(256, signingConcurrency * 2);
|
|
249
|
+
}
|
|
250
|
+
signedInWindow = 0;
|
|
251
|
+
signingWindowStartedAt = performance.now();
|
|
252
|
+
}
|
|
230
253
|
};
|
|
231
254
|
for (const chunk of materializePlanChunks(prepared.plan_descriptor, prepared.transfer_id)) {
|
|
232
255
|
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
@@ -243,10 +266,12 @@ export class BeamClient {
|
|
|
243
266
|
const finalObjectKey = typeof metadata.final_object_key === "string" ? metadata.final_object_key : target.object_key;
|
|
244
267
|
if (!finalObjectKey)
|
|
245
268
|
throw new Error("destination signing target is missing object_key");
|
|
246
|
-
const
|
|
247
|
-
|
|
269
|
+
const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
|
|
270
|
+
const upload = isHippiusDestination(destination)
|
|
271
|
+
? undefined
|
|
272
|
+
: await multipartGroupWaiters.get(multipartGroupId)?.promise;
|
|
273
|
+
if (!isHippiusDestination(destination) && !upload)
|
|
248
274
|
throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
|
|
249
|
-
}
|
|
250
275
|
return this.signProviderRoute({
|
|
251
276
|
chunk,
|
|
252
277
|
target,
|
|
@@ -262,12 +287,14 @@ export class BeamClient {
|
|
|
262
287
|
});
|
|
263
288
|
})();
|
|
264
289
|
pendingRoutes.add(pendingRoute);
|
|
265
|
-
if (pendingRoutes.size >=
|
|
290
|
+
if (pendingRoutes.size >= signingConcurrency)
|
|
266
291
|
await streamNextCompletedRoute();
|
|
267
292
|
}
|
|
268
293
|
}
|
|
269
294
|
while (pendingRoutes.size)
|
|
270
295
|
await streamNextCompletedRoute();
|
|
296
|
+
const multipartGroupManifest = await multipartManifestTask;
|
|
297
|
+
validateMultipartGroupManifest(multipartGroupManifest, prepared.transfer_id);
|
|
271
298
|
const attached = await routeStream.complete();
|
|
272
299
|
if (!attached.success) {
|
|
273
300
|
throw new Error(attached.error ?? attached.message ?? "route stream failed");
|
|
@@ -277,6 +304,8 @@ export class BeamClient {
|
|
|
277
304
|
if (routeStream)
|
|
278
305
|
await routeStream.abort();
|
|
279
306
|
await Promise.allSettled(pendingRoutes);
|
|
307
|
+
if (multipartManifestTask)
|
|
308
|
+
await Promise.allSettled([multipartManifestTask]);
|
|
280
309
|
await cancelAndAbortProviderFailure({
|
|
281
310
|
client: this,
|
|
282
311
|
transferId: prepared.transfer_id,
|
|
@@ -423,16 +452,14 @@ export class BeamClient {
|
|
|
423
452
|
}
|
|
424
453
|
}
|
|
425
454
|
}
|
|
426
|
-
const
|
|
427
|
-
const ROUTE_STREAM_STEADY_BATCH_ROUTES = 256;
|
|
455
|
+
const ROUTE_STREAM_BATCH_ROUTES = 2_048;
|
|
428
456
|
class RouteStreamSender {
|
|
429
457
|
control;
|
|
430
458
|
options;
|
|
431
459
|
streamId;
|
|
432
460
|
checksum = new RouteKeysChecksum();
|
|
433
461
|
batch = [];
|
|
434
|
-
|
|
435
|
-
nextDeliveryIndex = 0;
|
|
462
|
+
seenDeliveryIndices = new Set();
|
|
436
463
|
batchIndex = 0;
|
|
437
464
|
routeCount = 0;
|
|
438
465
|
sendTail = Promise.resolve();
|
|
@@ -449,7 +476,6 @@ class RouteStreamSender {
|
|
|
449
476
|
total_routes: this.options.totalRoutes,
|
|
450
477
|
total_chunks: this.options.totalChunks,
|
|
451
478
|
route_contract_version: "signed_url_v2",
|
|
452
|
-
multipart_group_manifest: this.options.multipartGroupManifest,
|
|
453
479
|
urls_expires_at: this.options.urlsExpiresAt,
|
|
454
480
|
signed_url_flow: "signed_url_v2",
|
|
455
481
|
auto_distribute: this.options.autoDistribute
|
|
@@ -458,7 +484,23 @@ class RouteStreamSender {
|
|
|
458
484
|
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:begin`
|
|
459
485
|
});
|
|
460
486
|
}
|
|
461
|
-
|
|
487
|
+
async addManifestGroups(groups) {
|
|
488
|
+
if (!groups.length)
|
|
489
|
+
return;
|
|
490
|
+
await Promise.all(groups.map(async (group) => {
|
|
491
|
+
const identity = await stableManifestBatchIdentity([group]);
|
|
492
|
+
await this.control.request("transfer.route_stream.manifest", {
|
|
493
|
+
transfer_id: this.options.transferId,
|
|
494
|
+
stream_id: this.streamId,
|
|
495
|
+
manifest_batch_id: identity,
|
|
496
|
+
groups: [group]
|
|
497
|
+
}, {
|
|
498
|
+
transferId: this.options.transferId,
|
|
499
|
+
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:manifest:${identity}`
|
|
500
|
+
});
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
async addRoute(route) {
|
|
462
504
|
const deliveryIndex = signedRouteDeliveryIndex(route);
|
|
463
505
|
if (deliveryIndex === undefined) {
|
|
464
506
|
throw new Error("route delivery_index is required");
|
|
@@ -466,31 +508,22 @@ class RouteStreamSender {
|
|
|
466
508
|
if (!Number.isSafeInteger(deliveryIndex) || deliveryIndex < 0 || deliveryIndex >= this.options.totalRoutes) {
|
|
467
509
|
throw new Error(`route delivery_index ${deliveryIndex} is outside the declared route stream`);
|
|
468
510
|
}
|
|
469
|
-
if (
|
|
511
|
+
if (this.seenDeliveryIndices.has(deliveryIndex)) {
|
|
470
512
|
throw new Error(`duplicate route delivery_index ${deliveryIndex}`);
|
|
471
513
|
}
|
|
472
|
-
this.
|
|
514
|
+
this.seenDeliveryIndices.add(deliveryIndex);
|
|
473
515
|
this.routeCount += 1;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
this.
|
|
477
|
-
this.batch.push({ ...next, delivery_index: this.nextDeliveryIndex });
|
|
478
|
-
this.nextDeliveryIndex += 1;
|
|
479
|
-
const target = this.batchIndex === 0
|
|
480
|
-
? ROUTE_STREAM_BOOTSTRAP_BATCH_ROUTES
|
|
481
|
-
: ROUTE_STREAM_STEADY_BATCH_ROUTES;
|
|
482
|
-
if (this.batch.length >= target) {
|
|
483
|
-
this.enqueueFlush();
|
|
484
|
-
}
|
|
516
|
+
this.batch.push({ ...route, delivery_index: deliveryIndex });
|
|
517
|
+
if (this.batch.length >= ROUTE_STREAM_BATCH_ROUTES) {
|
|
518
|
+
await this.enqueueFlush();
|
|
485
519
|
}
|
|
486
520
|
}
|
|
487
521
|
async complete() {
|
|
488
522
|
if (this.routeCount !== this.options.totalRoutes ||
|
|
489
|
-
this.
|
|
490
|
-
this.
|
|
491
|
-
throw new Error(`route stream has non-contiguous delivery indices: received ${this.routeCount} of ${this.options.totalRoutes} routes`);
|
|
523
|
+
this.seenDeliveryIndices.size !== this.options.totalRoutes) {
|
|
524
|
+
throw new Error(`route stream has incomplete or duplicate delivery indices: received ${this.routeCount} of ${this.options.totalRoutes} routes`);
|
|
492
525
|
}
|
|
493
|
-
this.enqueueFlush();
|
|
526
|
+
await this.enqueueFlush();
|
|
494
527
|
await this.sendTail;
|
|
495
528
|
if (this.sendError)
|
|
496
529
|
throw this.sendError;
|
|
@@ -505,9 +538,12 @@ class RouteStreamSender {
|
|
|
505
538
|
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:complete`
|
|
506
539
|
});
|
|
507
540
|
}
|
|
508
|
-
enqueueFlush() {
|
|
541
|
+
async enqueueFlush() {
|
|
509
542
|
if (!this.batch.length)
|
|
510
543
|
return;
|
|
544
|
+
await this.sendTail;
|
|
545
|
+
if (this.sendError)
|
|
546
|
+
throw this.sendError;
|
|
511
547
|
const routes = this.batch.splice(0, this.batch.length);
|
|
512
548
|
const chunks = this.control.splitRoutesForPayload("transfer.route_stream.batch", {
|
|
513
549
|
transfer_id: this.options.transferId,
|
|
@@ -604,6 +640,12 @@ async function stableRouteStreamId(input) {
|
|
|
604
640
|
const value = hex(bytes);
|
|
605
641
|
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
606
642
|
}
|
|
643
|
+
async function stableManifestBatchIdentity(groups) {
|
|
644
|
+
const identities = groups
|
|
645
|
+
.map((group) => `${group.multipart_group_id}:${group.source_id}:${group.destination_id}`)
|
|
646
|
+
.sort();
|
|
647
|
+
return hex((await sha256Bytes(identities.join("\n"))).slice(0, 16));
|
|
648
|
+
}
|
|
607
649
|
function countDistinctRouteChunks(routes) {
|
|
608
650
|
return new Set(routes.map((route) => `${route.source_id}:${route.chunk_index}`)).size;
|
|
609
651
|
}
|
|
@@ -704,7 +746,7 @@ async function cancelTransferAfterProviderFailure(client, transferId, cause) {
|
|
|
704
746
|
}
|
|
705
747
|
}
|
|
706
748
|
catch (cancelError) {
|
|
707
|
-
throw new Error(`provider transfer failed (${
|
|
749
|
+
throw new Error(`provider transfer failed (${safeErrorCode(cause)}) and transfer cancellation failed (${safeErrorCode(cancelError)})`);
|
|
708
750
|
}
|
|
709
751
|
}
|
|
710
752
|
async function cancelAndAbortProviderFailure(input) {
|
|
@@ -746,12 +788,46 @@ async function cancelAndAbortProviderFailure(input) {
|
|
|
746
788
|
throw new AggregateError([input.cause, cleanupError], `provider transfer failed and multipart cleanup failed for ${input.transferId}`);
|
|
747
789
|
}
|
|
748
790
|
}
|
|
749
|
-
function
|
|
750
|
-
|
|
791
|
+
function safeErrorCode(error) {
|
|
792
|
+
if (!(error instanceof Error))
|
|
793
|
+
return "UnknownError";
|
|
794
|
+
const record = error;
|
|
795
|
+
const code = typeof record.code === "string" && /^[A-Za-z0-9_.-]{1,64}$/.test(record.code)
|
|
796
|
+
? record.code
|
|
797
|
+
: error.name;
|
|
798
|
+
const status = [record.status, record.statusCode]
|
|
799
|
+
.find((value) => Number.isInteger(value) && Number(value) >= 100 && Number(value) <= 599);
|
|
800
|
+
return status === undefined ? code : `${code}:status=${status}`;
|
|
801
|
+
}
|
|
802
|
+
function createDeferred() {
|
|
803
|
+
let resolve;
|
|
804
|
+
let reject;
|
|
805
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
806
|
+
resolve = resolvePromise;
|
|
807
|
+
reject = rejectPromise;
|
|
808
|
+
});
|
|
809
|
+
return { promise, resolve, reject };
|
|
810
|
+
}
|
|
811
|
+
function createMultipartGroupWaiters(prepared, destinationsById) {
|
|
812
|
+
const waiters = new Map();
|
|
813
|
+
for (const source of prepared.plan_descriptor.sources) {
|
|
814
|
+
for (const destination of prepared.plan_descriptor.destinations) {
|
|
815
|
+
const destinationConfig = destinationsById.get(destination.destination_id);
|
|
816
|
+
if (!destinationConfig || isHippiusDestination(destinationConfig))
|
|
817
|
+
continue;
|
|
818
|
+
const finalObjectKey = destination.final_object_keys[source.source_id];
|
|
819
|
+
if (!finalObjectKey)
|
|
820
|
+
throw new Error(`plan final object key not found: ${source.source_id}:${destination.destination_id}`);
|
|
821
|
+
const groupId = multipartGroupStateKey(prepared.transfer_id, destination.destination_id, source.source_id, finalObjectKey);
|
|
822
|
+
waiters.set(groupId, createDeferred());
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return waiters;
|
|
751
826
|
}
|
|
752
827
|
async function createMultipartGroupManifest(input) {
|
|
753
828
|
const groups = input.prepared.plan_descriptor.sources.flatMap((source) => input.prepared.plan_descriptor.destinations.map((destination) => ({ source, destination })));
|
|
754
829
|
const results = await mapOrderedWithConcurrency(groups, input.concurrency, async ({ source, destination }) => {
|
|
830
|
+
let multipartGroupId = null;
|
|
755
831
|
try {
|
|
756
832
|
const destinationConfig = input.destinationsById.get(destination.destination_id);
|
|
757
833
|
if (!destinationConfig) {
|
|
@@ -764,7 +840,7 @@ async function createMultipartGroupManifest(input) {
|
|
|
764
840
|
if (!finalObjectKey) {
|
|
765
841
|
throw new Error(`plan final object key not found: ${source.source_id}:${destination.destination_id}`);
|
|
766
842
|
}
|
|
767
|
-
|
|
843
|
+
multipartGroupId = multipartGroupStateKey(input.prepared.transfer_id, destination.destination_id, source.source_id, finalObjectKey);
|
|
768
844
|
const finalObjectMetadata = {
|
|
769
845
|
"beam-transfer-id": input.prepared.transfer_id,
|
|
770
846
|
"beam-multipart-group-id": multipartGroupId
|
|
@@ -825,6 +901,7 @@ async function createMultipartGroupManifest(input) {
|
|
|
825
901
|
manifest
|
|
826
902
|
};
|
|
827
903
|
input.multipartUploads.set(multipartGroupId, state);
|
|
904
|
+
await input.onGroupReady(state);
|
|
828
905
|
return { ok: true, state };
|
|
829
906
|
}
|
|
830
907
|
catch (error) {
|
|
@@ -838,6 +915,8 @@ async function createMultipartGroupManifest(input) {
|
|
|
838
915
|
}
|
|
839
916
|
}
|
|
840
917
|
catch (error) {
|
|
918
|
+
if (multipartGroupId)
|
|
919
|
+
input.onGroupFailed(multipartGroupId, error);
|
|
841
920
|
return { ok: false, error };
|
|
842
921
|
}
|
|
843
922
|
});
|
package/dist/models.d.ts
CHANGED
|
@@ -122,22 +122,30 @@ export interface TransferStatusInfo {
|
|
|
122
122
|
started_at: string | null;
|
|
123
123
|
completed_at: string | null;
|
|
124
124
|
runtime?: boolean;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
source_bytes_total: number;
|
|
126
|
+
delivery_bytes_total: number;
|
|
127
|
+
delivery_bytes_completed: number;
|
|
128
|
+
delivery_tasks_total: number;
|
|
129
|
+
delivery_tasks_completed: number;
|
|
130
|
+
destinations_total: number;
|
|
131
|
+
destinations_completed: number;
|
|
132
|
+
destination_progress: Array<{
|
|
133
|
+
destination_id: string;
|
|
134
|
+
delivery_bytes_total: number;
|
|
135
|
+
delivery_bytes_completed: number;
|
|
136
|
+
completion_verified: boolean;
|
|
137
|
+
}>;
|
|
131
138
|
destination_groups?: {
|
|
132
139
|
totalGroups: number;
|
|
133
140
|
completedGroups: number;
|
|
134
141
|
pendingGroups: number;
|
|
135
142
|
totalDestinations: number;
|
|
136
143
|
completedDestinations: number;
|
|
144
|
+
completedDestinationIds: string[];
|
|
137
145
|
};
|
|
138
146
|
}
|
|
139
147
|
export interface TransferTerminalEvent {
|
|
140
|
-
schema_version: "transfer-client-control/
|
|
148
|
+
schema_version: "transfer-client-control/v4";
|
|
141
149
|
producer: "transfer-runtime";
|
|
142
150
|
transfer_id: string;
|
|
143
151
|
status: "completed" | "failed" | "cancelled";
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { MultipartGroupManifest, SignedChunkRoute, TransferTerminalSignalWaiter } from "./models.js";
|
|
2
|
-
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/
|
|
2
|
+
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v4";
|
|
3
3
|
export declare const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
|
|
4
4
|
export declare const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
|
|
5
5
|
export declare const BEAM_DEFAULT_MAX_PAYLOAD_BYTES: number;
|
|
6
|
-
export type TransferClientMessageType = "transfer.plan" | "transfer.create" | "transfer.prepare" | "transfer.route_stream.begin" | "transfer.route_stream.batch" | "transfer.route_stream.complete" | "transfer.distribute" | "transfer.status" | "transfer.cancel";
|
|
6
|
+
export type TransferClientMessageType = "transfer.plan" | "transfer.create" | "transfer.prepare" | "transfer.route_stream.begin" | "transfer.route_stream.manifest" | "transfer.route_stream.batch" | "transfer.route_stream.complete" | "transfer.distribute" | "transfer.status" | "transfer.cancel";
|
|
7
7
|
export interface CompactSignedRouteBatch {
|
|
8
8
|
source_chunks: Array<{
|
|
9
9
|
source_ref: number;
|
|
@@ -32,11 +32,16 @@ export interface RouteStreamBeginPayload {
|
|
|
32
32
|
total_routes: number;
|
|
33
33
|
total_chunks: number;
|
|
34
34
|
route_contract_version: "signed_url_v2";
|
|
35
|
-
multipart_group_manifest: MultipartGroupManifest[];
|
|
36
35
|
urls_expires_at?: string;
|
|
37
36
|
signed_url_flow: "signed_url_v2";
|
|
38
37
|
auto_distribute: boolean;
|
|
39
38
|
}
|
|
39
|
+
export interface RouteStreamManifestPayload {
|
|
40
|
+
transfer_id: string;
|
|
41
|
+
stream_id: string;
|
|
42
|
+
manifest_batch_id: string;
|
|
43
|
+
groups: MultipartGroupManifest[];
|
|
44
|
+
}
|
|
40
45
|
export interface TransferControlOptions {
|
|
41
46
|
apiKey: string;
|
|
42
47
|
natsUrl?: string;
|
package/dist/nats-control.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { encode, decode } from "@msgpack/msgpack";
|
|
2
2
|
import { connect } from "nats";
|
|
3
|
-
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/
|
|
3
|
+
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v4";
|
|
4
4
|
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
|
|
@@ -214,7 +214,7 @@ export class BeamTransferControl {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
splitRoutesForPayload(messageType, basePayload, routes) {
|
|
217
|
-
const
|
|
217
|
+
const encodedBytes = (candidate) => {
|
|
218
218
|
const encoded = encode({
|
|
219
219
|
schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
220
220
|
environment: this.environment,
|
|
@@ -227,15 +227,31 @@ export class BeamTransferControl {
|
|
|
227
227
|
producer: "sdk",
|
|
228
228
|
payload: { ...basePayload, route_batch: compactSignedRoutes(candidate) }
|
|
229
229
|
});
|
|
230
|
-
|
|
231
|
-
return [candidate];
|
|
232
|
-
if (candidate.length === 1) {
|
|
233
|
-
throw new Error(`single signed route is ${encoded.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
234
|
-
}
|
|
235
|
-
const midpoint = Math.ceil(candidate.length / 2);
|
|
236
|
-
return [...split(candidate.slice(0, midpoint)), ...split(candidate.slice(midpoint))];
|
|
230
|
+
return encoded.byteLength;
|
|
237
231
|
};
|
|
238
|
-
|
|
232
|
+
const chunks = [];
|
|
233
|
+
let offset = 0;
|
|
234
|
+
while (offset < routes.length) {
|
|
235
|
+
let low = 1;
|
|
236
|
+
let high = routes.length - offset;
|
|
237
|
+
let accepted = 0;
|
|
238
|
+
while (low <= high) {
|
|
239
|
+
const count = Math.floor((low + high) / 2);
|
|
240
|
+
if (encodedBytes(routes.slice(offset, offset + count)) <= this.maxPayloadBytes) {
|
|
241
|
+
accepted = count;
|
|
242
|
+
low = count + 1;
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
high = count - 1;
|
|
246
|
+
}
|
|
247
|
+
if (accepted === 0) {
|
|
248
|
+
const bytes = encodedBytes(routes.slice(offset, offset + 1));
|
|
249
|
+
throw new Error(`single signed route is ${bytes} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
250
|
+
}
|
|
251
|
+
chunks.push(routes.slice(offset, offset + accepted));
|
|
252
|
+
offset += accepted;
|
|
253
|
+
}
|
|
254
|
+
return chunks;
|
|
239
255
|
}
|
|
240
256
|
async connection() {
|
|
241
257
|
if (this.closed)
|
package/dist/provider-signing.js
CHANGED
|
@@ -384,7 +384,8 @@ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
|
|
|
384
384
|
bucket: source.bucket,
|
|
385
385
|
key: source.key,
|
|
386
386
|
region: s3CompatibleRegion(source),
|
|
387
|
-
endpoint_url: endpoint
|
|
387
|
+
endpoint_url: endpoint,
|
|
388
|
+
account_id: "account_id" in source ? source.account_id : undefined
|
|
388
389
|
});
|
|
389
390
|
}
|
|
390
391
|
function isHippiusProvider(config) {
|
|
@@ -397,7 +398,7 @@ async function hippiusPresign(fetchImpl, baseUrl, token, bucket, key, action, ex
|
|
|
397
398
|
url.searchParams.set("expires_in", String(expiresIn));
|
|
398
399
|
const response = await fetchImpl(url, { headers: { Authorization: `Token ${token}` } });
|
|
399
400
|
if (!response.ok) {
|
|
400
|
-
throw new Error(`Hippius presigned URL failed
|
|
401
|
+
throw new Error(`Hippius presigned URL failed status=${response.status}`);
|
|
401
402
|
}
|
|
402
403
|
return String((await response.json()).url);
|
|
403
404
|
}
|
|
@@ -407,12 +408,12 @@ async function hippiusObjectSize(fetchImpl, baseUrl, token, bucket, key) {
|
|
|
407
408
|
url.searchParams.set("max_keys", "1");
|
|
408
409
|
const response = await fetchImpl(url, { headers: { Authorization: `Token ${token}` } });
|
|
409
410
|
if (!response.ok) {
|
|
410
|
-
throw new Error(`Hippius object lookup failed
|
|
411
|
+
throw new Error(`Hippius object lookup failed status=${response.status}`);
|
|
411
412
|
}
|
|
412
413
|
const payload = await response.json();
|
|
413
414
|
const contents = payload.Contents ?? payload.contents ?? [];
|
|
414
415
|
if (!contents.length) {
|
|
415
|
-
throw new Error(
|
|
416
|
+
throw new Error("Hippius object not found");
|
|
416
417
|
}
|
|
417
418
|
return Number(contents[0].Size ?? contents[0].size);
|
|
418
419
|
}
|