@beam-network/sdk 0.5.0 → 0.5.2
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 +6 -2
- package/dist/client.d.ts +19 -2
- package/dist/client.js +331 -169
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/models.d.ts +10 -58
- package/dist/nats-control.d.ts +33 -16
- package/dist/nats-control.js +187 -60
- package/dist/provider-signing.js +1 -2
- package/package.json +35 -35
- package/dist/errors.d.ts +0 -17
- package/dist/errors.js +0 -26
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/models.d.ts
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
export type SignedUrlFlow = "signed_url_v1" | "signed_url_v2";
|
|
2
|
-
export type CreditVerdict = "unlimited" | "ok" | "low" | "exhausted" | "unknown";
|
|
3
|
-
/**
|
|
4
|
-
* Credit state for the API key that made the request.
|
|
5
|
-
*
|
|
6
|
-
* The verdict is computed server-side and is the single source of truth: never
|
|
7
|
-
* derive one client-side from `remaining` and `low_threshold`, or the SDK will
|
|
8
|
-
* warn at a different point than Beam actually blocks.
|
|
9
|
-
*/
|
|
10
|
-
export interface CreditStatus {
|
|
11
|
-
verdict: CreditVerdict;
|
|
12
|
-
/** Credits left in the organization pool. Null when unlimited or unknown. */
|
|
13
|
-
remaining: number | null;
|
|
14
|
-
/** Balance below which Beam warns. Display only. */
|
|
15
|
-
low_threshold: number | null;
|
|
16
|
-
billing_key_id: string | null;
|
|
17
|
-
billing_org_id: string | null;
|
|
18
|
-
}
|
|
19
|
-
export interface CreditWarning {
|
|
20
|
-
code: string;
|
|
21
|
-
message: string;
|
|
22
|
-
detail?: unknown;
|
|
23
|
-
credits?: CreditStatus;
|
|
24
|
-
}
|
|
25
2
|
export interface BeamClientOptions {
|
|
26
3
|
apiKey?: string;
|
|
27
4
|
natsUrl?: string;
|
|
@@ -33,12 +10,6 @@ export interface BeamClientOptions {
|
|
|
33
10
|
maxPayloadBytes?: number;
|
|
34
11
|
routeSigningConcurrency?: number;
|
|
35
12
|
fetch?: typeof fetch;
|
|
36
|
-
/**
|
|
37
|
-
* Called when Beam reports a credit problem, including once at connect time
|
|
38
|
-
* from the auth token. Without a handler the SDK logs one line per verdict
|
|
39
|
-
* change rather than per request.
|
|
40
|
-
*/
|
|
41
|
-
onCreditWarning?: (warning: CreditWarning) => void;
|
|
42
13
|
}
|
|
43
14
|
export interface SourceConfig {
|
|
44
15
|
type: string;
|
|
@@ -99,10 +70,10 @@ export interface RawTransferCreateInput {
|
|
|
99
70
|
progressiveMode?: boolean;
|
|
100
71
|
signedUrlFlow?: SignedUrlFlow;
|
|
101
72
|
idempotencyKey?: string;
|
|
73
|
+
/** Internal recovery generation; callers normally leave this unset. */
|
|
74
|
+
routeGenerationId?: string;
|
|
102
75
|
}
|
|
103
76
|
export interface TransferCreateResponse {
|
|
104
|
-
/** Credit state after this request. */
|
|
105
|
-
credits?: CreditStatus;
|
|
106
77
|
success: boolean;
|
|
107
78
|
transfer_id: string;
|
|
108
79
|
transfer_key?: string;
|
|
@@ -116,8 +87,6 @@ export interface TransferCreateResponse {
|
|
|
116
87
|
message?: string;
|
|
117
88
|
}
|
|
118
89
|
export interface TransferCancelResponse {
|
|
119
|
-
/** Credit state after this request. */
|
|
120
|
-
credits?: CreditStatus;
|
|
121
90
|
success: boolean;
|
|
122
91
|
message?: string;
|
|
123
92
|
}
|
|
@@ -148,8 +117,6 @@ export interface DestinationStatusInfo {
|
|
|
148
117
|
location?: string;
|
|
149
118
|
}
|
|
150
119
|
export interface TransferStatusInfo {
|
|
151
|
-
/** Credit state after this request. */
|
|
152
|
-
credits?: CreditStatus;
|
|
153
120
|
transfer_id: string;
|
|
154
121
|
name?: string;
|
|
155
122
|
status: string;
|
|
@@ -180,7 +147,7 @@ export interface TransferStatusInfo {
|
|
|
180
147
|
};
|
|
181
148
|
}
|
|
182
149
|
export interface TransferTerminalEvent {
|
|
183
|
-
schema_version: "transfer-client-control/
|
|
150
|
+
schema_version: "transfer-client-control/v5";
|
|
184
151
|
producer: "transfer-runtime";
|
|
185
152
|
transfer_id: string;
|
|
186
153
|
status: "completed" | "failed" | "cancelled";
|
|
@@ -211,23 +178,6 @@ export interface R2ProviderConfig {
|
|
|
211
178
|
account_id?: string;
|
|
212
179
|
endpoint_url?: string;
|
|
213
180
|
}
|
|
214
|
-
export interface S3CompatibleProviderCapabilities {
|
|
215
|
-
/**
|
|
216
|
-
* Whether the provider's ListParts / ListObjects response includes a per-part
|
|
217
|
-
* LastModified timestamp. Defaults to true (S3-spec compliant). Providers that
|
|
218
|
-
* omit it (e.g. Hippius) declare false so Beam finalization substitutes a
|
|
219
|
-
* Beam-side timestamp instead of looping into multipart recovery.
|
|
220
|
-
*/
|
|
221
|
-
multipart_part_last_modified?: boolean;
|
|
222
|
-
/**
|
|
223
|
-
* Per-destination override for Beam Core's assignment-recovery deadline
|
|
224
|
-
* (TRANSFER_ASSIGNMENT_TIMEOUT_MS), in milliseconds. Omit to use the global
|
|
225
|
-
* default (15s). Slower providers (e.g. Hippius) declare a larger value so
|
|
226
|
-
* their assignment waves aren't reassigned prematurely. Only integers >= 1000
|
|
227
|
-
* are honoured by Beam Core.
|
|
228
|
-
*/
|
|
229
|
-
assignment_timeout_ms?: number;
|
|
230
|
-
}
|
|
231
181
|
export interface S3CompatibleProviderConfig {
|
|
232
182
|
provider: string;
|
|
233
183
|
driver?: "s3-compatible";
|
|
@@ -241,7 +191,6 @@ export interface S3CompatibleProviderConfig {
|
|
|
241
191
|
session_token?: string;
|
|
242
192
|
force_path_style?: boolean;
|
|
243
193
|
account_id?: string;
|
|
244
|
-
capabilities?: S3CompatibleProviderCapabilities;
|
|
245
194
|
}
|
|
246
195
|
export interface HippiusProviderConfig {
|
|
247
196
|
provider: "hippius";
|
|
@@ -289,6 +238,8 @@ export interface ProviderTransferCreateInput {
|
|
|
289
238
|
signedUrlFlow?: SignedUrlFlow;
|
|
290
239
|
/** Stable caller identity used to derive the transfer and lifecycle request ids. */
|
|
291
240
|
idempotencyKey?: string;
|
|
241
|
+
/** Internal recovery generation; callers normally omit this. */
|
|
242
|
+
routeGenerationId?: string;
|
|
292
243
|
}
|
|
293
244
|
export interface PlanningHttpSource {
|
|
294
245
|
source_id: string;
|
|
@@ -385,8 +336,6 @@ export interface MultipartGroupManifest {
|
|
|
385
336
|
urls_expires_at: string;
|
|
386
337
|
}
|
|
387
338
|
export interface TransferPlanResponse {
|
|
388
|
-
/** Credit state after this request. */
|
|
389
|
-
credits?: CreditStatus;
|
|
390
339
|
success: boolean;
|
|
391
340
|
chunk_size?: number;
|
|
392
341
|
total_size?: number;
|
|
@@ -396,12 +345,12 @@ export interface TransferPlanResponse {
|
|
|
396
345
|
total_chunks?: number;
|
|
397
346
|
plan_descriptor: CompactTransferPlanDescriptor;
|
|
398
347
|
signed_url_flow: SignedUrlFlow;
|
|
348
|
+
plan_fingerprint: string;
|
|
349
|
+
coordinate_checksum: string;
|
|
399
350
|
error?: string;
|
|
400
351
|
message?: string;
|
|
401
352
|
}
|
|
402
353
|
export interface TransferPrepareResponse {
|
|
403
|
-
/** Credit state after this request. */
|
|
404
|
-
credits?: CreditStatus;
|
|
405
354
|
success: boolean;
|
|
406
355
|
transfer_id: string;
|
|
407
356
|
transfer_key?: string;
|
|
@@ -414,6 +363,9 @@ export interface TransferPrepareResponse {
|
|
|
414
363
|
total_chunks?: number;
|
|
415
364
|
plan_descriptor: CompactTransferPlanDescriptor;
|
|
416
365
|
signed_url_flow: SignedUrlFlow;
|
|
366
|
+
plan_fingerprint: string;
|
|
367
|
+
coordinate_checksum: string;
|
|
368
|
+
route_generation_id: string;
|
|
417
369
|
error?: string;
|
|
418
370
|
message?: string;
|
|
419
371
|
}
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { type ConnectionOptions } from "nats";
|
|
2
|
+
import type { MultipartGroupManifest, SignedChunkRoute, TransferTerminalSignalWaiter } from "./models.js";
|
|
3
|
+
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v5";
|
|
3
4
|
export declare const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
|
|
4
5
|
export declare const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
|
|
5
6
|
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.manifest" | "transfer.route_stream.batch" | "transfer.route_stream.complete" | "transfer.distribute" | "transfer.status" | "transfer.cancel";
|
|
7
|
+
export type TransferClientMessageType = "runtime.hello" | "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" | "transfer.resume";
|
|
7
8
|
export interface CompactSignedRouteBatch {
|
|
8
9
|
source_chunks: Array<{
|
|
9
10
|
source_ref: number;
|
|
@@ -28,6 +29,7 @@ export interface CompactSignedRouteBatch {
|
|
|
28
29
|
}
|
|
29
30
|
export interface RouteStreamBeginPayload {
|
|
30
31
|
transfer_id: string;
|
|
32
|
+
route_generation_id: string;
|
|
31
33
|
stream_id: string;
|
|
32
34
|
total_routes: number;
|
|
33
35
|
total_chunks: number;
|
|
@@ -38,10 +40,16 @@ export interface RouteStreamBeginPayload {
|
|
|
38
40
|
}
|
|
39
41
|
export interface RouteStreamManifestPayload {
|
|
40
42
|
transfer_id: string;
|
|
43
|
+
route_generation_id: string;
|
|
41
44
|
stream_id: string;
|
|
42
45
|
manifest_batch_id: string;
|
|
43
46
|
groups: MultipartGroupManifest[];
|
|
44
47
|
}
|
|
48
|
+
export declare function buildConnectionOptions(options: {
|
|
49
|
+
natsUrl: string;
|
|
50
|
+
apiKey: string;
|
|
51
|
+
keyPrefix: string;
|
|
52
|
+
}): ConnectionOptions;
|
|
45
53
|
export interface TransferControlOptions {
|
|
46
54
|
apiKey: string;
|
|
47
55
|
natsUrl?: string;
|
|
@@ -51,11 +59,18 @@ export interface TransferControlOptions {
|
|
|
51
59
|
transferRuntimeShardCount?: number;
|
|
52
60
|
requestTimeoutMs?: number;
|
|
53
61
|
maxPayloadBytes?: number;
|
|
54
|
-
onCreditWarning?: (warning: CreditWarning) => void;
|
|
55
62
|
}
|
|
56
63
|
export interface LifecycleRequestOptions {
|
|
57
64
|
transferId?: string;
|
|
58
65
|
idempotencyKey?: string;
|
|
66
|
+
shardId?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface TransferRecoveryLease {
|
|
69
|
+
transferId: string;
|
|
70
|
+
planFingerprint: string;
|
|
71
|
+
coordinateChecksum: string;
|
|
72
|
+
replayRoutes(routeGenerationId: string): Promise<void>;
|
|
73
|
+
disposeSecrets?(): void;
|
|
59
74
|
}
|
|
60
75
|
export declare class BeamTransferControl {
|
|
61
76
|
readonly environment: string;
|
|
@@ -73,24 +88,24 @@ export declare class BeamTransferControl {
|
|
|
73
88
|
private authTokenExpiresAt;
|
|
74
89
|
private authResolvePromise;
|
|
75
90
|
private readonly terminalWaitCancellations;
|
|
76
|
-
private
|
|
77
|
-
private
|
|
78
|
-
private readonly
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Records the credit state from a reply or auth token and surfaces a warning.
|
|
83
|
-
*
|
|
84
|
-
* Fires on verdict transitions, not per request, so a long-running client does
|
|
85
|
-
* not emit the same warning on every call.
|
|
86
|
-
*/
|
|
87
|
-
private observeCredits;
|
|
91
|
+
private readonly recoveryLeases;
|
|
92
|
+
private readonly recoveryOperations;
|
|
93
|
+
private readonly recoveryRequestedEpoch;
|
|
94
|
+
private readonly runtimeEpochs;
|
|
95
|
+
private helloTimer;
|
|
88
96
|
constructor(options: TransferControlOptions);
|
|
89
97
|
close(): Promise<void>;
|
|
98
|
+
registerRecoveryLease(lease: TransferRecoveryLease): () => void;
|
|
99
|
+
releaseRecoveryLease(transferId: string): void;
|
|
100
|
+
continueRecoveryLease(transferId: string): void;
|
|
90
101
|
request<T>(messageType: TransferClientMessageType, payload: Record<string, unknown>, options?: LifecycleRequestOptions): Promise<T>;
|
|
91
102
|
openTerminalSignalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
|
|
92
103
|
splitRoutesForPayload(messageType: TransferClientMessageType, basePayload: Record<string, unknown>, routes: SignedChunkRoute[]): SignedChunkRoute[][];
|
|
93
104
|
private connection;
|
|
105
|
+
private ensureHelloMonitor;
|
|
106
|
+
private pollRuntimeHello;
|
|
107
|
+
private observeRuntimeEpoch;
|
|
108
|
+
private recoverLease;
|
|
94
109
|
private resolveAuthToken;
|
|
95
110
|
private resolveAuthTokenFresh;
|
|
96
111
|
private authSubject;
|
|
@@ -99,3 +114,5 @@ export declare class BeamTransferControl {
|
|
|
99
114
|
}
|
|
100
115
|
export declare function compactSignedRoutes(routes: SignedChunkRoute[]): CompactSignedRouteBatch;
|
|
101
116
|
export declare function transferShardId(transferId: string, shardCount: number): number;
|
|
117
|
+
export declare function isRetryableLifecycleError(error: unknown): boolean;
|
|
118
|
+
export declare function isRecoverableRouteStreamError(error: unknown): boolean;
|
package/dist/nats-control.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { encode, decode } from "@msgpack/msgpack";
|
|
2
2
|
import { connect } from "nats";
|
|
3
|
-
|
|
4
|
-
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v4";
|
|
3
|
+
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v5";
|
|
5
4
|
export const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
|
|
6
5
|
export const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
|
|
7
6
|
// NATS enforces max_payload per message. This guard splits signed-route control
|
|
8
7
|
// messages before the broker rejects them; transfer bytes never flow through NATS.
|
|
9
|
-
export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES =
|
|
10
|
-
function buildConnectionOptions(options) {
|
|
8
|
+
export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
export function buildConnectionOptions(options) {
|
|
11
10
|
const connectionOptions = {
|
|
12
11
|
servers: options.natsUrl,
|
|
13
12
|
user: options.keyPrefix,
|
|
14
13
|
pass: options.apiKey,
|
|
15
|
-
name: `beam-sdk-${options.keyPrefix}
|
|
14
|
+
name: `beam-sdk-${options.keyPrefix}`,
|
|
15
|
+
reconnect: true,
|
|
16
|
+
maxReconnectAttempts: -1,
|
|
17
|
+
reconnectTimeWait: 1_000,
|
|
18
|
+
reconnectJitter: 500,
|
|
19
|
+
reconnectJitterTLS: 1_000
|
|
16
20
|
};
|
|
17
21
|
if (options.natsUrl.startsWith("tls://")) {
|
|
18
22
|
const tlsOptions = { handshakeFirst: true };
|
|
@@ -26,6 +30,14 @@ function buildConnectionOptions(options) {
|
|
|
26
30
|
}
|
|
27
31
|
return connectionOptions;
|
|
28
32
|
}
|
|
33
|
+
class LifecycleRequestError extends Error {
|
|
34
|
+
status;
|
|
35
|
+
constructor(status, message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.name = "LifecycleRequestError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
29
41
|
export class BeamTransferControl {
|
|
30
42
|
environment;
|
|
31
43
|
keyPrefix;
|
|
@@ -42,42 +54,11 @@ export class BeamTransferControl {
|
|
|
42
54
|
authTokenExpiresAt = 0;
|
|
43
55
|
authResolvePromise = null;
|
|
44
56
|
terminalWaitCancellations = new Set();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return this.lastCredits;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Records the credit state from a reply or auth token and surfaces a warning.
|
|
54
|
-
*
|
|
55
|
-
* Fires on verdict transitions, not per request, so a long-running client does
|
|
56
|
-
* not emit the same warning on every call.
|
|
57
|
-
*/
|
|
58
|
-
observeCredits(credits, warnings) {
|
|
59
|
-
if (!credits)
|
|
60
|
-
return this.lastCredits;
|
|
61
|
-
this.lastCredits = credits;
|
|
62
|
-
if (credits.verdict === this.reportedVerdict)
|
|
63
|
-
return credits;
|
|
64
|
-
this.reportedVerdict = credits.verdict;
|
|
65
|
-
if (credits.verdict !== "low" && credits.verdict !== "exhausted")
|
|
66
|
-
return credits;
|
|
67
|
-
const warning = warnings?.[0] ?? {
|
|
68
|
-
code: credits.verdict === "exhausted" ? "insufficient_credits" : "credits_low",
|
|
69
|
-
message: credits.remaining === null
|
|
70
|
-
? "Beam credit balance is low."
|
|
71
|
-
: `Beam credit balance is low: ${credits.remaining} credits remaining.`,
|
|
72
|
-
credits
|
|
73
|
-
};
|
|
74
|
-
if (this.onCreditWarning) {
|
|
75
|
-
this.onCreditWarning({ ...warning, credits });
|
|
76
|
-
return credits;
|
|
77
|
-
}
|
|
78
|
-
console.warn(`[beam] ${warning.message}`);
|
|
79
|
-
return credits;
|
|
80
|
-
}
|
|
57
|
+
recoveryLeases = new Map();
|
|
58
|
+
recoveryOperations = new Map();
|
|
59
|
+
recoveryRequestedEpoch = new Map();
|
|
60
|
+
runtimeEpochs = new Map();
|
|
61
|
+
helloTimer = null;
|
|
81
62
|
constructor(options) {
|
|
82
63
|
this.apiKey = options.apiKey;
|
|
83
64
|
this.keyPrefix = options.apiKey.slice(0, 12);
|
|
@@ -87,7 +68,6 @@ export class BeamTransferControl {
|
|
|
87
68
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
88
69
|
this.maxPayloadBytes = options.maxPayloadBytes ?? BEAM_DEFAULT_MAX_PAYLOAD_BYTES;
|
|
89
70
|
this.natsUrl = options.natsUrl ?? options.natsWsUrl ?? BEAM_DEFAULT_NATS_URL;
|
|
90
|
-
this.onCreditWarning = options.onCreditWarning;
|
|
91
71
|
if (/^https?:\/\//i.test(this.natsUrl) || /^wss?:\/\//i.test(this.natsUrl) && !options.natsWsUrl) {
|
|
92
72
|
throw new Error("Beam SDK lifecycle endpoint must be nats://, tls://, or configured as natsWsUrl.");
|
|
93
73
|
}
|
|
@@ -99,6 +79,13 @@ export class BeamTransferControl {
|
|
|
99
79
|
this.closed = true;
|
|
100
80
|
for (const cancel of [...this.terminalWaitCancellations])
|
|
101
81
|
cancel();
|
|
82
|
+
if (this.helloTimer)
|
|
83
|
+
clearInterval(this.helloTimer);
|
|
84
|
+
this.helloTimer = null;
|
|
85
|
+
for (const lease of this.recoveryLeases.values())
|
|
86
|
+
lease.disposeSecrets?.();
|
|
87
|
+
this.recoveryLeases.clear();
|
|
88
|
+
this.recoveryRequestedEpoch.clear();
|
|
102
89
|
if (this.connectionPromise) {
|
|
103
90
|
try {
|
|
104
91
|
await this.connectionPromise;
|
|
@@ -112,9 +99,53 @@ export class BeamTransferControl {
|
|
|
112
99
|
if (nc && !nc.isClosed())
|
|
113
100
|
await nc.drain();
|
|
114
101
|
}
|
|
102
|
+
registerRecoveryLease(lease) {
|
|
103
|
+
if (this.closed)
|
|
104
|
+
throw new Error("NATS lifecycle control is closed");
|
|
105
|
+
const previous = this.recoveryLeases.get(lease.transferId);
|
|
106
|
+
if (previous && previous !== lease)
|
|
107
|
+
previous.disposeSecrets?.();
|
|
108
|
+
this.recoveryLeases.set(lease.transferId, lease);
|
|
109
|
+
this.ensureHelloMonitor();
|
|
110
|
+
const shardId = transferShardId(lease.transferId, this.shardCount);
|
|
111
|
+
void this.pollRuntimeHello(shardId);
|
|
112
|
+
return () => {
|
|
113
|
+
const current = this.recoveryLeases.get(lease.transferId);
|
|
114
|
+
if (current !== lease)
|
|
115
|
+
return;
|
|
116
|
+
this.recoveryLeases.delete(lease.transferId);
|
|
117
|
+
this.recoveryRequestedEpoch.delete(lease.transferId);
|
|
118
|
+
lease.disposeSecrets?.();
|
|
119
|
+
if (!this.recoveryLeases.size && this.helloTimer) {
|
|
120
|
+
clearInterval(this.helloTimer);
|
|
121
|
+
this.helloTimer = null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
releaseRecoveryLease(transferId) {
|
|
126
|
+
const lease = this.recoveryLeases.get(transferId);
|
|
127
|
+
if (!lease)
|
|
128
|
+
return;
|
|
129
|
+
this.recoveryLeases.delete(transferId);
|
|
130
|
+
this.recoveryRequestedEpoch.delete(transferId);
|
|
131
|
+
lease.disposeSecrets?.();
|
|
132
|
+
if (!this.recoveryLeases.size && this.helloTimer) {
|
|
133
|
+
clearInterval(this.helloTimer);
|
|
134
|
+
this.helloTimer = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
continueRecoveryLease(transferId) {
|
|
138
|
+
const lease = this.recoveryLeases.get(transferId);
|
|
139
|
+
if (!lease || this.closed)
|
|
140
|
+
return;
|
|
141
|
+
const shardId = transferShardId(transferId, this.shardCount);
|
|
142
|
+
const epoch = this.runtimeEpochs.get(shardId);
|
|
143
|
+
this.recoveryRequestedEpoch.set(transferId, epoch ? `${epoch.runtimeEpoch}:${epoch.transportEpoch}` : `foreground:${randomId()}`);
|
|
144
|
+
void this.recoverLease(lease);
|
|
145
|
+
}
|
|
115
146
|
async request(messageType, payload, options = {}) {
|
|
116
147
|
const authToken = await this.resolveAuthToken();
|
|
117
|
-
const shardId = options.transferId ? transferShardId(options.transferId, this.shardCount) : 0;
|
|
148
|
+
const shardId = options.shardId ?? (options.transferId ? transferShardId(options.transferId, this.shardCount) : 0);
|
|
118
149
|
const requestId = options.idempotencyKey
|
|
119
150
|
? await stableRequestId(`${messageType}:${options.idempotencyKey}`)
|
|
120
151
|
: randomId();
|
|
@@ -142,19 +173,11 @@ export class BeamTransferControl {
|
|
|
142
173
|
const nc = await this.connection();
|
|
143
174
|
const response = await nc.request(subject, bytes, { timeout: this.requestTimeoutMs });
|
|
144
175
|
const decoded = decode(response.data);
|
|
145
|
-
|
|
146
|
-
if (decoded.ok)
|
|
147
|
-
|
|
148
|
-
// Credits ride on the envelope, not the payload, so they reach every
|
|
149
|
-
// response type without a per-message-type model change.
|
|
150
|
-
return (credits ? { ...payload, credits } : payload);
|
|
151
|
-
}
|
|
176
|
+
this.observeRuntimeEpoch(shardId, decoded.runtime_epoch, decoded.transport_epoch);
|
|
177
|
+
if (decoded.ok)
|
|
178
|
+
return (decoded.payload ?? {});
|
|
152
179
|
const body = JSON.stringify(decoded.error ?? {});
|
|
153
|
-
const
|
|
154
|
-
const message = decoded.error?.message ?? body;
|
|
155
|
-
const error = decoded.status === 402
|
|
156
|
-
? new BeamInsufficientCreditsError(message, code, credits, body)
|
|
157
|
-
: new BeamApiError(`Beam lifecycle request failed with ${decoded.status}: ${body}`, decoded.status, body);
|
|
180
|
+
const error = new LifecycleRequestError(decoded.status, `Beam lifecycle request failed with ${decoded.status}: ${body}`);
|
|
158
181
|
if (!isRetryableLifecycleStatus(decoded.status) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
|
|
159
182
|
throw error;
|
|
160
183
|
}
|
|
@@ -329,6 +352,96 @@ export class BeamTransferControl {
|
|
|
329
352
|
this.connectionPromise = null;
|
|
330
353
|
}
|
|
331
354
|
}
|
|
355
|
+
ensureHelloMonitor() {
|
|
356
|
+
if (this.helloTimer || this.closed)
|
|
357
|
+
return;
|
|
358
|
+
this.helloTimer = setInterval(() => {
|
|
359
|
+
const shards = new Set([...this.recoveryLeases.keys()].map((transferId) => transferShardId(transferId, this.shardCount)));
|
|
360
|
+
for (const shardId of shards)
|
|
361
|
+
void this.pollRuntimeHello(shardId);
|
|
362
|
+
}, 5_000);
|
|
363
|
+
this.helloTimer.unref?.();
|
|
364
|
+
}
|
|
365
|
+
async pollRuntimeHello(shardId) {
|
|
366
|
+
try {
|
|
367
|
+
await this.request("runtime.hello", {}, { idempotencyKey: `runtime:hello:${shardId}`, shardId });
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// Infinite NATS reconnect continues in the background; the next hello reconciles epochs.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
observeRuntimeEpoch(shardId, runtimeEpoch, transportEpoch) {
|
|
374
|
+
if (!runtimeEpoch || !transportEpoch)
|
|
375
|
+
return;
|
|
376
|
+
const previous = this.runtimeEpochs.get(shardId);
|
|
377
|
+
this.runtimeEpochs.set(shardId, { runtimeEpoch, transportEpoch });
|
|
378
|
+
if (!previous)
|
|
379
|
+
return;
|
|
380
|
+
if (previous.runtimeEpoch === runtimeEpoch && previous.transportEpoch === transportEpoch)
|
|
381
|
+
return;
|
|
382
|
+
if (previous.runtimeEpoch !== runtimeEpoch) {
|
|
383
|
+
this.authToken = null;
|
|
384
|
+
this.authTokenExpiresAt = 0;
|
|
385
|
+
}
|
|
386
|
+
for (const lease of this.recoveryLeases.values()) {
|
|
387
|
+
if (transferShardId(lease.transferId, this.shardCount) === shardId) {
|
|
388
|
+
this.recoveryRequestedEpoch.set(lease.transferId, `${runtimeEpoch}:${transportEpoch}`);
|
|
389
|
+
void this.recoverLease(lease);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
recoverLease(lease) {
|
|
394
|
+
const existing = this.recoveryOperations.get(lease.transferId);
|
|
395
|
+
if (existing)
|
|
396
|
+
return existing;
|
|
397
|
+
let operation;
|
|
398
|
+
operation = (async () => {
|
|
399
|
+
let attempt = 0;
|
|
400
|
+
while (!this.closed && this.recoveryLeases.get(lease.transferId) === lease) {
|
|
401
|
+
const requestedEpoch = this.recoveryRequestedEpoch.get(lease.transferId);
|
|
402
|
+
const routeGenerationId = randomId();
|
|
403
|
+
try {
|
|
404
|
+
const response = await this.request("transfer.resume", {
|
|
405
|
+
transfer_id: lease.transferId,
|
|
406
|
+
plan_fingerprint: lease.planFingerprint,
|
|
407
|
+
coordinate_checksum: lease.coordinateChecksum,
|
|
408
|
+
route_generation_id: routeGenerationId
|
|
409
|
+
}, { transferId: lease.transferId, idempotencyKey: `transfer:${lease.transferId}:resume:${routeGenerationId}` });
|
|
410
|
+
if (response.recovery === "terminal") {
|
|
411
|
+
this.releaseRecoveryLease(lease.transferId);
|
|
412
|
+
if (this.recoveryOperations.get(lease.transferId) === operation) {
|
|
413
|
+
this.recoveryOperations.delete(lease.transferId);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (response.route_replay_required || response.recovery === "route_replay_required") {
|
|
418
|
+
await lease.replayRoutes(routeGenerationId);
|
|
419
|
+
}
|
|
420
|
+
if (this.recoveryRequestedEpoch.get(lease.transferId) !== requestedEpoch) {
|
|
421
|
+
attempt = 0;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (this.recoveryOperations.get(lease.transferId) === operation) {
|
|
425
|
+
this.recoveryOperations.delete(lease.transferId);
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
if (!isRetryableLifecycleError(error)) {
|
|
431
|
+
this.releaseRecoveryLease(lease.transferId);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
attempt += 1;
|
|
435
|
+
await sleepWithJitter(Math.min(30_000, 500 * (2 ** Math.min(attempt, 6))));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
})().finally(() => {
|
|
439
|
+
if (this.recoveryOperations.get(lease.transferId) === operation)
|
|
440
|
+
this.recoveryOperations.delete(lease.transferId);
|
|
441
|
+
});
|
|
442
|
+
this.recoveryOperations.set(lease.transferId, operation);
|
|
443
|
+
return operation;
|
|
444
|
+
}
|
|
332
445
|
async resolveAuthToken() {
|
|
333
446
|
const now = Math.floor(Date.now() / 1000);
|
|
334
447
|
if (this.authToken && this.authTokenExpiresAt - 5 > now)
|
|
@@ -358,9 +471,6 @@ export class BeamTransferControl {
|
|
|
358
471
|
const claims = decodeJwtPayload(parsed.token);
|
|
359
472
|
this.authToken = parsed.token;
|
|
360
473
|
this.authTokenExpiresAt = claims.exp;
|
|
361
|
-
// Pre-flight: the token carries the verdict, so a low balance surfaces
|
|
362
|
-
// before the first lifecycle request is ever sent.
|
|
363
|
-
this.observeCredits(claims.credits);
|
|
364
474
|
return parsed.token;
|
|
365
475
|
}
|
|
366
476
|
catch (error) {
|
|
@@ -506,14 +616,22 @@ function randomId() {
|
|
|
506
616
|
const cryptoApi = globalThis.crypto;
|
|
507
617
|
if (cryptoApi?.randomUUID)
|
|
508
618
|
return cryptoApi.randomUUID();
|
|
509
|
-
|
|
619
|
+
if (!cryptoApi?.getRandomValues)
|
|
620
|
+
throw new Error("secure UUID generation is unavailable");
|
|
621
|
+
const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
|
|
622
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
623
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
624
|
+
const hex = [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
625
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
510
626
|
}
|
|
511
627
|
const LIFECYCLE_REQUEST_MAX_ATTEMPTS = 3;
|
|
512
628
|
const LIFECYCLE_REQUEST_RETRY_DELAYS_MS = [150, 500];
|
|
513
629
|
function isRetryableLifecycleStatus(status) {
|
|
514
630
|
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
515
631
|
}
|
|
516
|
-
function isRetryableLifecycleError(error) {
|
|
632
|
+
export function isRetryableLifecycleError(error) {
|
|
633
|
+
if (error instanceof LifecycleRequestError)
|
|
634
|
+
return isRetryableLifecycleStatus(error.status);
|
|
517
635
|
const record = error && typeof error === "object" ? error : null;
|
|
518
636
|
const code = typeof record?.code === "string" ? record.code : "";
|
|
519
637
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -526,10 +644,19 @@ function isRetryableLifecycleError(error) {
|
|
|
526
644
|
"econnreset",
|
|
527
645
|
"econnrefused",
|
|
528
646
|
"etimedout",
|
|
647
|
+
"fetch failed",
|
|
529
648
|
"socket",
|
|
530
649
|
"network"
|
|
531
650
|
].some((token) => combined.includes(token));
|
|
532
651
|
}
|
|
652
|
+
export function isRecoverableRouteStreamError(error) {
|
|
653
|
+
if (isRetryableLifecycleError(error))
|
|
654
|
+
return true;
|
|
655
|
+
if (error instanceof LifecycleRequestError)
|
|
656
|
+
return error.status === 404 || error.status === 409;
|
|
657
|
+
const status = error && typeof error === "object" ? Number(error.status) : NaN;
|
|
658
|
+
return status === 404 || status === 409;
|
|
659
|
+
}
|
|
533
660
|
async function sleepWithJitter(baseMs) {
|
|
534
661
|
const jitter = Math.floor(baseMs * (Math.random() * 0.4 - 0.2));
|
|
535
662
|
await new Promise((resolve) => setTimeout(resolve, Math.max(1, baseMs + jitter)));
|
package/dist/provider-signing.js
CHANGED
|
@@ -405,8 +405,7 @@ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
|
|
|
405
405
|
key: source.key,
|
|
406
406
|
region: s3CompatibleRegion(source),
|
|
407
407
|
endpoint_url: endpoint,
|
|
408
|
-
account_id: "account_id" in source ? source.account_id : undefined
|
|
409
|
-
capabilities: "capabilities" in source ? source.capabilities : undefined
|
|
408
|
+
account_id: "account_id" in source ? source.account_id : undefined
|
|
410
409
|
});
|
|
411
410
|
}
|
|
412
411
|
function isHippiusProvider(config) {
|