@beam-network/sdk 0.2.22 → 0.3.0
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 -1
- package/dist/client.d.ts +5 -6
- package/dist/client.js +679 -279
- package/dist/models.d.ts +50 -7
- package/dist/multipart-limits.d.ts +3 -4
- package/dist/multipart-limits.js +8 -9
- package/dist/nats-control.d.ts +26 -43
- package/dist/nats-control.js +262 -124
- package/dist/provider-signing.d.ts +10 -15
- package/dist/provider-signing.js +45 -55
- package/package.json +1 -1
package/dist/nats-control.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
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/v3";
|
|
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
|
|
7
7
|
// messages before the broker rejects them; transfer bytes never flow through NATS.
|
|
8
8
|
export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 24 * 1024 * 1024;
|
|
9
|
-
export const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE = "transfer.route_recovery.sign";
|
|
10
9
|
function buildConnectionOptions(options) {
|
|
11
10
|
const connectionOptions = {
|
|
12
11
|
servers: options.natsUrl,
|
|
@@ -36,8 +35,12 @@ export class BeamTransferControl {
|
|
|
36
35
|
natsUrl;
|
|
37
36
|
apiKey;
|
|
38
37
|
nc = null;
|
|
38
|
+
connectionPromise = null;
|
|
39
|
+
closed = false;
|
|
39
40
|
authToken = null;
|
|
40
41
|
authTokenExpiresAt = 0;
|
|
42
|
+
authResolvePromise = null;
|
|
43
|
+
terminalWaitCancellations = new Set();
|
|
41
44
|
constructor(options) {
|
|
42
45
|
this.apiKey = options.apiKey;
|
|
43
46
|
this.keyPrefix = options.apiKey.slice(0, 12);
|
|
@@ -54,59 +57,29 @@ export class BeamTransferControl {
|
|
|
54
57
|
throw new Error("transferRuntimeShardCount must be a positive integer.");
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
|
-
async serveRouteRecoverySigner(transferId, handler) {
|
|
58
|
-
const nc = await this.connection();
|
|
59
|
-
const subject = this.routeRecoverySignSubject(transferId);
|
|
60
|
-
const sub = nc.subscribe(subject);
|
|
61
|
-
await nc.flush();
|
|
62
|
-
let active = true;
|
|
63
|
-
void (async () => {
|
|
64
|
-
for await (const msg of sub) {
|
|
65
|
-
if (!active)
|
|
66
|
-
break;
|
|
67
|
-
let request = null;
|
|
68
|
-
try {
|
|
69
|
-
request = decode(msg.data);
|
|
70
|
-
if (request.schema_version !== TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION ||
|
|
71
|
-
request.environment !== this.environment ||
|
|
72
|
-
request.key_prefix !== this.keyPrefix ||
|
|
73
|
-
request.transfer_id !== transferId ||
|
|
74
|
-
request.message_type !== ROUTE_RECOVERY_SIGN_MESSAGE_TYPE ||
|
|
75
|
-
request.producer !== "transfer-runtime") {
|
|
76
|
-
throw new Error("route recovery request envelope mismatch");
|
|
77
|
-
}
|
|
78
|
-
const payload = await handler(request.payload);
|
|
79
|
-
msg.respond(encode(this.buildRouteRecoveryReply(request, true, 200, payload)));
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
const fallback = request ?? {
|
|
83
|
-
message_id: "unknown",
|
|
84
|
-
request_id: "unknown",
|
|
85
|
-
transfer_id: transferId
|
|
86
|
-
};
|
|
87
|
-
msg.respond(encode(this.buildRouteRecoveryReply(fallback, false, 500, undefined, {
|
|
88
|
-
code: "route_recovery_sign_failed",
|
|
89
|
-
message: error instanceof Error ? error.message : String(error)
|
|
90
|
-
})));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
})();
|
|
94
|
-
return () => {
|
|
95
|
-
active = false;
|
|
96
|
-
sub.unsubscribe();
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
60
|
async close() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
61
|
+
this.closed = true;
|
|
62
|
+
for (const cancel of [...this.terminalWaitCancellations])
|
|
63
|
+
cancel();
|
|
64
|
+
if (this.connectionPromise) {
|
|
65
|
+
try {
|
|
66
|
+
await this.connectionPromise;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// A connection that loses the close race closes itself before rejecting.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const nc = this.nc;
|
|
103
73
|
this.nc = null;
|
|
74
|
+
if (nc && !nc.isClosed())
|
|
75
|
+
await nc.drain();
|
|
104
76
|
}
|
|
105
|
-
async request(messageType, payload,
|
|
106
|
-
const nc = await this.connection();
|
|
77
|
+
async request(messageType, payload, options = {}) {
|
|
107
78
|
const authToken = await this.resolveAuthToken();
|
|
108
|
-
const shardId = transferId ? transferShardId(transferId, this.shardCount) : 0;
|
|
109
|
-
const requestId =
|
|
79
|
+
const shardId = options.transferId ? transferShardId(options.transferId, this.shardCount) : 0;
|
|
80
|
+
const requestId = options.idempotencyKey
|
|
81
|
+
? await stableRequestId(`${messageType}:${options.idempotencyKey}`)
|
|
82
|
+
: randomId();
|
|
110
83
|
const envelope = {
|
|
111
84
|
message_id: `${TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION}:${this.environment}:${this.keyPrefix}:${messageType}:${requestId}`,
|
|
112
85
|
schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
@@ -125,13 +98,120 @@ export class BeamTransferControl {
|
|
|
125
98
|
throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
126
99
|
}
|
|
127
100
|
const subject = this.requestSubject(messageType, shardId);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
101
|
+
let lastError;
|
|
102
|
+
for (let attempt = 0; attempt < LIFECYCLE_REQUEST_MAX_ATTEMPTS; attempt += 1) {
|
|
103
|
+
try {
|
|
104
|
+
const nc = await this.connection();
|
|
105
|
+
const response = await nc.request(subject, bytes, { timeout: this.requestTimeoutMs });
|
|
106
|
+
const decoded = decode(response.data);
|
|
107
|
+
if (decoded.ok)
|
|
108
|
+
return (decoded.payload ?? {});
|
|
109
|
+
const body = JSON.stringify(decoded.error ?? {});
|
|
110
|
+
const error = new Error(`Beam lifecycle request failed with ${decoded.status}: ${body}`);
|
|
111
|
+
if (!isRetryableLifecycleStatus(decoded.status) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
lastError = error;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (!isRetryableLifecycleError(error) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
lastError = error;
|
|
121
|
+
if (this.nc?.isClosed())
|
|
122
|
+
this.nc = null;
|
|
123
|
+
}
|
|
124
|
+
await sleepWithJitter(LIFECYCLE_REQUEST_RETRY_DELAYS_MS[attempt] ?? 1_000);
|
|
125
|
+
}
|
|
126
|
+
throw lastError instanceof Error ? lastError : new Error("Beam lifecycle request failed after retries");
|
|
127
|
+
}
|
|
128
|
+
async openTerminalSignalWaiter(transferId) {
|
|
129
|
+
const nc = await this.connection();
|
|
130
|
+
const pending = [];
|
|
131
|
+
let activeWait = null;
|
|
132
|
+
let closed = false;
|
|
133
|
+
let terminalError = null;
|
|
134
|
+
let subscription = null;
|
|
135
|
+
const settleActive = (event, error) => {
|
|
136
|
+
const wait = activeWait;
|
|
137
|
+
if (!wait)
|
|
138
|
+
return;
|
|
139
|
+
activeWait = null;
|
|
140
|
+
clearTimeout(wait.timer);
|
|
141
|
+
if (error !== undefined)
|
|
142
|
+
wait.reject(error);
|
|
143
|
+
else
|
|
144
|
+
wait.resolve(event);
|
|
145
|
+
};
|
|
146
|
+
const close = async () => {
|
|
147
|
+
if (closed)
|
|
148
|
+
return;
|
|
149
|
+
closed = true;
|
|
150
|
+
this.terminalWaitCancellations.delete(cancel);
|
|
151
|
+
subscription?.unsubscribe();
|
|
152
|
+
settleActive(null);
|
|
153
|
+
};
|
|
154
|
+
const cancel = () => { void close(); };
|
|
155
|
+
try {
|
|
156
|
+
subscription = nc.subscribe(this.terminalSubject(transferId), {
|
|
157
|
+
callback: (error, message) => {
|
|
158
|
+
if (error) {
|
|
159
|
+
terminalError = error;
|
|
160
|
+
subscription?.unsubscribe();
|
|
161
|
+
settleActive(null, error);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const event = parseTerminalEvent(decode(message.data), transferId);
|
|
166
|
+
if (activeWait)
|
|
167
|
+
settleActive(event);
|
|
168
|
+
else if (pending.length === 0)
|
|
169
|
+
pending.push(event);
|
|
170
|
+
}
|
|
171
|
+
catch (decodeError) {
|
|
172
|
+
terminalError = decodeError;
|
|
173
|
+
subscription?.unsubscribe();
|
|
174
|
+
settleActive(null, decodeError);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
await nc.flush();
|
|
179
|
+
if (this.closed) {
|
|
180
|
+
await close();
|
|
181
|
+
throw new Error("NATS lifecycle control is closed");
|
|
182
|
+
}
|
|
183
|
+
this.terminalWaitCancellations.add(cancel);
|
|
184
|
+
if (this.closed) {
|
|
185
|
+
await close();
|
|
186
|
+
throw new Error("NATS lifecycle control is closed");
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
wait: async (timeoutMs) => {
|
|
190
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
191
|
+
throw new Error("timeoutMs must be a positive number.");
|
|
192
|
+
}
|
|
193
|
+
if (closed)
|
|
194
|
+
return null;
|
|
195
|
+
if (terminalError !== null)
|
|
196
|
+
throw terminalError;
|
|
197
|
+
const event = pending.shift();
|
|
198
|
+
if (event)
|
|
199
|
+
return event;
|
|
200
|
+
if (activeWait)
|
|
201
|
+
throw new Error("terminal signal waiter already has an active wait");
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const timer = setTimeout(() => settleActive(null), timeoutMs);
|
|
204
|
+
activeWait = { resolve, reject, timer };
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
close
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
subscription?.unsubscribe();
|
|
212
|
+
settleActive(null);
|
|
213
|
+
throw error;
|
|
133
214
|
}
|
|
134
|
-
return (decoded.payload ?? {});
|
|
135
215
|
}
|
|
136
216
|
splitRoutesForPayload(messageType, basePayload, routes) {
|
|
137
217
|
const split = (candidate) => {
|
|
@@ -158,56 +238,99 @@ export class BeamTransferControl {
|
|
|
158
238
|
return routes.length ? split(routes) : [];
|
|
159
239
|
}
|
|
160
240
|
async connection() {
|
|
241
|
+
if (this.closed)
|
|
242
|
+
throw new Error("NATS lifecycle control is closed");
|
|
161
243
|
if (this.nc && !this.nc.isClosed())
|
|
162
244
|
return this.nc;
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
245
|
+
if (this.connectionPromise)
|
|
246
|
+
return this.connectionPromise;
|
|
247
|
+
const pending = (async () => {
|
|
248
|
+
const nc = await connect(buildConnectionOptions({
|
|
249
|
+
natsUrl: this.natsUrl,
|
|
250
|
+
apiKey: this.apiKey,
|
|
251
|
+
keyPrefix: this.keyPrefix
|
|
252
|
+
}));
|
|
253
|
+
if (this.closed) {
|
|
254
|
+
await nc.drain();
|
|
255
|
+
throw new Error("NATS lifecycle control is closed");
|
|
256
|
+
}
|
|
257
|
+
this.nc = nc;
|
|
258
|
+
return nc;
|
|
259
|
+
})();
|
|
260
|
+
this.connectionPromise = pending;
|
|
261
|
+
try {
|
|
262
|
+
return await pending;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
if (this.connectionPromise === pending)
|
|
266
|
+
this.connectionPromise = null;
|
|
267
|
+
}
|
|
169
268
|
}
|
|
170
269
|
async resolveAuthToken() {
|
|
171
270
|
const now = Math.floor(Date.now() / 1000);
|
|
172
271
|
if (this.authToken && this.authTokenExpiresAt - 5 > now)
|
|
173
272
|
return this.authToken;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
273
|
+
if (this.authResolvePromise)
|
|
274
|
+
return this.authResolvePromise;
|
|
275
|
+
const pending = this.resolveAuthTokenFresh();
|
|
276
|
+
this.authResolvePromise = pending;
|
|
277
|
+
try {
|
|
278
|
+
return await pending;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
if (this.authResolvePromise === pending)
|
|
282
|
+
this.authResolvePromise = null;
|
|
179
283
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
284
|
+
}
|
|
285
|
+
async resolveAuthTokenFresh() {
|
|
286
|
+
let lastError;
|
|
287
|
+
for (let attempt = 0; attempt < LIFECYCLE_REQUEST_MAX_ATTEMPTS; attempt += 1) {
|
|
288
|
+
try {
|
|
289
|
+
const nc = await this.connection();
|
|
290
|
+
const response = await nc.request(this.authSubject(), new TextEncoder().encode("{}"), { timeout: this.requestTimeoutMs });
|
|
291
|
+
const parsed = JSON.parse(new TextDecoder().decode(response.data));
|
|
292
|
+
if (!parsed.ok || !parsed.token) {
|
|
293
|
+
throw new Error(`Beam SDK NATS auth resolve failed: ${parsed.error ?? "unknown_error"}`);
|
|
294
|
+
}
|
|
295
|
+
const claims = decodeJwtPayload(parsed.token);
|
|
296
|
+
this.authToken = parsed.token;
|
|
297
|
+
this.authTokenExpiresAt = claims.exp;
|
|
298
|
+
return parsed.token;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (!isRetryableLifecycleError(error) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
lastError = error;
|
|
305
|
+
if (this.nc?.isClosed())
|
|
306
|
+
this.nc = null;
|
|
307
|
+
await sleepWithJitter(LIFECYCLE_REQUEST_RETRY_DELAYS_MS[attempt] ?? 1_000);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw lastError instanceof Error ? lastError : new Error("Beam SDK NATS auth resolve failed after retries");
|
|
184
311
|
}
|
|
185
312
|
authSubject() {
|
|
186
313
|
return `${this.subjectPrefix}.${this.environment}.auth.${this.keyPrefix}.resolve`;
|
|
187
314
|
}
|
|
188
|
-
routeRecoverySignSubject(transferId) {
|
|
189
|
-
return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.transfer.${transferId}.route_recovery_sign`;
|
|
190
|
-
}
|
|
191
|
-
buildRouteRecoveryReply(request, ok, status, payload, error) {
|
|
192
|
-
return {
|
|
193
|
-
message_id: request.message_id,
|
|
194
|
-
schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
195
|
-
environment: this.environment,
|
|
196
|
-
key_prefix: this.keyPrefix,
|
|
197
|
-
transfer_id: request.transfer_id,
|
|
198
|
-
message_type: ROUTE_RECOVERY_SIGN_MESSAGE_TYPE,
|
|
199
|
-
request_id: request.request_id,
|
|
200
|
-
occurred_at: new Date().toISOString(),
|
|
201
|
-
producer: "sdk",
|
|
202
|
-
ok,
|
|
203
|
-
status,
|
|
204
|
-
...(payload ? { payload } : {}),
|
|
205
|
-
...(error ? { error } : {})
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
315
|
requestSubject(messageType, shardId) {
|
|
209
316
|
return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.shard.${shardId}.${messageType.replace(/\./g, "_")}`;
|
|
210
317
|
}
|
|
318
|
+
terminalSubject(transferId) {
|
|
319
|
+
return `${this.subjectPrefix}.${this.environment}.events.${this.keyPrefix}.${transferId}.terminal`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function parseTerminalEvent(value, transferId) {
|
|
323
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
324
|
+
throw new Error("invalid transfer terminal event");
|
|
325
|
+
const event = value;
|
|
326
|
+
if (event.schema_version !== TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION ||
|
|
327
|
+
event.producer !== "transfer-runtime" ||
|
|
328
|
+
event.transfer_id !== transferId ||
|
|
329
|
+
!["completed", "failed", "cancelled"].includes(String(event.status)) ||
|
|
330
|
+
typeof event.occurred_at !== "string") {
|
|
331
|
+
throw new Error("invalid transfer terminal event");
|
|
332
|
+
}
|
|
333
|
+
return event;
|
|
211
334
|
}
|
|
212
335
|
const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
|
|
213
336
|
"part_number",
|
|
@@ -218,9 +341,7 @@ const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
|
|
|
218
341
|
"copy_url",
|
|
219
342
|
"copy_source",
|
|
220
343
|
"copy_source_range",
|
|
221
|
-
"delete_url"
|
|
222
|
-
"part_verify_url",
|
|
223
|
-
"list_page_url"
|
|
344
|
+
"delete_url"
|
|
224
345
|
]);
|
|
225
346
|
const ROUTE_IDENTITY_METADATA_KEYS = [
|
|
226
347
|
"source_id",
|
|
@@ -232,8 +353,6 @@ const ROUTE_IDENTITY_METADATA_KEYS = [
|
|
|
232
353
|
export function compactSignedRoutes(routes) {
|
|
233
354
|
const source_chunks = [];
|
|
234
355
|
const sourceRefs = new Map();
|
|
235
|
-
const multipart_groups = [];
|
|
236
|
-
const groupRefs = new Map();
|
|
237
356
|
const compactRoutes = [];
|
|
238
357
|
for (const [routeIndex, route] of routes.entries()) {
|
|
239
358
|
const sourceKey = JSON.stringify([
|
|
@@ -264,21 +383,12 @@ export function compactSignedRoutes(routes) {
|
|
|
264
383
|
const deliveryIndex = route.delivery_index ?? nonNegativeInteger(metadata.delivery_index) ?? routeIndex;
|
|
265
384
|
for (const key of ROUTE_IDENTITY_METADATA_KEYS)
|
|
266
385
|
delete metadata[key];
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
}
|
|
386
|
+
const multipartGroupId = typeof metadata.multipart_group_id === "string"
|
|
387
|
+
? metadata.multipart_group_id
|
|
388
|
+
: undefined;
|
|
389
|
+
for (const key of Object.keys(metadata)) {
|
|
390
|
+
if (!ROUTE_ATTEMPT_METADATA_KEYS.has(key))
|
|
391
|
+
delete metadata[key];
|
|
282
392
|
}
|
|
283
393
|
compactRoutes.push(compact({
|
|
284
394
|
source_ref: sourceRef,
|
|
@@ -287,21 +397,11 @@ export function compactSignedRoutes(routes) {
|
|
|
287
397
|
dest_url: route.dest_url,
|
|
288
398
|
expires_at: route.expires_at,
|
|
289
399
|
dest_headers: route.dest_headers,
|
|
290
|
-
|
|
400
|
+
multipart_group_id: multipartGroupId,
|
|
291
401
|
metadata: Object.keys(metadata).length ? metadata : undefined
|
|
292
402
|
}));
|
|
293
403
|
}
|
|
294
|
-
return { source_chunks,
|
|
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;
|
|
404
|
+
return { source_chunks, routes: compactRoutes };
|
|
305
405
|
}
|
|
306
406
|
function nonNegativeInteger(value) {
|
|
307
407
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
@@ -340,3 +440,41 @@ function randomId() {
|
|
|
340
440
|
return cryptoApi.randomUUID();
|
|
341
441
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
342
442
|
}
|
|
443
|
+
const LIFECYCLE_REQUEST_MAX_ATTEMPTS = 3;
|
|
444
|
+
const LIFECYCLE_REQUEST_RETRY_DELAYS_MS = [150, 500];
|
|
445
|
+
function isRetryableLifecycleStatus(status) {
|
|
446
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
447
|
+
}
|
|
448
|
+
function isRetryableLifecycleError(error) {
|
|
449
|
+
const record = error && typeof error === "object" ? error : null;
|
|
450
|
+
const code = typeof record?.code === "string" ? record.code : "";
|
|
451
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
452
|
+
const combined = `${code} ${message}`.toLowerCase();
|
|
453
|
+
return [
|
|
454
|
+
"timeout",
|
|
455
|
+
"no responders",
|
|
456
|
+
"connection closed",
|
|
457
|
+
"disconnected",
|
|
458
|
+
"econnreset",
|
|
459
|
+
"econnrefused",
|
|
460
|
+
"etimedout",
|
|
461
|
+
"socket",
|
|
462
|
+
"network"
|
|
463
|
+
].some((token) => combined.includes(token));
|
|
464
|
+
}
|
|
465
|
+
async function sleepWithJitter(baseMs) {
|
|
466
|
+
const jitter = Math.floor(baseMs * (Math.random() * 0.4 - 0.2));
|
|
467
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(1, baseMs + jitter)));
|
|
468
|
+
}
|
|
469
|
+
async function stableRequestId(identity) {
|
|
470
|
+
const bytes = new TextEncoder().encode(identity);
|
|
471
|
+
let digest;
|
|
472
|
+
if (globalThis.crypto?.subtle) {
|
|
473
|
+
digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
const { createHash } = await import("node:crypto");
|
|
477
|
+
digest = new Uint8Array(createHash("sha256").update(bytes).digest());
|
|
478
|
+
}
|
|
479
|
+
return `idempotent-${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
480
|
+
}
|
|
@@ -14,16 +14,21 @@ export declare function prepareProviderSourceForPlan(source: ProviderSourceConfi
|
|
|
14
14
|
export declare function prepareProviderDestination(destination: ProviderDestinationConfig, options?: {
|
|
15
15
|
index?: number;
|
|
16
16
|
}): PreparedDestination;
|
|
17
|
-
export declare function createMultipartUpload(
|
|
17
|
+
export declare function createMultipartUpload(input: {
|
|
18
|
+
destination: ProviderDestinationConfig;
|
|
19
|
+
objectKey: string;
|
|
20
|
+
metadata: Record<string, string>;
|
|
21
|
+
}): Promise<string>;
|
|
22
|
+
export declare function signFinalObjectHead(destination: ProviderDestinationConfig, objectKey: string, expiresIn: number): Promise<string>;
|
|
18
23
|
export declare function signCompleteMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
19
24
|
export declare function signAbortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
20
|
-
export declare function signListObjects(destination: ProviderDestinationConfig, prefix: string, expiresIn: number
|
|
25
|
+
export declare function signListObjects(destination: ProviderDestinationConfig, prefix: string, expiresIn: number, options?: {
|
|
26
|
+
startAfter?: string;
|
|
27
|
+
}): Promise<string>;
|
|
21
28
|
export declare function signListMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number, options?: {
|
|
22
29
|
maxParts?: number;
|
|
23
30
|
partNumberMarker?: number;
|
|
24
31
|
}): Promise<string>;
|
|
25
|
-
export declare function listPartsPageMarker(partNumber: number, pageSize?: number): number | undefined;
|
|
26
|
-
export declare function listPartsVerifyMarker(partNumber: number): number | undefined;
|
|
27
32
|
export declare function signUploadPartCopy(destination: ProviderDestinationConfig, finalObjectKey: string, stagingObjectKey: string, uploadId: string, partNumber: number, expiresIn: number, copySourceRange?: string): Promise<{
|
|
28
33
|
copyUrl: string;
|
|
29
34
|
copySource: string;
|
|
@@ -36,20 +41,10 @@ export declare function signDestinationRoute(input: {
|
|
|
36
41
|
source?: ProviderSourceConfig;
|
|
37
42
|
destination: ProviderDestinationConfig;
|
|
38
43
|
expiresIn: number;
|
|
39
|
-
uploadId?: string;
|
|
40
44
|
partNumber?: number;
|
|
41
|
-
multipartObjectKey?: string;
|
|
42
|
-
completeUrl?: string;
|
|
43
|
-
abortUrl?: string;
|
|
44
|
-
listUrl?: string;
|
|
45
|
-
transferId?: string;
|
|
46
45
|
multipartGroupId?: string;
|
|
47
|
-
|
|
48
|
-
commitMethod?: "upload_part" | "upload_part_copy";
|
|
49
|
-
partVerifyUrl?: string;
|
|
50
|
-
listPageUrl?: string;
|
|
46
|
+
commitMethod?: "upload_part_copy";
|
|
51
47
|
stagingObjectKey?: string;
|
|
52
|
-
stagingListUrl?: string;
|
|
53
48
|
copyUrl?: string;
|
|
54
49
|
copySource?: string;
|
|
55
50
|
copySourceRange?: string;
|