@beam-network/sdk 0.2.21 → 0.2.22
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 +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +163 -78
- package/dist/models.d.ts +29 -2
- package/dist/nats-control.d.ts +31 -3
- package/dist/nats-control.js +113 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,6 @@ For low-level raw transfer configs, use `createRawTransfer`; lifecycle transport
|
|
|
95
95
|
|
|
96
96
|
## Route Streaming And Payload Size
|
|
97
97
|
|
|
98
|
-
Provider transfers
|
|
98
|
+
Provider transfers materialize the compact Runtime plan lazily, sign up to 64 routes concurrently by default, and stream a 256-route bootstrap batch followed by 2,048-route steady batches. Set `routeSigningConcurrency` on `BeamClientOptions` to tune the signing bound. Completed batches are sent FIFO while signing continues. Shared route fields use compact dictionaries, and encoded MessagePack requests split under `maxPayloadBytes`, which defaults to 24 MiB.
|
|
99
99
|
|
|
100
100
|
NATS requires this guard because the broker rejects messages above its configured `max_payload`. This limit applies only to lifecycle/control metadata; transfer file bytes do not flow through NATS.
|
package/dist/client.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class BeamClient {
|
|
|
11
11
|
readonly natsUrl: string;
|
|
12
12
|
private readonly fetchImpl;
|
|
13
13
|
private readonly control;
|
|
14
|
+
private readonly routeSigningConcurrency;
|
|
14
15
|
private readonly recoverySigners;
|
|
15
16
|
constructor(options?: BeamClientOptions);
|
|
16
17
|
close(): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { abortMultipartUpload, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, listPartsPageMarker, listPartsVerifyMarker, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signListMultipartUpload, signListObjects, signUploadPartCopy } from "./provider-signing.js";
|
|
2
|
-
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL } from "./nats-control.js";
|
|
2
|
+
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes } from "./nats-control.js";
|
|
3
3
|
import { directMultipartInitialPartNumber } from "./multipart-limits.js";
|
|
4
4
|
export { BEAM_DEFAULT_NATS_URL };
|
|
5
5
|
export class BeamApiError extends Error {
|
|
@@ -17,6 +17,7 @@ export class BeamClient {
|
|
|
17
17
|
natsUrl;
|
|
18
18
|
fetchImpl;
|
|
19
19
|
control;
|
|
20
|
+
routeSigningConcurrency;
|
|
20
21
|
recoverySigners = new Map();
|
|
21
22
|
constructor(options = {}) {
|
|
22
23
|
if (!options.apiKey?.trim()) {
|
|
@@ -24,6 +25,7 @@ export class BeamClient {
|
|
|
24
25
|
}
|
|
25
26
|
this.apiKey = options.apiKey;
|
|
26
27
|
this.natsUrl = options.natsUrl ?? options.natsWsUrl ?? BEAM_DEFAULT_NATS_URL;
|
|
28
|
+
this.routeSigningConcurrency = positiveInteger(options.routeSigningConcurrency ?? 64, "routeSigningConcurrency");
|
|
27
29
|
this.control = new BeamTransferControl({
|
|
28
30
|
apiKey: this.apiKey,
|
|
29
31
|
natsUrl: options.natsUrl,
|
|
@@ -157,12 +159,13 @@ export class BeamClient {
|
|
|
157
159
|
const signedUrlFlow = prepared.signed_url_flow ?? requestedSignedUrlFlow;
|
|
158
160
|
const routeStream = new RouteStreamSender(this.control, {
|
|
159
161
|
transferId: prepared.transfer_id,
|
|
160
|
-
totalRoutes:
|
|
161
|
-
totalChunks: prepared.
|
|
162
|
+
totalRoutes: prepared.plan_descriptor.delivery_route_count,
|
|
163
|
+
totalChunks: prepared.plan_descriptor.logical_chunk_count,
|
|
162
164
|
signedUrlFlow,
|
|
163
165
|
autoDistribute: input.distribute !== false
|
|
164
166
|
});
|
|
165
167
|
const multipartUploads = new Map();
|
|
168
|
+
const multipartUploadPromises = new Map();
|
|
166
169
|
await this.startProviderRouteRecoverySigner({
|
|
167
170
|
prepared,
|
|
168
171
|
sourcesById,
|
|
@@ -175,71 +178,83 @@ export class BeamClient {
|
|
|
175
178
|
try {
|
|
176
179
|
await routeStream.begin();
|
|
177
180
|
routeStreamStarted = true;
|
|
178
|
-
|
|
181
|
+
const pendingRoutes = [];
|
|
182
|
+
const flushOldestRoute = async () => {
|
|
183
|
+
const pending = pendingRoutes.shift();
|
|
184
|
+
if (pending)
|
|
185
|
+
routeStream.addRoute(await pending);
|
|
186
|
+
};
|
|
187
|
+
for (const chunk of materializePlanChunks(prepared.plan_descriptor)) {
|
|
179
188
|
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
180
189
|
for (const target of chunk.destinations) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
190
|
+
pendingRoutes.push((async () => {
|
|
191
|
+
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
192
|
+
const destination = destinationsById.get(target.destination_id);
|
|
193
|
+
if (!destination)
|
|
194
|
+
throw new Error(`BeamCore returned unknown destination_id: ${target.destination_id}`);
|
|
195
|
+
const source = sourcesById.get(chunk.source_id);
|
|
196
|
+
if (!source)
|
|
197
|
+
throw new Error(`BeamCore returned unknown source_id: ${chunk.source_id}`);
|
|
198
|
+
const metadata = target.metadata ?? {};
|
|
199
|
+
const finalObjectKey = typeof metadata.final_object_key === "string" ? metadata.final_object_key : target.object_key;
|
|
200
|
+
if (!finalObjectKey)
|
|
201
|
+
throw new Error("destination signing target is missing object_key");
|
|
202
|
+
const multipartKey = `${prepared.transfer_id}:${target.destination_id}:${chunk.source_id}:${finalObjectKey}`;
|
|
203
|
+
let uploadPromise = multipartUploadPromises.get(multipartKey);
|
|
204
|
+
if (!uploadPromise) {
|
|
205
|
+
uploadPromise = (async () => {
|
|
206
|
+
const multipartCreatedAt = nowIso();
|
|
207
|
+
const upload = isHippiusDestination(destination)
|
|
208
|
+
? {
|
|
209
|
+
destination,
|
|
210
|
+
objectKey: finalObjectKey,
|
|
211
|
+
uploadId: "",
|
|
212
|
+
completeUrl: "",
|
|
213
|
+
abortUrl: "",
|
|
214
|
+
listUrl: "",
|
|
215
|
+
multipartGroupId: multipartKey,
|
|
216
|
+
multipartCreatedAt
|
|
217
|
+
}
|
|
218
|
+
: await (async () => {
|
|
219
|
+
const uploadId = await createMultipartUpload(destination, finalObjectKey);
|
|
220
|
+
return {
|
|
221
|
+
destination,
|
|
222
|
+
objectKey: finalObjectKey,
|
|
223
|
+
uploadId,
|
|
224
|
+
completeUrl: await signCompleteMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
225
|
+
abortUrl: await signAbortMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
226
|
+
listUrl: await signListMultipartUpload(destination, finalObjectKey, uploadId, expiresIn),
|
|
227
|
+
multipartGroupId: multipartKey,
|
|
228
|
+
multipartCreatedAt
|
|
229
|
+
};
|
|
230
|
+
})();
|
|
231
|
+
multipartUploads.set(multipartKey, upload);
|
|
232
|
+
return upload;
|
|
233
|
+
})();
|
|
234
|
+
multipartUploadPromises.set(multipartKey, uploadPromise);
|
|
224
235
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}));
|
|
236
|
+
const upload = await uploadPromise;
|
|
237
|
+
return this.signProviderRoute({
|
|
238
|
+
chunk,
|
|
239
|
+
target,
|
|
240
|
+
source,
|
|
241
|
+
destination,
|
|
242
|
+
expiresIn,
|
|
243
|
+
upload,
|
|
244
|
+
transferId: prepared.transfer_id,
|
|
245
|
+
finalObjectKey,
|
|
246
|
+
partNumber: typeof metadata.part_number === "number"
|
|
247
|
+
? metadata.part_number
|
|
248
|
+
: directMultipartInitialPartNumber(chunk.chunk_index),
|
|
249
|
+
signedUrlFlow
|
|
250
|
+
});
|
|
251
|
+
})());
|
|
252
|
+
if (pendingRoutes.length >= this.routeSigningConcurrency)
|
|
253
|
+
await flushOldestRoute();
|
|
241
254
|
}
|
|
242
255
|
}
|
|
256
|
+
while (pendingRoutes.length)
|
|
257
|
+
await flushOldestRoute();
|
|
243
258
|
const attached = await routeStream.complete();
|
|
244
259
|
if (!attached.success) {
|
|
245
260
|
throw new Error(attached.error ?? attached.message ?? "route stream failed");
|
|
@@ -339,9 +354,8 @@ export class BeamClient {
|
|
|
339
354
|
async startProviderRouteRecoverySigner(input) {
|
|
340
355
|
this.stopRecoverySigner(input.prepared.transfer_id);
|
|
341
356
|
const stop = await this.control.serveRouteRecoverySigner(input.prepared.transfer_id, async (payload) => {
|
|
342
|
-
const routes =
|
|
343
|
-
|
|
344
|
-
const chunk = input.prepared.chunk_plan.find((candidate) => candidate.chunk_index === requested.chunk_index && candidate.source_id === requested.source_id);
|
|
357
|
+
const routes = await mapOrderedWithConcurrency(payload.chunks, this.routeSigningConcurrency, async (requested) => {
|
|
358
|
+
const chunk = materializePlanChunk(input.prepared.plan_descriptor, requested.source_id, requested.chunk_index);
|
|
345
359
|
if (!chunk) {
|
|
346
360
|
throw new Error(`recovery signing chunk not found: ${requested.source_id}:${requested.chunk_index}`);
|
|
347
361
|
}
|
|
@@ -372,7 +386,7 @@ export class BeamClient {
|
|
|
372
386
|
attempt_slot: requested.attempt_slot,
|
|
373
387
|
route_generation_id: requested.route_generation_id
|
|
374
388
|
};
|
|
375
|
-
|
|
389
|
+
return this.signProviderRoute({
|
|
376
390
|
chunk,
|
|
377
391
|
target: {
|
|
378
392
|
...target,
|
|
@@ -387,13 +401,13 @@ export class BeamClient {
|
|
|
387
401
|
finalObjectKey: requested.final_object_key,
|
|
388
402
|
partNumber: requested.part_number,
|
|
389
403
|
signedUrlFlow: input.signedUrlFlow
|
|
390
|
-
})
|
|
391
|
-
}
|
|
404
|
+
});
|
|
405
|
+
});
|
|
392
406
|
return {
|
|
393
407
|
transfer_id: payload.transfer_id,
|
|
394
408
|
route_generation_id: payload.route_generation_id,
|
|
395
409
|
signed_at: nowIso(),
|
|
396
|
-
|
|
410
|
+
route_batch: compactSignedRoutes(routes)
|
|
397
411
|
};
|
|
398
412
|
});
|
|
399
413
|
this.recoverySigners.set(input.prepared.transfer_id, stop);
|
|
@@ -442,9 +456,8 @@ export class BeamClient {
|
|
|
442
456
|
}
|
|
443
457
|
}
|
|
444
458
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const ROUTE_STREAM_TARGET_BATCH_ROUTES = 128;
|
|
459
|
+
const ROUTE_STREAM_BOOTSTRAP_BATCH_ROUTES = 256;
|
|
460
|
+
const ROUTE_STREAM_STEADY_BATCH_ROUTES = 2_048;
|
|
448
461
|
class RouteStreamSender {
|
|
449
462
|
control;
|
|
450
463
|
options;
|
|
@@ -452,6 +465,7 @@ class RouteStreamSender {
|
|
|
452
465
|
checksum = new RouteKeysChecksum();
|
|
453
466
|
batch = [];
|
|
454
467
|
batchIndex = 0;
|
|
468
|
+
routeCount = 0;
|
|
455
469
|
sendTail = Promise.resolve();
|
|
456
470
|
sendError = null;
|
|
457
471
|
constructor(control, options) {
|
|
@@ -470,8 +484,15 @@ class RouteStreamSender {
|
|
|
470
484
|
}), this.options.transferId);
|
|
471
485
|
}
|
|
472
486
|
addRoute(route) {
|
|
473
|
-
|
|
474
|
-
|
|
487
|
+
const normalizedRoute = route.delivery_index === undefined
|
|
488
|
+
? { ...route, delivery_index: this.routeCount }
|
|
489
|
+
: route;
|
|
490
|
+
this.routeCount += 1;
|
|
491
|
+
this.batch.push(normalizedRoute);
|
|
492
|
+
const target = this.batchIndex === 0
|
|
493
|
+
? ROUTE_STREAM_BOOTSTRAP_BATCH_ROUTES
|
|
494
|
+
: ROUTE_STREAM_STEADY_BATCH_ROUTES;
|
|
495
|
+
if (this.batch.length >= target) {
|
|
475
496
|
this.enqueueFlush();
|
|
476
497
|
}
|
|
477
498
|
}
|
|
@@ -513,7 +534,7 @@ class RouteStreamSender {
|
|
|
513
534
|
stream_id: this.streamId,
|
|
514
535
|
batch_id: `${this.streamId}:${batchIndex}`,
|
|
515
536
|
batch_index: batchIndex,
|
|
516
|
-
|
|
537
|
+
route_batch: compactSignedRoutes(chunk),
|
|
517
538
|
route_count: chunk.length,
|
|
518
539
|
route_keys_checksum: chunkChecksum.value()
|
|
519
540
|
}, this.options.transferId);
|
|
@@ -556,8 +577,72 @@ function routeKeyForSignedRoute(route) {
|
|
|
556
577
|
function countDistinctRouteChunks(routes) {
|
|
557
578
|
return new Set(routes.map((route) => `${route.source_id}:${route.chunk_index}`)).size;
|
|
558
579
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
580
|
+
function* materializePlanChunks(descriptor) {
|
|
581
|
+
for (const source of descriptor.sources) {
|
|
582
|
+
for (let sourceChunkIndex = 0; sourceChunkIndex < source.chunk_count; sourceChunkIndex += 1) {
|
|
583
|
+
yield materializePlanChunk(descriptor, source.source_id, source.global_chunk_start + sourceChunkIndex);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async function mapOrderedWithConcurrency(values, concurrency, map) {
|
|
588
|
+
const results = new Array(values.length);
|
|
589
|
+
let nextIndex = 0;
|
|
590
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
591
|
+
while (true) {
|
|
592
|
+
const index = nextIndex;
|
|
593
|
+
nextIndex += 1;
|
|
594
|
+
if (index >= values.length)
|
|
595
|
+
return;
|
|
596
|
+
results[index] = await map(values[index], index);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
await Promise.all(workers);
|
|
600
|
+
return results;
|
|
601
|
+
}
|
|
602
|
+
function positiveInteger(value, label) {
|
|
603
|
+
if (!Number.isInteger(value) || value < 1)
|
|
604
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
function materializePlanChunk(descriptor, sourceId, chunkIndex) {
|
|
608
|
+
const source = descriptor.sources.find((candidate) => candidate.source_id === sourceId);
|
|
609
|
+
if (!source)
|
|
610
|
+
throw new Error(`plan source not found: ${sourceId}`);
|
|
611
|
+
const sourceChunkIndex = chunkIndex - source.global_chunk_start;
|
|
612
|
+
if (sourceChunkIndex < 0 || sourceChunkIndex >= source.chunk_count) {
|
|
613
|
+
throw new Error(`plan chunk is outside source range: ${sourceId}:${chunkIndex}`);
|
|
614
|
+
}
|
|
615
|
+
const sourceOffset = sourceChunkIndex * descriptor.chunk_size;
|
|
616
|
+
const chunkSize = Math.min(descriptor.chunk_size, source.size - sourceOffset);
|
|
617
|
+
return {
|
|
618
|
+
chunk_index: chunkIndex,
|
|
619
|
+
source_id: sourceId,
|
|
620
|
+
source_chunk_index: sourceChunkIndex,
|
|
621
|
+
source_offset: sourceOffset,
|
|
622
|
+
chunk_size: chunkSize,
|
|
623
|
+
source_url: source.url,
|
|
624
|
+
destinations: descriptor.destinations.map((destination) => {
|
|
625
|
+
const finalObjectKey = destination.final_object_keys[sourceId];
|
|
626
|
+
if (!finalObjectKey)
|
|
627
|
+
throw new Error(`plan final object key not found: ${sourceId}:${destination.destination_id}`);
|
|
628
|
+
const deliveryIndex = chunkIndex * descriptor.destinations.length + destination.destination_index;
|
|
629
|
+
return {
|
|
630
|
+
destination_id: destination.destination_id,
|
|
631
|
+
delivery_index: deliveryIndex,
|
|
632
|
+
provider: destination.provider,
|
|
633
|
+
object_key: `${finalObjectKey}/${descriptor.plan_nonce}/chunk-${String(sourceChunkIndex).padStart(6, "0")}`,
|
|
634
|
+
metadata: {
|
|
635
|
+
...(destination.metadata ?? {}),
|
|
636
|
+
final_object_key: finalObjectKey,
|
|
637
|
+
part_number: directMultipartInitialPartNumber(chunkIndex),
|
|
638
|
+
logical_attempt_index: 0,
|
|
639
|
+
attempt_slot: 0,
|
|
640
|
+
route_generation_id: `initial-${chunkIndex}-${destination.destination_id}`,
|
|
641
|
+
delivery_index: deliveryIndex
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
})
|
|
645
|
+
};
|
|
561
646
|
}
|
|
562
647
|
async function sha256Bytes(value) {
|
|
563
648
|
const input = new TextEncoder().encode(value);
|
package/dist/models.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface BeamClientOptions {
|
|
|
8
8
|
transferRuntimeShardCount?: number;
|
|
9
9
|
requestTimeoutMs?: number;
|
|
10
10
|
maxPayloadBytes?: number;
|
|
11
|
+
routeSigningConcurrency?: number;
|
|
11
12
|
fetch?: typeof fetch;
|
|
12
13
|
}
|
|
13
14
|
export interface SourceConfig {
|
|
@@ -240,10 +241,36 @@ export interface ChunkSigningPlanItem {
|
|
|
240
241
|
source_url: string;
|
|
241
242
|
destinations: ChunkDestinationSigningTarget[];
|
|
242
243
|
}
|
|
244
|
+
export interface CompactTransferPlanSource extends PreparedHttpSource {
|
|
245
|
+
global_chunk_start: number;
|
|
246
|
+
chunk_count: number;
|
|
247
|
+
}
|
|
248
|
+
export interface CompactTransferPlanDestination extends PreparedDestination {
|
|
249
|
+
destination_index: number;
|
|
250
|
+
final_object_keys: Record<string, string>;
|
|
251
|
+
}
|
|
252
|
+
export interface CompactTransferPlanDescriptor {
|
|
253
|
+
version: "compact-transfer-plan/v1";
|
|
254
|
+
plan_nonce: string;
|
|
255
|
+
chunk_size: number;
|
|
256
|
+
sources: CompactTransferPlanSource[];
|
|
257
|
+
destinations: CompactTransferPlanDestination[];
|
|
258
|
+
logical_chunk_count: number;
|
|
259
|
+
delivery_route_count: number;
|
|
260
|
+
multipart_attempt_slots: number;
|
|
261
|
+
formulas: {
|
|
262
|
+
source_offset: "source_chunk_index * chunk_size";
|
|
263
|
+
delivery_index: "chunk_index * destination_count + destination_index";
|
|
264
|
+
part_number: "chunk_index * multipart_attempt_slots + attempt_slot + 1";
|
|
265
|
+
route_generation_id: "initial-{chunk_index}-{destination_id}";
|
|
266
|
+
staging_object_key: "{final_object_key}/{plan_nonce}/chunk-{source_chunk_index:6}";
|
|
267
|
+
};
|
|
268
|
+
}
|
|
243
269
|
export interface SignedChunkRoute {
|
|
244
270
|
source_id: string;
|
|
245
271
|
destination_id: string;
|
|
246
272
|
chunk_index: number;
|
|
273
|
+
delivery_index?: number;
|
|
247
274
|
source_url: string;
|
|
248
275
|
dest_url: string;
|
|
249
276
|
source_offset: number;
|
|
@@ -261,7 +288,7 @@ export interface TransferPlanResponse {
|
|
|
261
288
|
total_destinations?: number;
|
|
262
289
|
logical_chunks?: number;
|
|
263
290
|
total_chunks?: number;
|
|
264
|
-
|
|
291
|
+
plan_descriptor: CompactTransferPlanDescriptor;
|
|
265
292
|
signed_url_flow?: SignedUrlFlow;
|
|
266
293
|
error?: string;
|
|
267
294
|
message?: string;
|
|
@@ -277,7 +304,7 @@ export interface TransferPrepareResponse {
|
|
|
277
304
|
total_destinations?: number;
|
|
278
305
|
logical_chunks?: number;
|
|
279
306
|
total_chunks?: number;
|
|
280
|
-
|
|
307
|
+
plan_descriptor: CompactTransferPlanDescriptor;
|
|
281
308
|
signed_url_flow?: SignedUrlFlow;
|
|
282
309
|
error?: string;
|
|
283
310
|
message?: string;
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { SignedChunkRoute } from "./models.js";
|
|
2
|
+
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v2";
|
|
2
3
|
export declare const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
|
|
3
4
|
export declare const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
|
|
4
5
|
export declare const BEAM_DEFAULT_MAX_PAYLOAD_BYTES: number;
|
|
@@ -33,7 +34,33 @@ export interface RouteRecoverySignReplyPayload {
|
|
|
33
34
|
transfer_id: string;
|
|
34
35
|
route_generation_id: string;
|
|
35
36
|
signed_at?: string;
|
|
36
|
-
|
|
37
|
+
route_batch: CompactSignedRouteBatch;
|
|
38
|
+
}
|
|
39
|
+
export interface CompactSignedRouteBatch {
|
|
40
|
+
source_chunks: Array<{
|
|
41
|
+
source_ref: number;
|
|
42
|
+
source_id: string;
|
|
43
|
+
chunk_index: number;
|
|
44
|
+
source_url: string;
|
|
45
|
+
source_offset: number;
|
|
46
|
+
chunk_size: number;
|
|
47
|
+
expires_at?: string;
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
}>;
|
|
50
|
+
multipart_groups: Array<{
|
|
51
|
+
group_ref: number;
|
|
52
|
+
metadata: Record<string, unknown>;
|
|
53
|
+
}>;
|
|
54
|
+
routes: Array<{
|
|
55
|
+
source_ref: number;
|
|
56
|
+
destination_id: string;
|
|
57
|
+
delivery_index: number;
|
|
58
|
+
dest_url: string;
|
|
59
|
+
expires_at?: string;
|
|
60
|
+
dest_headers?: Record<string, string>;
|
|
61
|
+
multipart_group_ref?: number;
|
|
62
|
+
metadata?: Record<string, unknown>;
|
|
63
|
+
}>;
|
|
37
64
|
}
|
|
38
65
|
export interface TransferControlOptions {
|
|
39
66
|
apiKey: string;
|
|
@@ -61,7 +88,7 @@ export declare class BeamTransferControl {
|
|
|
61
88
|
serveRouteRecoverySigner(transferId: string, handler: (payload: RouteRecoverySignRequestPayload) => Promise<RouteRecoverySignReplyPayload>): Promise<() => void>;
|
|
62
89
|
close(): Promise<void>;
|
|
63
90
|
request<T>(messageType: TransferClientMessageType, payload: Record<string, unknown>, transferId?: string): Promise<T>;
|
|
64
|
-
splitRoutesForPayload(messageType: TransferClientMessageType, basePayload: Record<string, unknown>, routes:
|
|
91
|
+
splitRoutesForPayload(messageType: TransferClientMessageType, basePayload: Record<string, unknown>, routes: SignedChunkRoute[]): SignedChunkRoute[][];
|
|
65
92
|
private connection;
|
|
66
93
|
private resolveAuthToken;
|
|
67
94
|
private authSubject;
|
|
@@ -69,4 +96,5 @@ export declare class BeamTransferControl {
|
|
|
69
96
|
private buildRouteRecoveryReply;
|
|
70
97
|
private requestSubject;
|
|
71
98
|
}
|
|
99
|
+
export declare function compactSignedRoutes(routes: SignedChunkRoute[]): CompactSignedRouteBatch;
|
|
72
100
|
export declare function transferShardId(transferId: string, shardCount: number): number;
|
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/v2";
|
|
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
|
|
@@ -134,33 +134,28 @@ export class BeamTransferControl {
|
|
|
134
134
|
return (decoded.payload ?? {});
|
|
135
135
|
}
|
|
136
136
|
splitRoutesForPayload(messageType, basePayload, routes) {
|
|
137
|
-
const
|
|
138
|
-
let current = [];
|
|
139
|
-
for (const route of routes) {
|
|
140
|
-
const candidate = [...current, route];
|
|
137
|
+
const split = (candidate) => {
|
|
141
138
|
const encoded = encode({
|
|
142
139
|
schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
143
140
|
environment: this.environment,
|
|
144
141
|
key_prefix: this.keyPrefix,
|
|
145
142
|
shard_id: 0,
|
|
146
143
|
message_type: messageType,
|
|
147
|
-
request_id:
|
|
144
|
+
request_id: "00000000-0000-4000-8000-000000000000",
|
|
148
145
|
auth_token: "x".repeat(512),
|
|
149
146
|
occurred_at: new Date().toISOString(),
|
|
150
147
|
producer: "sdk",
|
|
151
|
-
payload: { ...basePayload,
|
|
148
|
+
payload: { ...basePayload, route_batch: compactSignedRoutes(candidate) }
|
|
152
149
|
});
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
if (encoded.byteLength <= this.maxPayloadBytes)
|
|
151
|
+
return [candidate];
|
|
152
|
+
if (candidate.length === 1) {
|
|
153
|
+
throw new Error(`single signed route is ${encoded.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
156
154
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (current.length > 0)
|
|
162
|
-
chunks.push(current);
|
|
163
|
-
return chunks;
|
|
155
|
+
const midpoint = Math.ceil(candidate.length / 2);
|
|
156
|
+
return [...split(candidate.slice(0, midpoint)), ...split(candidate.slice(midpoint))];
|
|
157
|
+
};
|
|
158
|
+
return routes.length ? split(routes) : [];
|
|
164
159
|
}
|
|
165
160
|
async connection() {
|
|
166
161
|
if (this.nc && !this.nc.isClosed())
|
|
@@ -214,6 +209,107 @@ export class BeamTransferControl {
|
|
|
214
209
|
return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.shard.${shardId}.${messageType.replace(/\./g, "_")}`;
|
|
215
210
|
}
|
|
216
211
|
}
|
|
212
|
+
const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
|
|
213
|
+
"part_number",
|
|
214
|
+
"logical_attempt_index",
|
|
215
|
+
"attempt_slot",
|
|
216
|
+
"route_generation_id",
|
|
217
|
+
"staging_object_key",
|
|
218
|
+
"copy_url",
|
|
219
|
+
"copy_source",
|
|
220
|
+
"copy_source_range",
|
|
221
|
+
"delete_url",
|
|
222
|
+
"part_verify_url",
|
|
223
|
+
"list_page_url"
|
|
224
|
+
]);
|
|
225
|
+
const ROUTE_IDENTITY_METADATA_KEYS = [
|
|
226
|
+
"source_id",
|
|
227
|
+
"destination_id",
|
|
228
|
+
"chunk_index",
|
|
229
|
+
"route_chunk_index",
|
|
230
|
+
"delivery_index"
|
|
231
|
+
];
|
|
232
|
+
export function compactSignedRoutes(routes) {
|
|
233
|
+
const source_chunks = [];
|
|
234
|
+
const sourceRefs = new Map();
|
|
235
|
+
const multipart_groups = [];
|
|
236
|
+
const groupRefs = new Map();
|
|
237
|
+
const compactRoutes = [];
|
|
238
|
+
for (const [routeIndex, route] of routes.entries()) {
|
|
239
|
+
const sourceKey = JSON.stringify([
|
|
240
|
+
route.source_id,
|
|
241
|
+
route.chunk_index,
|
|
242
|
+
route.source_url,
|
|
243
|
+
route.source_offset,
|
|
244
|
+
route.chunk_size,
|
|
245
|
+
route.expires_at ?? null,
|
|
246
|
+
route.headers ?? null
|
|
247
|
+
]);
|
|
248
|
+
let sourceRef = sourceRefs.get(sourceKey);
|
|
249
|
+
if (sourceRef === undefined) {
|
|
250
|
+
sourceRef = source_chunks.length;
|
|
251
|
+
sourceRefs.set(sourceKey, sourceRef);
|
|
252
|
+
source_chunks.push(compact({
|
|
253
|
+
source_ref: sourceRef,
|
|
254
|
+
source_id: route.source_id,
|
|
255
|
+
chunk_index: route.chunk_index,
|
|
256
|
+
source_url: route.source_url,
|
|
257
|
+
source_offset: route.source_offset,
|
|
258
|
+
chunk_size: route.chunk_size,
|
|
259
|
+
expires_at: route.expires_at,
|
|
260
|
+
headers: route.headers
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
const metadata = { ...(route.metadata ?? {}) };
|
|
264
|
+
const deliveryIndex = route.delivery_index ?? nonNegativeInteger(metadata.delivery_index) ?? routeIndex;
|
|
265
|
+
for (const key of ROUTE_IDENTITY_METADATA_KEYS)
|
|
266
|
+
delete metadata[key];
|
|
267
|
+
const groupBaseKey = multipartGroupKey(route, metadata);
|
|
268
|
+
const groupMetadata = Object.fromEntries(Object.entries(metadata).filter(([key]) => !ROUTE_ATTEMPT_METADATA_KEYS.has(key)));
|
|
269
|
+
const groupKey = groupBaseKey ? `${groupBaseKey}:${JSON.stringify(groupMetadata)}` : null;
|
|
270
|
+
let groupRef;
|
|
271
|
+
if (groupKey) {
|
|
272
|
+
groupRef = groupRefs.get(groupKey);
|
|
273
|
+
if (groupRef === undefined) {
|
|
274
|
+
groupRef = multipart_groups.length;
|
|
275
|
+
groupRefs.set(groupKey, groupRef);
|
|
276
|
+
multipart_groups.push({ group_ref: groupRef, metadata: groupMetadata });
|
|
277
|
+
}
|
|
278
|
+
for (const key of Object.keys(metadata)) {
|
|
279
|
+
if (!ROUTE_ATTEMPT_METADATA_KEYS.has(key))
|
|
280
|
+
delete metadata[key];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
compactRoutes.push(compact({
|
|
284
|
+
source_ref: sourceRef,
|
|
285
|
+
destination_id: route.destination_id,
|
|
286
|
+
delivery_index: deliveryIndex,
|
|
287
|
+
dest_url: route.dest_url,
|
|
288
|
+
expires_at: route.expires_at,
|
|
289
|
+
dest_headers: route.dest_headers,
|
|
290
|
+
multipart_group_ref: groupRef,
|
|
291
|
+
metadata: Object.keys(metadata).length ? metadata : undefined
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
return { source_chunks, multipart_groups, routes: compactRoutes };
|
|
295
|
+
}
|
|
296
|
+
function multipartGroupKey(route, metadata) {
|
|
297
|
+
const groupId = typeof metadata.multipart_group_id === "string" ? metadata.multipart_group_id : "";
|
|
298
|
+
if (groupId)
|
|
299
|
+
return `id:${groupId}`;
|
|
300
|
+
const uploadId = typeof metadata.upload_id === "string" ? metadata.upload_id : "";
|
|
301
|
+
const finalObjectKey = typeof metadata.final_object_key === "string" ? metadata.final_object_key : "";
|
|
302
|
+
return uploadId || finalObjectKey
|
|
303
|
+
? JSON.stringify([route.source_id, route.destination_id, uploadId, finalObjectKey])
|
|
304
|
+
: null;
|
|
305
|
+
}
|
|
306
|
+
function nonNegativeInteger(value) {
|
|
307
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
308
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
309
|
+
}
|
|
310
|
+
function compact(value) {
|
|
311
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
312
|
+
}
|
|
217
313
|
export function transferShardId(transferId, shardCount) {
|
|
218
314
|
let hash = 2166136261;
|
|
219
315
|
for (let i = 0; i < transferId.length; i += 1) {
|