@beam-network/sdk 0.2.23 → 0.4.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 +6 -6
- package/dist/client.js +765 -286
- package/dist/models.d.ts +62 -11
- package/dist/multipart-limits.d.ts +3 -4
- package/dist/multipart-limits.js +8 -9
- package/dist/nats-control.d.ts +32 -44
- package/dist/nats-control.js +287 -133
- package/dist/provider-signing.d.ts +10 -15
- package/dist/provider-signing.js +50 -59
- 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/v4";
|
|
4
4
|
export const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
|
|
5
5
|
export const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
|
|
6
6
|
// NATS enforces max_payload per message. This guard splits signed-route control
|
|
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,16 +98,123 @@ 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
|
-
const
|
|
217
|
+
const encodedBytes = (candidate) => {
|
|
138
218
|
const encoded = encode({
|
|
139
219
|
schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
140
220
|
environment: this.environment,
|
|
@@ -147,67 +227,126 @@ export class BeamTransferControl {
|
|
|
147
227
|
producer: "sdk",
|
|
148
228
|
payload: { ...basePayload, route_batch: compactSignedRoutes(candidate) }
|
|
149
229
|
});
|
|
150
|
-
|
|
151
|
-
return [candidate];
|
|
152
|
-
if (candidate.length === 1) {
|
|
153
|
-
throw new Error(`single signed route is ${encoded.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
154
|
-
}
|
|
155
|
-
const midpoint = Math.ceil(candidate.length / 2);
|
|
156
|
-
return [...split(candidate.slice(0, midpoint)), ...split(candidate.slice(midpoint))];
|
|
230
|
+
return encoded.byteLength;
|
|
157
231
|
};
|
|
158
|
-
|
|
232
|
+
const chunks = [];
|
|
233
|
+
let offset = 0;
|
|
234
|
+
while (offset < routes.length) {
|
|
235
|
+
let low = 1;
|
|
236
|
+
let high = routes.length - offset;
|
|
237
|
+
let accepted = 0;
|
|
238
|
+
while (low <= high) {
|
|
239
|
+
const count = Math.floor((low + high) / 2);
|
|
240
|
+
if (encodedBytes(routes.slice(offset, offset + count)) <= this.maxPayloadBytes) {
|
|
241
|
+
accepted = count;
|
|
242
|
+
low = count + 1;
|
|
243
|
+
}
|
|
244
|
+
else
|
|
245
|
+
high = count - 1;
|
|
246
|
+
}
|
|
247
|
+
if (accepted === 0) {
|
|
248
|
+
const bytes = encodedBytes(routes.slice(offset, offset + 1));
|
|
249
|
+
throw new Error(`single signed route is ${bytes} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
250
|
+
}
|
|
251
|
+
chunks.push(routes.slice(offset, offset + accepted));
|
|
252
|
+
offset += accepted;
|
|
253
|
+
}
|
|
254
|
+
return chunks;
|
|
159
255
|
}
|
|
160
256
|
async connection() {
|
|
257
|
+
if (this.closed)
|
|
258
|
+
throw new Error("NATS lifecycle control is closed");
|
|
161
259
|
if (this.nc && !this.nc.isClosed())
|
|
162
260
|
return this.nc;
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
261
|
+
if (this.connectionPromise)
|
|
262
|
+
return this.connectionPromise;
|
|
263
|
+
const pending = (async () => {
|
|
264
|
+
const nc = await connect(buildConnectionOptions({
|
|
265
|
+
natsUrl: this.natsUrl,
|
|
266
|
+
apiKey: this.apiKey,
|
|
267
|
+
keyPrefix: this.keyPrefix
|
|
268
|
+
}));
|
|
269
|
+
if (this.closed) {
|
|
270
|
+
await nc.drain();
|
|
271
|
+
throw new Error("NATS lifecycle control is closed");
|
|
272
|
+
}
|
|
273
|
+
this.nc = nc;
|
|
274
|
+
return nc;
|
|
275
|
+
})();
|
|
276
|
+
this.connectionPromise = pending;
|
|
277
|
+
try {
|
|
278
|
+
return await pending;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
if (this.connectionPromise === pending)
|
|
282
|
+
this.connectionPromise = null;
|
|
283
|
+
}
|
|
169
284
|
}
|
|
170
285
|
async resolveAuthToken() {
|
|
171
286
|
const now = Math.floor(Date.now() / 1000);
|
|
172
287
|
if (this.authToken && this.authTokenExpiresAt - 5 > now)
|
|
173
288
|
return this.authToken;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
289
|
+
if (this.authResolvePromise)
|
|
290
|
+
return this.authResolvePromise;
|
|
291
|
+
const pending = this.resolveAuthTokenFresh();
|
|
292
|
+
this.authResolvePromise = pending;
|
|
293
|
+
try {
|
|
294
|
+
return await pending;
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
if (this.authResolvePromise === pending)
|
|
298
|
+
this.authResolvePromise = null;
|
|
179
299
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
300
|
+
}
|
|
301
|
+
async resolveAuthTokenFresh() {
|
|
302
|
+
let lastError;
|
|
303
|
+
for (let attempt = 0; attempt < LIFECYCLE_REQUEST_MAX_ATTEMPTS; attempt += 1) {
|
|
304
|
+
try {
|
|
305
|
+
const nc = await this.connection();
|
|
306
|
+
const response = await nc.request(this.authSubject(), new TextEncoder().encode("{}"), { timeout: this.requestTimeoutMs });
|
|
307
|
+
const parsed = JSON.parse(new TextDecoder().decode(response.data));
|
|
308
|
+
if (!parsed.ok || !parsed.token) {
|
|
309
|
+
throw new Error(`Beam SDK NATS auth resolve failed: ${parsed.error ?? "unknown_error"}`);
|
|
310
|
+
}
|
|
311
|
+
const claims = decodeJwtPayload(parsed.token);
|
|
312
|
+
this.authToken = parsed.token;
|
|
313
|
+
this.authTokenExpiresAt = claims.exp;
|
|
314
|
+
return parsed.token;
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (!isRetryableLifecycleError(error) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
lastError = error;
|
|
321
|
+
if (this.nc?.isClosed())
|
|
322
|
+
this.nc = null;
|
|
323
|
+
await sleepWithJitter(LIFECYCLE_REQUEST_RETRY_DELAYS_MS[attempt] ?? 1_000);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw lastError instanceof Error ? lastError : new Error("Beam SDK NATS auth resolve failed after retries");
|
|
184
327
|
}
|
|
185
328
|
authSubject() {
|
|
186
329
|
return `${this.subjectPrefix}.${this.environment}.auth.${this.keyPrefix}.resolve`;
|
|
187
330
|
}
|
|
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
331
|
requestSubject(messageType, shardId) {
|
|
209
332
|
return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.shard.${shardId}.${messageType.replace(/\./g, "_")}`;
|
|
210
333
|
}
|
|
334
|
+
terminalSubject(transferId) {
|
|
335
|
+
return `${this.subjectPrefix}.${this.environment}.events.${this.keyPrefix}.${transferId}.terminal`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function parseTerminalEvent(value, transferId) {
|
|
339
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
340
|
+
throw new Error("invalid transfer terminal event");
|
|
341
|
+
const event = value;
|
|
342
|
+
if (event.schema_version !== TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION ||
|
|
343
|
+
event.producer !== "transfer-runtime" ||
|
|
344
|
+
event.transfer_id !== transferId ||
|
|
345
|
+
!["completed", "failed", "cancelled"].includes(String(event.status)) ||
|
|
346
|
+
typeof event.occurred_at !== "string") {
|
|
347
|
+
throw new Error("invalid transfer terminal event");
|
|
348
|
+
}
|
|
349
|
+
return event;
|
|
211
350
|
}
|
|
212
351
|
const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
|
|
213
352
|
"part_number",
|
|
@@ -218,9 +357,7 @@ const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
|
|
|
218
357
|
"copy_url",
|
|
219
358
|
"copy_source",
|
|
220
359
|
"copy_source_range",
|
|
221
|
-
"delete_url"
|
|
222
|
-
"part_verify_url",
|
|
223
|
-
"list_page_url"
|
|
360
|
+
"delete_url"
|
|
224
361
|
]);
|
|
225
362
|
const ROUTE_IDENTITY_METADATA_KEYS = [
|
|
226
363
|
"source_id",
|
|
@@ -232,8 +369,6 @@ const ROUTE_IDENTITY_METADATA_KEYS = [
|
|
|
232
369
|
export function compactSignedRoutes(routes) {
|
|
233
370
|
const source_chunks = [];
|
|
234
371
|
const sourceRefs = new Map();
|
|
235
|
-
const multipart_groups = [];
|
|
236
|
-
const groupRefs = new Map();
|
|
237
372
|
const compactRoutes = [];
|
|
238
373
|
for (const [routeIndex, route] of routes.entries()) {
|
|
239
374
|
const sourceKey = JSON.stringify([
|
|
@@ -264,21 +399,12 @@ export function compactSignedRoutes(routes) {
|
|
|
264
399
|
const deliveryIndex = route.delivery_index ?? nonNegativeInteger(metadata.delivery_index) ?? routeIndex;
|
|
265
400
|
for (const key of ROUTE_IDENTITY_METADATA_KEYS)
|
|
266
401
|
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
|
-
}
|
|
402
|
+
const multipartGroupId = typeof metadata.multipart_group_id === "string"
|
|
403
|
+
? metadata.multipart_group_id
|
|
404
|
+
: undefined;
|
|
405
|
+
for (const key of Object.keys(metadata)) {
|
|
406
|
+
if (!ROUTE_ATTEMPT_METADATA_KEYS.has(key))
|
|
407
|
+
delete metadata[key];
|
|
282
408
|
}
|
|
283
409
|
compactRoutes.push(compact({
|
|
284
410
|
source_ref: sourceRef,
|
|
@@ -287,21 +413,11 @@ export function compactSignedRoutes(routes) {
|
|
|
287
413
|
dest_url: route.dest_url,
|
|
288
414
|
expires_at: route.expires_at,
|
|
289
415
|
dest_headers: route.dest_headers,
|
|
290
|
-
|
|
416
|
+
multipart_group_id: multipartGroupId,
|
|
291
417
|
metadata: Object.keys(metadata).length ? metadata : undefined
|
|
292
418
|
}));
|
|
293
419
|
}
|
|
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;
|
|
420
|
+
return { source_chunks, routes: compactRoutes };
|
|
305
421
|
}
|
|
306
422
|
function nonNegativeInteger(value) {
|
|
307
423
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
@@ -340,3 +456,41 @@ function randomId() {
|
|
|
340
456
|
return cryptoApi.randomUUID();
|
|
341
457
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
342
458
|
}
|
|
459
|
+
const LIFECYCLE_REQUEST_MAX_ATTEMPTS = 3;
|
|
460
|
+
const LIFECYCLE_REQUEST_RETRY_DELAYS_MS = [150, 500];
|
|
461
|
+
function isRetryableLifecycleStatus(status) {
|
|
462
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
463
|
+
}
|
|
464
|
+
function isRetryableLifecycleError(error) {
|
|
465
|
+
const record = error && typeof error === "object" ? error : null;
|
|
466
|
+
const code = typeof record?.code === "string" ? record.code : "";
|
|
467
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
468
|
+
const combined = `${code} ${message}`.toLowerCase();
|
|
469
|
+
return [
|
|
470
|
+
"timeout",
|
|
471
|
+
"no responders",
|
|
472
|
+
"connection closed",
|
|
473
|
+
"disconnected",
|
|
474
|
+
"econnreset",
|
|
475
|
+
"econnrefused",
|
|
476
|
+
"etimedout",
|
|
477
|
+
"socket",
|
|
478
|
+
"network"
|
|
479
|
+
].some((token) => combined.includes(token));
|
|
480
|
+
}
|
|
481
|
+
async function sleepWithJitter(baseMs) {
|
|
482
|
+
const jitter = Math.floor(baseMs * (Math.random() * 0.4 - 0.2));
|
|
483
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(1, baseMs + jitter)));
|
|
484
|
+
}
|
|
485
|
+
async function stableRequestId(identity) {
|
|
486
|
+
const bytes = new TextEncoder().encode(identity);
|
|
487
|
+
let digest;
|
|
488
|
+
if (globalThis.crypto?.subtle) {
|
|
489
|
+
digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const { createHash } = await import("node:crypto");
|
|
493
|
+
digest = new Uint8Array(createHash("sha256").update(bytes).digest());
|
|
494
|
+
}
|
|
495
|
+
return `idempotent-${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
496
|
+
}
|
|
@@ -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;
|