@beam-network/sdk 0.5.13 → 0.5.15
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 +2 -2
- package/dist/client.d.ts +16 -11
- package/dist/client.js +143 -13
- package/dist/models.d.ts +12 -0
- package/dist/nats-control.js +31 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ For low-level raw transfer configs, use `createRawTransfer`; lifecycle transport
|
|
|
98
98
|
|
|
99
99
|
Provider transfers use `transfer-client-control/v6`. 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. Encoded MessagePack requests target 4 MiB physical batches under `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 above the 4 MiB target but within the configured guard publishes alone; 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.
|
|
101
|
+
The client signs up to 64 routes concurrently by default and emits 2,048-route logical batches. Encoded MessagePack requests target 4 MiB physical batches under `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. Lifecycle control refreshes cached SDK auth before the final 30 seconds of token lifetime and retries `auth_token_expired` replies with the same request identity and payload but a fresh token, preserving Core request-conflict protection during large route streams. A single route above the 4 MiB target but within the configured guard publishes alone; 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
102
|
|
|
103
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.
|
|
104
104
|
|
|
@@ -110,4 +110,4 @@ NATS requires this guard because the broker rejects messages above its configure
|
|
|
110
110
|
|
|
111
111
|
## Restart Recovery
|
|
112
112
|
|
|
113
|
-
The client keeps one in-memory recovery lease per active transfer and one `runtime.hello` monitor per active shard. The lease is installed before route-stream begin, and a per-transfer lock coalesces initial streaming with replay. Multipart recovery retains the existing upload IDs and compact group state, then re-signs only the expiring route and commit controls. A Runtime epoch change invalidates cached auth, coalesces one `transfer.resume`, and regenerates routes under a fresh generation; a transport-only epoch change replays only when Runtime reports routes missing or expired. Provider credentials and signing inputs are retained only in memory and released on terminal status or `close()`. Low-level `attachSignedUrls` requires `routeGenerationId`, the plan fingerprint/checksum, and a `recoveryFactory`; signed URLs are never journaled to disk. Foreground cancellation, deadline expiry, and Runtime state-loss responses keep the background lease and retained multipart upload alive.
|
|
113
|
+
The client keeps one in-memory recovery lease per active transfer and one `runtime.hello` monitor per active shard. The lease is installed before route-stream begin, and a per-transfer lock coalesces initial streaming with replay. Multipart recovery retains the existing upload IDs and compact group state, then re-signs only the expiring route and commit controls. Each `resumeProviderTransfer` invocation re-prepares an existing transfer by explicit transfer id with a fresh request and route generation, while transport retries within that invocation reuse the same request. It then reattaches route-recovery signing from the current provider configs without creating duplicate multipart uploads. A Runtime epoch change invalidates cached auth, coalesces one `transfer.resume`, and regenerates routes under a fresh generation; a transport-only epoch change replays only when Runtime reports routes missing or expired. Provider credentials and signing inputs are retained only in memory and released on terminal status or `close()`. Low-level `attachSignedUrls` requires `routeGenerationId`, the plan fingerprint/checksum, and a `recoveryFactory`; signed URLs are never journaled to disk. Foreground cancellation, deadline expiry, and Runtime state-loss responses keep the background lease and retained multipart upload alive.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, MultipartGroupManifest, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, SignedUrlFlow, TransferCancelResponse, TransferCreateResponse, TransferPlanResponse, TransferPrepareResponse, TransferStatusInfo, TransferTerminalSignalWaiter } from "./models.js";
|
|
1
|
+
import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, MultipartGroupManifest, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, ProviderTransferResumeInput, RawTransferCreateInput, SignedChunkRoute, SignedUrlFlow, TransferCancelResponse, TransferCreateResponse, TransferPlanResponse, TransferPrepareResponse, TransferStatusInfo, TransferTerminalSignalWaiter } from "./models.js";
|
|
2
2
|
import { BEAM_DEFAULT_NATS_URL } from "./nats-control.js";
|
|
3
3
|
export declare class BeamRouteRecoveryPendingError extends Error {
|
|
4
4
|
readonly transferId: string;
|
|
@@ -24,6 +24,17 @@ export declare class BeamApiError extends Error {
|
|
|
24
24
|
readonly body: string;
|
|
25
25
|
constructor(message: string, status: number, body: string);
|
|
26
26
|
}
|
|
27
|
+
type TransferPrepareInput = {
|
|
28
|
+
transferId?: string;
|
|
29
|
+
sources: PreparedHttpSource[];
|
|
30
|
+
destinations: PreparedDestination[];
|
|
31
|
+
name?: string;
|
|
32
|
+
testMode?: boolean;
|
|
33
|
+
urlsExpiresAt?: string;
|
|
34
|
+
signedUrlFlow?: SignedUrlFlow;
|
|
35
|
+
idempotencyKey?: string;
|
|
36
|
+
routeGenerationId?: string;
|
|
37
|
+
};
|
|
27
38
|
export declare class BeamClient {
|
|
28
39
|
readonly apiKey: string;
|
|
29
40
|
readonly natsUrl: string;
|
|
@@ -40,6 +51,7 @@ export declare class BeamClient {
|
|
|
40
51
|
openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
|
|
41
52
|
createRawTransfer(input: RawTransferCreateInput): Promise<TransferCreateResponse>;
|
|
42
53
|
createTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
54
|
+
resumeProviderTransfer(input: ProviderTransferResumeInput): Promise<TransferPrepareResponse>;
|
|
43
55
|
transferStatus(transferId: string): Promise<TransferStatusInfo>;
|
|
44
56
|
distributeTransfer(transferId: string): Promise<DistributeResponse>;
|
|
45
57
|
cancelTransfer(transferId: string): Promise<TransferCancelResponse>;
|
|
@@ -51,16 +63,8 @@ export declare class BeamClient {
|
|
|
51
63
|
urlsExpiresAt?: string;
|
|
52
64
|
signedUrlFlow?: SignedUrlFlow;
|
|
53
65
|
}): Promise<TransferPlanResponse>;
|
|
54
|
-
prepareTransfer(input:
|
|
55
|
-
|
|
56
|
-
destinations: PreparedDestination[];
|
|
57
|
-
name?: string;
|
|
58
|
-
testMode?: boolean;
|
|
59
|
-
urlsExpiresAt?: string;
|
|
60
|
-
signedUrlFlow?: SignedUrlFlow;
|
|
61
|
-
idempotencyKey?: string;
|
|
62
|
-
routeGenerationId?: string;
|
|
63
|
-
}): Promise<TransferPrepareResponse>;
|
|
66
|
+
prepareTransfer(input: TransferPrepareInput): Promise<TransferPrepareResponse>;
|
|
67
|
+
private prepareTransferWithRequestKey;
|
|
64
68
|
attachSignedUrls(transferId: string, input: {
|
|
65
69
|
chunkRoutes: SignedChunkRoute[];
|
|
66
70
|
multipartGroupManifest: MultipartGroupManifest[];
|
|
@@ -79,6 +83,7 @@ export declare class BeamClient {
|
|
|
79
83
|
prepareProviderTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
80
84
|
private signProviderRoute;
|
|
81
85
|
private startProviderRouteRecoverySigner;
|
|
86
|
+
private recoveryMultipartUploadState;
|
|
82
87
|
private submitIntegrityAuditGrantsIfPresent;
|
|
83
88
|
private submitProviderIntegrityAuditGrants;
|
|
84
89
|
private stopRecoverySigner;
|
package/dist/client.js
CHANGED
|
@@ -111,6 +111,65 @@ export class BeamClient {
|
|
|
111
111
|
createTransfer(input) {
|
|
112
112
|
return this.prepareProviderTransfer(input);
|
|
113
113
|
}
|
|
114
|
+
async resumeProviderTransfer(input) {
|
|
115
|
+
validateId(input.transferId, "transferId");
|
|
116
|
+
const expiresIn = input.expiresIn ?? 3600;
|
|
117
|
+
const requestedSignedUrlFlow = input.signedUrlFlow ?? "signed_url";
|
|
118
|
+
const retainedRecoveryInput = {
|
|
119
|
+
...input,
|
|
120
|
+
sources: input.sources.map((source) => ({ ...source })),
|
|
121
|
+
destinations: input.destinations.map((destination) => ({ ...destination }))
|
|
122
|
+
};
|
|
123
|
+
const preparedDestinations = retainedRecoveryInput.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
|
|
124
|
+
const destinationsById = new Map();
|
|
125
|
+
preparedDestinations.forEach((preparedDestination, index) => {
|
|
126
|
+
const destination = retainedRecoveryInput.destinations[index];
|
|
127
|
+
if (!destination) {
|
|
128
|
+
throw new Error(`missing provider destination config for prepared destination ${preparedDestination.destination_id}`);
|
|
129
|
+
}
|
|
130
|
+
destinationsById.set(preparedDestination.destination_id, destination);
|
|
131
|
+
});
|
|
132
|
+
const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
|
|
133
|
+
const resumePrepareRequestKey = `transfer:${input.transferId}:prepare:resume:${randomUuid()}`;
|
|
134
|
+
const prepared = await this.prepareTransferWithRequestKey({
|
|
135
|
+
transferId: input.transferId,
|
|
136
|
+
sources: preparedSources,
|
|
137
|
+
destinations: preparedDestinations,
|
|
138
|
+
name: input.name,
|
|
139
|
+
testMode: input.testMode,
|
|
140
|
+
signedUrlFlow: requestedSignedUrlFlow
|
|
141
|
+
}, resumePrepareRequestKey);
|
|
142
|
+
if (!prepared.success)
|
|
143
|
+
return prepared;
|
|
144
|
+
if (prepared.transfer_id !== input.transferId) {
|
|
145
|
+
throw new Error("resumed provider transfer id mismatch");
|
|
146
|
+
}
|
|
147
|
+
const sourcesById = new Map();
|
|
148
|
+
preparedSources.forEach((preparedSource, index) => {
|
|
149
|
+
const source = retainedRecoveryInput.sources[index];
|
|
150
|
+
if (!source) {
|
|
151
|
+
throw new Error(`missing provider source config for prepared source ${preparedSource.source_id}`);
|
|
152
|
+
}
|
|
153
|
+
sourcesById.set(preparedSource.source_id, source);
|
|
154
|
+
});
|
|
155
|
+
await this.startProviderRouteRecoverySigner({
|
|
156
|
+
prepared,
|
|
157
|
+
sourcesById,
|
|
158
|
+
destinationsById,
|
|
159
|
+
multipartUploads: new Map(),
|
|
160
|
+
expiresIn
|
|
161
|
+
});
|
|
162
|
+
this.control.registerRecoveryLease({
|
|
163
|
+
transferId: prepared.transfer_id,
|
|
164
|
+
planFingerprint: prepared.plan_fingerprint,
|
|
165
|
+
coordinateChecksum: prepared.coordinate_checksum,
|
|
166
|
+
replayRoutes: async () => {
|
|
167
|
+
throw new Error("route replay is unavailable after provider action resume; route signing remains available");
|
|
168
|
+
},
|
|
169
|
+
disposeSecrets: () => clearProviderResumeSecrets(retainedRecoveryInput)
|
|
170
|
+
});
|
|
171
|
+
return prepared;
|
|
172
|
+
}
|
|
114
173
|
async transferStatus(transferId) {
|
|
115
174
|
validateId(transferId, "transferId");
|
|
116
175
|
const status = await this.control.request("transfer.status", { transfer_id: transferId }, { transferId });
|
|
@@ -151,9 +210,13 @@ export class BeamClient {
|
|
|
151
210
|
validateCompactTransferPlan(result.plan_descriptor, result.signed_url_flow);
|
|
152
211
|
return result;
|
|
153
212
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
213
|
+
prepareTransfer(input) {
|
|
214
|
+
return this.prepareTransferWithRequestKey(input);
|
|
215
|
+
}
|
|
216
|
+
async prepareTransferWithRequestKey(input, requestKey) {
|
|
217
|
+
const transferId = input.transferId ?? await transferIdForIdempotencyKey(input.idempotencyKey);
|
|
218
|
+
validateId(transferId, "transferId");
|
|
219
|
+
const prepareIdempotencyKey = requestKey ?? `transfer:${transferId}:prepare`;
|
|
157
220
|
const routeGenerationId = input.routeGenerationId ?? await routeGenerationIdForPrepareIdempotencyKey(prepareIdempotencyKey);
|
|
158
221
|
const result = await this.control.request("transfer.prepare", compact({
|
|
159
222
|
transfer_id: transferId,
|
|
@@ -548,15 +611,15 @@ export class BeamClient {
|
|
|
548
611
|
const destination = input.destinationsById.get(requested.destination_id);
|
|
549
612
|
if (!source || !destination)
|
|
550
613
|
throw new Error("route recovery provider configuration is unavailable");
|
|
551
|
-
const upload =
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
614
|
+
const upload = isHippiusDestination(destination)
|
|
615
|
+
? undefined
|
|
616
|
+
: await this.recoveryMultipartUploadState({
|
|
617
|
+
prepared: input.prepared,
|
|
618
|
+
destination,
|
|
619
|
+
requested,
|
|
620
|
+
multipartUploads: input.multipartUploads,
|
|
621
|
+
expiresIn: input.expiresIn
|
|
622
|
+
});
|
|
560
623
|
return this.signProviderRoute({
|
|
561
624
|
chunk,
|
|
562
625
|
target: {
|
|
@@ -579,7 +642,7 @@ export class BeamClient {
|
|
|
579
642
|
source,
|
|
580
643
|
destination,
|
|
581
644
|
expiresIn: input.expiresIn,
|
|
582
|
-
upload,
|
|
645
|
+
...(upload ? { upload } : {}),
|
|
583
646
|
transferId: input.prepared.transfer_id,
|
|
584
647
|
finalObjectKey: requested.final_object_key,
|
|
585
648
|
signedUrlFlow: "signed_url",
|
|
@@ -604,6 +667,63 @@ export class BeamClient {
|
|
|
604
667
|
});
|
|
605
668
|
});
|
|
606
669
|
}
|
|
670
|
+
async recoveryMultipartUploadState(input) {
|
|
671
|
+
const existing = input.multipartUploads.get(input.requested.multipart_group_id);
|
|
672
|
+
if (existing) {
|
|
673
|
+
if (existing.uploadId !== input.requested.upload_id
|
|
674
|
+
|| existing.objectKey !== input.requested.final_object_key
|
|
675
|
+
|| existing.manifest.source_id !== input.requested.source_id
|
|
676
|
+
|| existing.manifest.destination_id !== input.requested.destination_id) {
|
|
677
|
+
throw new Error("route recovery multipart identity mismatch");
|
|
678
|
+
}
|
|
679
|
+
return existing;
|
|
680
|
+
}
|
|
681
|
+
if (!input.requested.upload_id) {
|
|
682
|
+
throw new Error("route recovery multipart upload id is required");
|
|
683
|
+
}
|
|
684
|
+
const source = input.prepared.plan_descriptor.sources.find((candidate) => candidate.source_id === input.requested.source_id);
|
|
685
|
+
if (!source) {
|
|
686
|
+
throw new Error("route recovery source plan is unavailable");
|
|
687
|
+
}
|
|
688
|
+
const expectedGroupId = multipartGroupStateKey(input.prepared.transfer_id, input.requested.destination_id, input.requested.source_id, input.requested.final_object_key);
|
|
689
|
+
if (input.requested.multipart_group_id !== expectedGroupId) {
|
|
690
|
+
throw new Error("route recovery multipart group identity mismatch");
|
|
691
|
+
}
|
|
692
|
+
const finalObjectMetadata = { "beam-transfer-id": input.prepared.transfer_id };
|
|
693
|
+
const maxPartNumber = multipartMaxPartNumber(source.chunk_count);
|
|
694
|
+
const listPageMarkers = multipartListPageMarkers(maxPartNumber);
|
|
695
|
+
const [completeUrl, abortUrl, listPageUrls, finalHeadUrl] = await Promise.all([
|
|
696
|
+
signCompleteMultipartUpload(input.destination, input.requested.final_object_key, input.requested.upload_id, input.expiresIn),
|
|
697
|
+
signAbortMultipartUpload(input.destination, input.requested.final_object_key, input.requested.upload_id, input.expiresIn),
|
|
698
|
+
Promise.all(listPageMarkers.map((partNumberMarker) => signListMultipartUpload(input.destination, input.requested.final_object_key, input.requested.upload_id, input.expiresIn, { maxParts: 1_000, ...(partNumberMarker ? { partNumberMarker } : {}) }))),
|
|
699
|
+
signFinalObjectHead(input.destination, input.requested.final_object_key, input.expiresIn)
|
|
700
|
+
]);
|
|
701
|
+
const manifest = {
|
|
702
|
+
multipart_group_id: input.requested.multipart_group_id,
|
|
703
|
+
source_id: input.requested.source_id,
|
|
704
|
+
destination_id: input.requested.destination_id,
|
|
705
|
+
final_object_key: input.requested.final_object_key,
|
|
706
|
+
upload_id: input.requested.upload_id,
|
|
707
|
+
expected_object_size: source.size,
|
|
708
|
+
expected_part_count: source.chunk_count,
|
|
709
|
+
max_part_number: maxPartNumber,
|
|
710
|
+
complete_url: completeUrl,
|
|
711
|
+
abort_url: abortUrl,
|
|
712
|
+
list_page_urls: listPageUrls,
|
|
713
|
+
final_head_url: finalHeadUrl,
|
|
714
|
+
final_object_metadata: finalObjectMetadata,
|
|
715
|
+
urls_expires_at: expiresAtIso(input.expiresIn)
|
|
716
|
+
};
|
|
717
|
+
validateMultipartGroupManifest([manifest], input.prepared.transfer_id);
|
|
718
|
+
const state = {
|
|
719
|
+
destination: input.destination,
|
|
720
|
+
objectKey: input.requested.final_object_key,
|
|
721
|
+
uploadId: input.requested.upload_id,
|
|
722
|
+
manifest
|
|
723
|
+
};
|
|
724
|
+
input.multipartUploads.set(input.requested.multipart_group_id, state);
|
|
725
|
+
return state;
|
|
726
|
+
}
|
|
607
727
|
async submitIntegrityAuditGrantsIfPresent(status) {
|
|
608
728
|
const challenge = status.integrity_audit_challenge;
|
|
609
729
|
if (!challenge)
|
|
@@ -1496,3 +1616,13 @@ function clearRecoverySecrets(input) {
|
|
|
1496
1616
|
}
|
|
1497
1617
|
}
|
|
1498
1618
|
}
|
|
1619
|
+
function clearProviderResumeSecrets(input) {
|
|
1620
|
+
const secretKeys = new Set(["access_key_id", "secret_access_key", "session_token", "api_token", "token", "password"]);
|
|
1621
|
+
for (const provider of [...input.sources, ...input.destinations]) {
|
|
1622
|
+
const record = provider;
|
|
1623
|
+
for (const key of secretKeys) {
|
|
1624
|
+
if (key in record)
|
|
1625
|
+
record[key] = "";
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
package/dist/models.d.ts
CHANGED
|
@@ -171,6 +171,18 @@ export interface TransferStatusInfo {
|
|
|
171
171
|
};
|
|
172
172
|
integrity_audit_challenge?: IntegrityAuditChallenge;
|
|
173
173
|
}
|
|
174
|
+
export interface ProviderTransferResumeInput {
|
|
175
|
+
transferId: string;
|
|
176
|
+
sources: ProviderSourceConfig[];
|
|
177
|
+
destinations: ProviderDestinationConfig[];
|
|
178
|
+
name?: string;
|
|
179
|
+
/**
|
|
180
|
+
* Maps to BeamCore test_mode. Must match the original prepared transfer.
|
|
181
|
+
*/
|
|
182
|
+
testMode?: boolean;
|
|
183
|
+
expiresIn?: number;
|
|
184
|
+
signedUrlFlow?: SignedUrlFlow;
|
|
185
|
+
}
|
|
174
186
|
export interface TransferTerminalEvent {
|
|
175
187
|
schema_version: "transfer-client-control/v6";
|
|
176
188
|
producer: "transfer-runtime";
|
package/dist/nats-control.js
CHANGED
|
@@ -8,6 +8,7 @@ export const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
|
|
|
8
8
|
export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
|
|
9
9
|
export const BEAM_ROUTE_TARGET_PAYLOAD_BYTES = 4 * 1024 * 1024;
|
|
10
10
|
const ROUTE_BATCH_AUTH_TOKEN_ESTIMATE_BYTES = 64 * 1024;
|
|
11
|
+
const AUTH_TOKEN_REFRESH_SAFETY_SECONDS = 30;
|
|
11
12
|
export const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE = "transfer.route_recovery.sign";
|
|
12
13
|
export function buildConnectionOptions(options) {
|
|
13
14
|
const connectionOptions = {
|
|
@@ -199,32 +200,33 @@ export class BeamTransferControl {
|
|
|
199
200
|
void this.recoverLease(lease);
|
|
200
201
|
}
|
|
201
202
|
async request(messageType, payload, options = {}) {
|
|
202
|
-
const authToken = await this.resolveAuthToken();
|
|
203
203
|
const shardId = options.shardId ?? (options.transferId ? transferShardId(options.transferId, this.shardCount) : 0);
|
|
204
204
|
const requestId = options.idempotencyKey
|
|
205
205
|
? await stableRequestId(`${messageType}:${options.idempotencyKey}`)
|
|
206
206
|
: randomId();
|
|
207
|
-
const
|
|
208
|
-
message_id: `${TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION}:${this.environment}:${this.keyPrefix}:${messageType}:${requestId}`,
|
|
209
|
-
schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
210
|
-
environment: this.environment,
|
|
211
|
-
key_prefix: this.keyPrefix,
|
|
212
|
-
shard_id: shardId,
|
|
213
|
-
message_type: messageType,
|
|
214
|
-
request_id: requestId,
|
|
215
|
-
auth_token: authToken,
|
|
216
|
-
occurred_at: new Date().toISOString(),
|
|
217
|
-
producer: "sdk",
|
|
218
|
-
payload
|
|
219
|
-
};
|
|
220
|
-
const bytes = encode(envelope);
|
|
221
|
-
if (bytes.byteLength > this.maxPayloadBytes) {
|
|
222
|
-
throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
223
|
-
}
|
|
207
|
+
const occurredAt = new Date().toISOString();
|
|
224
208
|
const subject = this.requestSubject(messageType, shardId);
|
|
225
209
|
let lastError;
|
|
226
210
|
for (let attempt = 0; attempt < LIFECYCLE_REQUEST_MAX_ATTEMPTS; attempt += 1) {
|
|
227
211
|
try {
|
|
212
|
+
const authToken = await this.resolveAuthToken();
|
|
213
|
+
const envelope = {
|
|
214
|
+
message_id: `${TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION}:${this.environment}:${this.keyPrefix}:${messageType}:${requestId}`,
|
|
215
|
+
schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
216
|
+
environment: this.environment,
|
|
217
|
+
key_prefix: this.keyPrefix,
|
|
218
|
+
shard_id: shardId,
|
|
219
|
+
message_type: messageType,
|
|
220
|
+
request_id: requestId,
|
|
221
|
+
auth_token: authToken,
|
|
222
|
+
occurred_at: occurredAt,
|
|
223
|
+
producer: "sdk",
|
|
224
|
+
payload
|
|
225
|
+
};
|
|
226
|
+
const bytes = encode(envelope);
|
|
227
|
+
if (bytes.byteLength > this.maxPayloadBytes) {
|
|
228
|
+
throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
229
|
+
}
|
|
228
230
|
const nc = await this.connection();
|
|
229
231
|
const response = await nc.request(subject, bytes, { timeout: this.requestTimeoutMs });
|
|
230
232
|
const decoded = decode(response.data);
|
|
@@ -233,6 +235,13 @@ export class BeamTransferControl {
|
|
|
233
235
|
return (decoded.payload ?? {});
|
|
234
236
|
const body = JSON.stringify(decoded.error ?? {});
|
|
235
237
|
const error = new LifecycleRequestError(decoded.status, `Beam lifecycle request failed with ${decoded.status}: ${body}`);
|
|
238
|
+
if (isExpiredAuthTokenReply(decoded) && attempt + 1 < LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
|
|
239
|
+
this.authToken = null;
|
|
240
|
+
this.authTokenExpiresAt = 0;
|
|
241
|
+
lastError = error;
|
|
242
|
+
await sleepWithJitter(LIFECYCLE_REQUEST_RETRY_DELAYS_MS[attempt] ?? 1_000);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
236
245
|
if (!isRetryableLifecycleStatus(decoded.status) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
|
|
237
246
|
throw error;
|
|
238
247
|
}
|
|
@@ -507,7 +516,7 @@ export class BeamTransferControl {
|
|
|
507
516
|
}
|
|
508
517
|
async resolveAuthToken() {
|
|
509
518
|
const now = Math.floor(Date.now() / 1000);
|
|
510
|
-
if (this.authToken && this.authTokenExpiresAt -
|
|
519
|
+
if (this.authToken && this.authTokenExpiresAt - AUTH_TOKEN_REFRESH_SAFETY_SECONDS > now)
|
|
511
520
|
return this.authToken;
|
|
512
521
|
if (this.authResolvePromise)
|
|
513
522
|
return this.authResolvePromise;
|
|
@@ -707,6 +716,9 @@ const LIFECYCLE_REQUEST_RETRY_DELAYS_MS = [150, 500];
|
|
|
707
716
|
function isRetryableLifecycleStatus(status) {
|
|
708
717
|
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
709
718
|
}
|
|
719
|
+
function isExpiredAuthTokenReply(reply) {
|
|
720
|
+
return reply.status === 401 && reply.error?.code === "auth_token_expired";
|
|
721
|
+
}
|
|
710
722
|
export function isRetryableLifecycleError(error) {
|
|
711
723
|
if (error instanceof LifecycleRequestError)
|
|
712
724
|
return isRetryableLifecycleStatus(error.status);
|