@miosa/sdk 3.3.1 → 3.4.1
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/dist/index.d.ts +194 -11
- package/dist/index.js +306 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual, randomUUID } from 'crypto';
|
|
2
2
|
import EventEmitter from 'events';
|
|
3
3
|
import { isIP } from 'net';
|
|
4
|
-
import {
|
|
4
|
+
import { lookup } from 'dns/promises';
|
|
5
5
|
import * as http2 from 'http2';
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
|
|
@@ -30,7 +30,7 @@ var MiosaError = class _MiosaError extends Error {
|
|
|
30
30
|
* its own backoff policy.
|
|
31
31
|
*/
|
|
32
32
|
retryAfterMs;
|
|
33
|
-
constructor(message, status, code, details, requestId, retryable = defaultRetryable(status),
|
|
33
|
+
constructor(message, status, code, details, requestId, retryable = defaultRetryable(status), retryAfterMs2) {
|
|
34
34
|
super(message);
|
|
35
35
|
this.name = "MiosaError";
|
|
36
36
|
this.status = status;
|
|
@@ -38,7 +38,7 @@ var MiosaError = class _MiosaError extends Error {
|
|
|
38
38
|
this.details = details;
|
|
39
39
|
this.requestId = requestId;
|
|
40
40
|
this.retryable = retryable;
|
|
41
|
-
this.retryAfterMs =
|
|
41
|
+
this.retryAfterMs = retryAfterMs2;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* @param retryAfterHeaderMs The `Retry-After` header value, already
|
|
@@ -54,16 +54,16 @@ var MiosaError = class _MiosaError extends Error {
|
|
|
54
54
|
const details = nested?.details ?? body5.details ?? (body5.detail || body5.reason ? { detail: body5.detail, reason: body5.reason } : void 0);
|
|
55
55
|
requestId ??= nested?.request_id ?? body5.request_id;
|
|
56
56
|
const retryable = nested?.retryable ?? body5.retryable ?? defaultRetryable(status);
|
|
57
|
-
const
|
|
57
|
+
const retryAfterMs2 = nested?.retry_after_ms ?? body5.retry_after_ms ?? retryAfterHeaderMs2;
|
|
58
58
|
const connectError = connectErrorFromCode(code, message, status, details, requestId, retryable);
|
|
59
59
|
const built = connectError ?? (status === 401 || status === 403 ? new AuthError(message, status, code, details, requestId, retryable) : status === 404 ? new NotFoundError(message, code, details, requestId, retryable) : status === 429 ? new RateLimitError(
|
|
60
60
|
message,
|
|
61
61
|
details,
|
|
62
62
|
requestId,
|
|
63
|
-
|
|
63
|
+
retryAfterMs2 === void 0 ? void 0 : retryAfterMs2 / 1e3,
|
|
64
64
|
retryable
|
|
65
65
|
) : status === 402 ? new InsufficientCreditsError(message, details, requestId, retryable) : status >= 400 && status < 500 ? new ValidationError(message, status, code, details, requestId, retryable) : new _MiosaError(message, status, code, details, requestId, retryable));
|
|
66
|
-
built.retryAfterMs =
|
|
66
|
+
built.retryAfterMs = retryAfterMs2;
|
|
67
67
|
return built;
|
|
68
68
|
}
|
|
69
69
|
};
|
|
@@ -185,7 +185,7 @@ var TokenRefreshFailedError = class extends MiosaError {
|
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
// src/version.ts
|
|
188
|
-
var SDK_VERSION = "3.
|
|
188
|
+
var SDK_VERSION = "3.4.1";
|
|
189
189
|
var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
|
|
190
190
|
|
|
191
191
|
// src/http.ts
|
|
@@ -214,7 +214,11 @@ async function ensureHttp2Agent() {
|
|
|
214
214
|
_h2Available = false;
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
var _h2Bootstrap;
|
|
218
|
+
function startHttp2Agent() {
|
|
219
|
+
_h2Bootstrap ??= ensureHttp2Agent();
|
|
220
|
+
return _h2Bootstrap;
|
|
221
|
+
}
|
|
218
222
|
var DEFAULT_TIMEOUT = 3e4;
|
|
219
223
|
var DEFAULT_MAX_RETRIES = 3;
|
|
220
224
|
var BASE_DELAY_MS = 500;
|
|
@@ -293,6 +297,7 @@ var HttpClient = class {
|
|
|
293
297
|
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
294
298
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
295
299
|
try {
|
|
300
|
+
await startHttp2Agent();
|
|
296
301
|
const response = await fetch(this.buildUrl(path), {
|
|
297
302
|
method,
|
|
298
303
|
headers,
|
|
@@ -303,7 +308,7 @@ var HttpClient = class {
|
|
|
303
308
|
clearTimeout(timer);
|
|
304
309
|
if (!response.ok) {
|
|
305
310
|
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
306
|
-
const
|
|
311
|
+
const retryAfterMs2 = retryAfterHeaderMs(
|
|
307
312
|
response.headers.get("retry-after")
|
|
308
313
|
);
|
|
309
314
|
let errorBody = {};
|
|
@@ -315,7 +320,7 @@ var HttpClient = class {
|
|
|
315
320
|
response.status,
|
|
316
321
|
errorBody,
|
|
317
322
|
requestId,
|
|
318
|
-
|
|
323
|
+
retryAfterMs2
|
|
319
324
|
);
|
|
320
325
|
if (error.retryable && attempt < maxRetries) {
|
|
321
326
|
lastError = error;
|
|
@@ -430,6 +435,7 @@ var HttpClient = class {
|
|
|
430
435
|
try {
|
|
431
436
|
let response;
|
|
432
437
|
try {
|
|
438
|
+
await startHttp2Agent();
|
|
433
439
|
response = await fetch(this.buildUrl(path), {
|
|
434
440
|
method,
|
|
435
441
|
headers,
|
|
@@ -11208,24 +11214,36 @@ function sandboxTag(sandboxId) {
|
|
|
11208
11214
|
}
|
|
11209
11215
|
return first.toLowerCase();
|
|
11210
11216
|
}
|
|
11217
|
+
async function lookupAllIpv4(hostname) {
|
|
11218
|
+
const entries = await lookup(hostname, { family: 4, all: true });
|
|
11219
|
+
return entries.map((entry) => entry.address);
|
|
11220
|
+
}
|
|
11221
|
+
function dedupe(addresses) {
|
|
11222
|
+
return [...new Set(addresses)];
|
|
11223
|
+
}
|
|
11211
11224
|
var DnsAddressResolver = class {
|
|
11212
|
-
constructor(ttlMs = 6e4) {
|
|
11225
|
+
constructor(ttlMs = 6e4, lookupAddresses = lookupAllIpv4) {
|
|
11213
11226
|
this.ttlMs = ttlMs;
|
|
11227
|
+
this.lookupAddresses = lookupAddresses;
|
|
11214
11228
|
}
|
|
11215
11229
|
ttlMs;
|
|
11230
|
+
lookupAddresses;
|
|
11216
11231
|
cache = /* @__PURE__ */ new Map();
|
|
11217
11232
|
async resolve(hostname) {
|
|
11218
11233
|
if (isIP(hostname) !== 0) return [hostname];
|
|
11219
11234
|
if (hostname === "localhost") return ["127.0.0.1"];
|
|
11220
11235
|
const cached = this.cache.get(hostname);
|
|
11221
11236
|
if (cached && cached.expiresAt > Date.now()) return cached.addresses;
|
|
11222
|
-
const addresses = await
|
|
11237
|
+
const addresses = dedupe(await this.lookupAddresses(hostname));
|
|
11223
11238
|
if (addresses.length === 0) {
|
|
11224
11239
|
throw new Error(`DnsAddressResolver: ${hostname} resolved to zero A records`);
|
|
11225
11240
|
}
|
|
11226
11241
|
this.cache.set(hostname, { addresses, expiresAt: Date.now() + this.ttlMs });
|
|
11227
11242
|
return addresses;
|
|
11228
11243
|
}
|
|
11244
|
+
invalidate(hostname) {
|
|
11245
|
+
this.cache.delete(hostname);
|
|
11246
|
+
}
|
|
11229
11247
|
};
|
|
11230
11248
|
var StaticAddressResolver = class {
|
|
11231
11249
|
constructor(table) {
|
|
@@ -11273,7 +11291,7 @@ function markHandled(promise) {
|
|
|
11273
11291
|
promise.catch(() => void 0);
|
|
11274
11292
|
return promise;
|
|
11275
11293
|
}
|
|
11276
|
-
var DEFAULT_SESSIONS_PER_ADDRESS =
|
|
11294
|
+
var DEFAULT_SESSIONS_PER_ADDRESS = 2;
|
|
11277
11295
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
11278
11296
|
var Http2Transport = class {
|
|
11279
11297
|
protocol = "h2";
|
|
@@ -11282,13 +11300,25 @@ var Http2Transport = class {
|
|
|
11282
11300
|
port;
|
|
11283
11301
|
resolver;
|
|
11284
11302
|
defaultTimeoutMs;
|
|
11303
|
+
pinAddresses;
|
|
11304
|
+
insecure;
|
|
11305
|
+
dial;
|
|
11285
11306
|
pools = /* @__PURE__ */ new Map();
|
|
11307
|
+
/**
|
|
11308
|
+
* Requests in flight per session. A pooled session is unref'd while idle so
|
|
11309
|
+
* a process that only imported the client (or finished its last request)
|
|
11310
|
+
* still exits; it is ref'd again for exactly as long as it carries a request.
|
|
11311
|
+
*/
|
|
11312
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
11286
11313
|
constructor(options = {}) {
|
|
11287
11314
|
this.defaultSessionsPerAddress = options.defaultSessionsPerAddress ?? DEFAULT_SESSIONS_PER_ADDRESS;
|
|
11288
11315
|
this.tls = options.tls ?? true;
|
|
11289
11316
|
this.port = options.port ?? (this.tls ? 443 : 80);
|
|
11290
11317
|
this.resolver = options.resolver ?? new DnsAddressResolver();
|
|
11291
11318
|
this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11319
|
+
this.pinAddresses = options.pinAddresses ?? true;
|
|
11320
|
+
this.insecure = options.insecure ?? false;
|
|
11321
|
+
this.dial = options.connect ?? http2.connect;
|
|
11292
11322
|
}
|
|
11293
11323
|
prewarm(regionHostname2, sessionsPerAddress) {
|
|
11294
11324
|
this.poolFor(regionHostname2, sessionsPerAddress);
|
|
@@ -11328,6 +11358,7 @@ var Http2Transport = class {
|
|
|
11328
11358
|
try {
|
|
11329
11359
|
session = await group[cursor];
|
|
11330
11360
|
} catch (error) {
|
|
11361
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11331
11362
|
throw new RunnerConnectError(regionHostname2, address, error);
|
|
11332
11363
|
}
|
|
11333
11364
|
if (session.closed || session.destroyed) {
|
|
@@ -11336,6 +11367,7 @@ var Http2Transport = class {
|
|
|
11336
11367
|
try {
|
|
11337
11368
|
session = await fresh;
|
|
11338
11369
|
} catch (error) {
|
|
11370
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11339
11371
|
throw new RunnerConnectError(regionHostname2, address, error);
|
|
11340
11372
|
}
|
|
11341
11373
|
}
|
|
@@ -11366,28 +11398,104 @@ var Http2Transport = class {
|
|
|
11366
11398
|
return pool;
|
|
11367
11399
|
}
|
|
11368
11400
|
buildPool(regionHostname2, sessionsPerAddress) {
|
|
11369
|
-
const
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11401
|
+
const pool = {
|
|
11402
|
+
addresses: this.resolveAddresses(regionHostname2),
|
|
11403
|
+
groups: Promise.resolve([]),
|
|
11404
|
+
nextSlot: /* @__PURE__ */ new Map(),
|
|
11405
|
+
sessionsPerAddress,
|
|
11406
|
+
failed: /* @__PURE__ */ new Set(),
|
|
11407
|
+
rebuilding: void 0
|
|
11408
|
+
};
|
|
11409
|
+
pool.groups = pool.addresses.then(
|
|
11410
|
+
(resolved) => resolved.map((address) => this.openGroup(regionHostname2, address, sessionsPerAddress))
|
|
11411
|
+
);
|
|
11412
|
+
markHandled(pool.addresses);
|
|
11413
|
+
markHandled(pool.groups);
|
|
11414
|
+
return pool;
|
|
11415
|
+
}
|
|
11416
|
+
/**
|
|
11417
|
+
* The addresses this pool pins to. With pinning on, every IPv4 address
|
|
11418
|
+
* the hostname resolves to; with it off, the hostname itself - one entry,
|
|
11419
|
+
* dialled as-is so the OS resolver chooses.
|
|
11420
|
+
*/
|
|
11421
|
+
resolveAddresses(regionHostname2) {
|
|
11422
|
+
return this.pinAddresses ? this.resolver.resolve(regionHostname2) : Promise.resolve([regionHostname2]);
|
|
11423
|
+
}
|
|
11424
|
+
/**
|
|
11425
|
+
* Opens `sessionsPerAddress` sessions against one address, in parallel.
|
|
11426
|
+
* sessionsPerAddress: 0 is a deliberate "open nothing" (e.g. a count-only
|
|
11427
|
+
* probe).
|
|
11428
|
+
*/
|
|
11429
|
+
openGroup(regionHostname2, address, sessionsPerAddress) {
|
|
11430
|
+
return Array.from(
|
|
11431
|
+
{ length: sessionsPerAddress },
|
|
11432
|
+
() => markHandled(this.connectSession(regionHostname2, address))
|
|
11433
|
+
);
|
|
11434
|
+
}
|
|
11435
|
+
/** Closes a session slot once it connects, ignoring one that never did. */
|
|
11436
|
+
closeSlot(slot) {
|
|
11437
|
+
slot.then((session) => {
|
|
11438
|
+
if (!session.closed) session.close();
|
|
11439
|
+
}).catch(() => void 0);
|
|
11440
|
+
}
|
|
11441
|
+
/**
|
|
11442
|
+
* A session pinned to `address` failed to connect. Mark that address's
|
|
11443
|
+
* group for reopening and re-resolve in the background (single-flight),
|
|
11444
|
+
* so one dead address never costs every concurrent failure its own DNS
|
|
11445
|
+
* lookup. Never blocks or fails the request that is already on its way
|
|
11446
|
+
* out - the caller retries another index (C6's next-IP retry).
|
|
11447
|
+
*/
|
|
11448
|
+
noteAddressFailure(regionHostname2, address) {
|
|
11449
|
+
if (!this.pinAddresses) return;
|
|
11450
|
+
const pool = this.pools.get(regionHostname2);
|
|
11451
|
+
if (!pool) return;
|
|
11452
|
+
pool.failed.add(address);
|
|
11453
|
+
this.resolver.invalidate?.(regionHostname2);
|
|
11454
|
+
if (pool.rebuilding) return;
|
|
11455
|
+
pool.rebuilding = this.rebuildPool(regionHostname2, pool).finally(() => {
|
|
11456
|
+
pool.rebuilding = void 0;
|
|
11457
|
+
});
|
|
11458
|
+
}
|
|
11459
|
+
/**
|
|
11460
|
+
* Re-resolves `regionHostname` and redistributes the pool over the fresh
|
|
11461
|
+
* addresses: the failed address's sessions are dropped and reopened, a
|
|
11462
|
+
* newly advertised address gets a group of its own, an address DNS no
|
|
11463
|
+
* longer lists loses its sessions, and every healthy address keeps the
|
|
11464
|
+
* sessions it already has - so a failure on one host never costs the rest
|
|
11465
|
+
* their warm connections.
|
|
11466
|
+
*/
|
|
11467
|
+
async rebuildPool(regionHostname2, pool) {
|
|
11468
|
+
const failed = new Set(pool.failed);
|
|
11469
|
+
pool.failed.clear();
|
|
11470
|
+
const resolved = await this.resolveAddresses(regionHostname2).catch(() => void 0);
|
|
11471
|
+
if (!resolved) {
|
|
11472
|
+
for (const address of failed) pool.failed.add(address);
|
|
11473
|
+
return;
|
|
11474
|
+
}
|
|
11475
|
+
const previous = await pool.addresses.catch(() => []);
|
|
11476
|
+
const previousGroups = await pool.groups.catch(
|
|
11477
|
+
() => []
|
|
11381
11478
|
);
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11479
|
+
const groups = resolved.map((address) => {
|
|
11480
|
+
const index = previous.indexOf(address);
|
|
11481
|
+
const reusable = index >= 0 && !failed.has(address) ? previousGroups[index] : void 0;
|
|
11482
|
+
if (reusable) return reusable;
|
|
11483
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11484
|
+
return this.openGroup(regionHostname2, address, pool.sessionsPerAddress);
|
|
11485
|
+
});
|
|
11486
|
+
previous.forEach((address, index) => {
|
|
11487
|
+
if (resolved.includes(address)) return;
|
|
11488
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11489
|
+
});
|
|
11490
|
+
pool.addresses = Promise.resolve(resolved);
|
|
11491
|
+
pool.groups = Promise.resolve(groups);
|
|
11492
|
+
pool.nextSlot.clear();
|
|
11385
11493
|
}
|
|
11386
11494
|
connectSession(regionHostname2, address) {
|
|
11387
11495
|
return new Promise((resolve, reject) => {
|
|
11388
11496
|
const authority = `${this.tls ? "https" : "http"}://${address}:${this.port}`;
|
|
11389
|
-
const connectOptions = this.tls ? { servername: regionHostname2 } : {};
|
|
11390
|
-
const session =
|
|
11497
|
+
const connectOptions = this.tls ? { servername: regionHostname2, rejectUnauthorized: !this.insecure } : {};
|
|
11498
|
+
const session = this.dial(authority, connectOptions);
|
|
11391
11499
|
const onError = (error) => {
|
|
11392
11500
|
session.destroy();
|
|
11393
11501
|
reject(error);
|
|
@@ -11395,11 +11503,26 @@ var Http2Transport = class {
|
|
|
11395
11503
|
session.once("error", onError);
|
|
11396
11504
|
session.once("connect", () => {
|
|
11397
11505
|
session.removeListener("error", onError);
|
|
11506
|
+
session.unref();
|
|
11398
11507
|
session.on("error", () => void 0);
|
|
11399
11508
|
resolve(session);
|
|
11400
11509
|
});
|
|
11401
11510
|
});
|
|
11402
11511
|
}
|
|
11512
|
+
retain(session) {
|
|
11513
|
+
const n = this.inFlight.get(session) ?? 0;
|
|
11514
|
+
this.inFlight.set(session, n + 1);
|
|
11515
|
+
if (n === 0) session.ref();
|
|
11516
|
+
}
|
|
11517
|
+
release(session) {
|
|
11518
|
+
const n = (this.inFlight.get(session) ?? 1) - 1;
|
|
11519
|
+
if (n > 0) {
|
|
11520
|
+
this.inFlight.set(session, n);
|
|
11521
|
+
return;
|
|
11522
|
+
}
|
|
11523
|
+
this.inFlight.delete(session);
|
|
11524
|
+
session.unref();
|
|
11525
|
+
}
|
|
11403
11526
|
sendRequest(regionHostname2, authority, path, session, init) {
|
|
11404
11527
|
const timeoutMs = init.timeoutMs ?? this.defaultTimeoutMs;
|
|
11405
11528
|
return new Promise((resolve, reject) => {
|
|
@@ -11409,6 +11532,7 @@ var Http2Transport = class {
|
|
|
11409
11532
|
":authority": authority,
|
|
11410
11533
|
...init.headers
|
|
11411
11534
|
};
|
|
11535
|
+
this.retain(session);
|
|
11412
11536
|
const stream = session.request(requestHeaders);
|
|
11413
11537
|
let status = 0;
|
|
11414
11538
|
const responseHeaders = {};
|
|
@@ -11417,12 +11541,14 @@ var Http2Transport = class {
|
|
|
11417
11541
|
const timer = setTimeout(() => {
|
|
11418
11542
|
if (settled) return;
|
|
11419
11543
|
settled = true;
|
|
11544
|
+
this.release(session);
|
|
11420
11545
|
stream.close(http2.constants.NGHTTP2_CANCEL);
|
|
11421
11546
|
reject(new RunnerTimeoutError(regionHostname2, path, timeoutMs));
|
|
11422
11547
|
}, timeoutMs);
|
|
11423
11548
|
const onAbort = () => {
|
|
11424
11549
|
if (settled) return;
|
|
11425
11550
|
settled = true;
|
|
11551
|
+
this.release(session);
|
|
11426
11552
|
clearTimeout(timer);
|
|
11427
11553
|
stream.close(http2.constants.NGHTTP2_CANCEL);
|
|
11428
11554
|
reject(new DOMException("runner request aborted", "AbortError"));
|
|
@@ -11439,6 +11565,7 @@ var Http2Transport = class {
|
|
|
11439
11565
|
stream.on("end", () => {
|
|
11440
11566
|
if (settled) return;
|
|
11441
11567
|
settled = true;
|
|
11568
|
+
this.release(session);
|
|
11442
11569
|
clearTimeout(timer);
|
|
11443
11570
|
init.signal?.removeEventListener("abort", onAbort);
|
|
11444
11571
|
resolve({ status, headers: responseHeaders, body: Buffer.concat(chunks).toString("utf8") });
|
|
@@ -11446,6 +11573,7 @@ var Http2Transport = class {
|
|
|
11446
11573
|
stream.on("error", (error) => {
|
|
11447
11574
|
if (settled) return;
|
|
11448
11575
|
settled = true;
|
|
11576
|
+
this.release(session);
|
|
11449
11577
|
clearTimeout(timer);
|
|
11450
11578
|
init.signal?.removeEventListener("abort", onAbort);
|
|
11451
11579
|
reject(error);
|
|
@@ -11560,6 +11688,8 @@ var Http3Transport = class {
|
|
|
11560
11688
|
connectTimeoutMs;
|
|
11561
11689
|
insecure;
|
|
11562
11690
|
port;
|
|
11691
|
+
pinAddresses;
|
|
11692
|
+
dial;
|
|
11563
11693
|
pools = /* @__PURE__ */ new Map();
|
|
11564
11694
|
constructor(options = {}) {
|
|
11565
11695
|
const native = loadNative();
|
|
@@ -11573,6 +11703,8 @@ var Http3Transport = class {
|
|
|
11573
11703
|
this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
11574
11704
|
this.insecure = options.insecure ?? false;
|
|
11575
11705
|
this.port = options.port ?? 443;
|
|
11706
|
+
this.pinAddresses = options.pinAddresses ?? true;
|
|
11707
|
+
this.dial = options.connect ?? ((address, servername, port, insecure, timeoutMs) => this.native.h3Connect(address, servername, port, insecure, timeoutMs));
|
|
11576
11708
|
}
|
|
11577
11709
|
prewarm(regionHostname2, sessionsPerAddress) {
|
|
11578
11710
|
this.poolFor(regionHostname2, sessionsPerAddress);
|
|
@@ -11618,6 +11750,7 @@ var Http3Transport = class {
|
|
|
11618
11750
|
try {
|
|
11619
11751
|
handle = await group[cursor];
|
|
11620
11752
|
} catch (error) {
|
|
11753
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11621
11754
|
throw new RunnerConnectError(regionHostname2, address, error);
|
|
11622
11755
|
}
|
|
11623
11756
|
const reconnect = () => {
|
|
@@ -11652,21 +11785,99 @@ var Http3Transport = class {
|
|
|
11652
11785
|
return pool;
|
|
11653
11786
|
}
|
|
11654
11787
|
buildPool(regionHostname2, sessionsPerAddress) {
|
|
11655
|
-
const
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11788
|
+
const pool = {
|
|
11789
|
+
addresses: this.resolveAddresses(regionHostname2),
|
|
11790
|
+
groups: Promise.resolve([]),
|
|
11791
|
+
nextSlot: /* @__PURE__ */ new Map(),
|
|
11792
|
+
sessionsPerAddress,
|
|
11793
|
+
failed: /* @__PURE__ */ new Set(),
|
|
11794
|
+
rebuilding: void 0
|
|
11795
|
+
};
|
|
11796
|
+
pool.groups = pool.addresses.then(
|
|
11797
|
+
(resolved) => resolved.map((address) => this.openGroup(regionHostname2, address, sessionsPerAddress))
|
|
11798
|
+
);
|
|
11799
|
+
markHandled2(pool.addresses);
|
|
11800
|
+
markHandled2(pool.groups);
|
|
11801
|
+
return pool;
|
|
11802
|
+
}
|
|
11803
|
+
/**
|
|
11804
|
+
* The addresses this pool pins to. With pinning on, every IPv4 address
|
|
11805
|
+
* the hostname resolves to. With it off, the hostname is resolved once
|
|
11806
|
+
* and only its first address is kept - one target, chosen by the OS
|
|
11807
|
+
* resolver, which is the closest this transport can get to HTTP/2's
|
|
11808
|
+
* hostname dial: the native addon takes a literal socket address, never
|
|
11809
|
+
* a hostname.
|
|
11810
|
+
*/
|
|
11811
|
+
async resolveAddresses(regionHostname2) {
|
|
11812
|
+
if (this.pinAddresses) return this.resolver.resolve(regionHostname2);
|
|
11813
|
+
const [first] = await this.resolver.resolve(regionHostname2);
|
|
11814
|
+
if (!first) {
|
|
11815
|
+
throw new Error(`Http3Transport: ${regionHostname2} resolved to zero addresses`);
|
|
11816
|
+
}
|
|
11817
|
+
return [first];
|
|
11818
|
+
}
|
|
11819
|
+
/** Opens `sessionsPerAddress` connections against one address, in parallel. */
|
|
11820
|
+
openGroup(regionHostname2, address, sessionsPerAddress) {
|
|
11821
|
+
return Array.from(
|
|
11822
|
+
{ length: sessionsPerAddress },
|
|
11823
|
+
() => markHandled2(this.connectSession(regionHostname2, address))
|
|
11663
11824
|
);
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
11825
|
+
}
|
|
11826
|
+
/** Closes a connection slot once it connects, ignoring one that never did. */
|
|
11827
|
+
closeSlot(slot) {
|
|
11828
|
+
slot.then((handle) => this.native.h3Close(handle)).catch(() => void 0);
|
|
11829
|
+
}
|
|
11830
|
+
/**
|
|
11831
|
+
* A connection pinned to `address` failed to connect. Mark that address's
|
|
11832
|
+
* group for reopening and re-resolve in the background (single-flight),
|
|
11833
|
+
* so one dead address never costs every concurrent failure its own DNS
|
|
11834
|
+
* lookup. Never blocks or fails the request already on its way out.
|
|
11835
|
+
*/
|
|
11836
|
+
noteAddressFailure(regionHostname2, address) {
|
|
11837
|
+
if (!this.pinAddresses) return;
|
|
11838
|
+
const pool = this.pools.get(regionHostname2);
|
|
11839
|
+
if (!pool) return;
|
|
11840
|
+
pool.failed.add(address);
|
|
11841
|
+
this.resolver.invalidate?.(regionHostname2);
|
|
11842
|
+
if (pool.rebuilding) return;
|
|
11843
|
+
pool.rebuilding = this.rebuildPool(regionHostname2, pool).finally(() => {
|
|
11844
|
+
pool.rebuilding = void 0;
|
|
11845
|
+
});
|
|
11846
|
+
}
|
|
11847
|
+
/**
|
|
11848
|
+
* Re-resolves `regionHostname` and redistributes the pool over the fresh
|
|
11849
|
+
* addresses: the failed address's connections are dropped and reopened, a
|
|
11850
|
+
* newly advertised address gets a group of its own, an address DNS no
|
|
11851
|
+
* longer lists loses its connections, and every healthy address keeps the
|
|
11852
|
+
* connections it already has.
|
|
11853
|
+
*/
|
|
11854
|
+
async rebuildPool(regionHostname2, pool) {
|
|
11855
|
+
const failed = new Set(pool.failed);
|
|
11856
|
+
pool.failed.clear();
|
|
11857
|
+
const resolved = await this.resolveAddresses(regionHostname2).catch(() => void 0);
|
|
11858
|
+
if (!resolved) {
|
|
11859
|
+
for (const address of failed) pool.failed.add(address);
|
|
11860
|
+
return;
|
|
11861
|
+
}
|
|
11862
|
+
const previous = await pool.addresses.catch(() => []);
|
|
11863
|
+
const previousGroups = await pool.groups.catch(() => []);
|
|
11864
|
+
const groups = resolved.map((address) => {
|
|
11865
|
+
const index = previous.indexOf(address);
|
|
11866
|
+
const reusable = index >= 0 && !failed.has(address) ? previousGroups[index] : void 0;
|
|
11867
|
+
if (reusable) return reusable;
|
|
11868
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11869
|
+
return this.openGroup(regionHostname2, address, pool.sessionsPerAddress);
|
|
11870
|
+
});
|
|
11871
|
+
previous.forEach((address, index) => {
|
|
11872
|
+
if (resolved.includes(address)) return;
|
|
11873
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11874
|
+
});
|
|
11875
|
+
pool.addresses = Promise.resolve(resolved);
|
|
11876
|
+
pool.groups = Promise.resolve(groups);
|
|
11877
|
+
pool.nextSlot.clear();
|
|
11667
11878
|
}
|
|
11668
11879
|
connectSession(regionHostname2, address) {
|
|
11669
|
-
return this.
|
|
11880
|
+
return this.dial(address, regionHostname2, this.port, this.insecure, this.connectTimeoutMs);
|
|
11670
11881
|
}
|
|
11671
11882
|
async sendRequest(regionHostname2, address, authority, path, handle, init, reconnect) {
|
|
11672
11883
|
const timeoutMs = init.timeoutMs ?? this.defaultTimeoutMs;
|
|
@@ -11694,6 +11905,7 @@ var Http3Transport = class {
|
|
|
11694
11905
|
try {
|
|
11695
11906
|
fresh = await reconnect();
|
|
11696
11907
|
} catch (reconnectError) {
|
|
11908
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11697
11909
|
throw new RunnerConnectError(regionHostname2, address, reconnectError);
|
|
11698
11910
|
}
|
|
11699
11911
|
return attempt(fresh);
|
|
@@ -11722,7 +11934,7 @@ async function* singleChunk(body5) {
|
|
|
11722
11934
|
// src/runner/transport-factory.ts
|
|
11723
11935
|
function createBestTransport(options = {}) {
|
|
11724
11936
|
const http3 = detectHttp3Support();
|
|
11725
|
-
if (!http3.available) {
|
|
11937
|
+
if (!http3.available || options.raceHttp3 !== true) {
|
|
11726
11938
|
return { transport: new Http2Transport(options.http2), http3 };
|
|
11727
11939
|
}
|
|
11728
11940
|
const transport = new RacingTransport(new Http2Transport(options.http2), new Http3Transport(options.http3));
|
|
@@ -11830,6 +12042,16 @@ var RunnerError = class _RunnerError extends Error {
|
|
|
11830
12042
|
get isBusy() {
|
|
11831
12043
|
return this.code === "runtime_busy" || this.code === "feed_stale";
|
|
11832
12044
|
}
|
|
12045
|
+
/**
|
|
12046
|
+
* `429 rate_limited`: the key's own window on this runner has no room.
|
|
12047
|
+
*
|
|
12048
|
+
* Separate from `isBusy`, which means this runner's admission is full for
|
|
12049
|
+
* everyone. The limiter counts per key per runner, so a refusal here says
|
|
12050
|
+
* nothing about the next address, and `createSandbox` may try that one.
|
|
12051
|
+
*/
|
|
12052
|
+
get isRateLimited() {
|
|
12053
|
+
return this.code === "rate_limited";
|
|
12054
|
+
}
|
|
11833
12055
|
/** C2: the runner doesn't serve this create shape at all - send it to api.miosa.ai instead. */
|
|
11834
12056
|
get isUnsupportedRequest() {
|
|
11835
12057
|
return this.code === "RUNNER_UNSUPPORTED_REQUEST";
|
|
@@ -11879,7 +12101,7 @@ function unwrapSomaEnvelope(httpStatus, body5) {
|
|
|
11879
12101
|
}
|
|
11880
12102
|
|
|
11881
12103
|
// src/runner/runner-client.ts
|
|
11882
|
-
var DEFAULT_SESSIONS_PER_ADDRESS3 =
|
|
12104
|
+
var DEFAULT_SESSIONS_PER_ADDRESS3 = 2;
|
|
11883
12105
|
function createSupportedByRunner(params) {
|
|
11884
12106
|
const hasTemplate = params["template_id"] !== void 0 || params["templateId"] !== void 0 || params["image"] !== void 0;
|
|
11885
12107
|
const hasSnapshot = params["snapshot_id"] !== void 0 || params["snapshotId"] !== void 0 || params["source_snapshot_path"] !== void 0 || params["sourceSnapshotPath"] !== void 0;
|
|
@@ -11889,6 +12111,18 @@ function createSupportedByRunner(params) {
|
|
|
11889
12111
|
const isNonXs = size !== void 0 && size !== "xs";
|
|
11890
12112
|
return !(hasTemplate || hasSnapshot || hasCwd || isPersistent || isNonXs);
|
|
11891
12113
|
}
|
|
12114
|
+
var RATE_LIMIT_RETRY_CAP_MS = 250;
|
|
12115
|
+
function retryAfterMs(headers, capMs = RATE_LIMIT_RETRY_CAP_MS) {
|
|
12116
|
+
const raw = headers["retry-after"] ?? headers["Retry-After"];
|
|
12117
|
+
if (raw === void 0) return 0;
|
|
12118
|
+
const seconds = Number(raw.trim());
|
|
12119
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, capMs);
|
|
12120
|
+
const at = Date.parse(raw);
|
|
12121
|
+
return Number.isNaN(at) ? 0 : Math.min(Math.max(0, at - Date.now()), capMs);
|
|
12122
|
+
}
|
|
12123
|
+
function sleep9(ms) {
|
|
12124
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12125
|
+
}
|
|
11892
12126
|
var RunnerClient = class {
|
|
11893
12127
|
region;
|
|
11894
12128
|
hostname;
|
|
@@ -11901,13 +12135,26 @@ var RunnerClient = class {
|
|
|
11901
12135
|
addressIndexByTag = /* @__PURE__ */ new Map();
|
|
11902
12136
|
/** sandbox id -> tag, set only once a 421 corrects the id-derived guess. */
|
|
11903
12137
|
correctedTag = /* @__PURE__ */ new Map();
|
|
12138
|
+
/**
|
|
12139
|
+
* Rotating start address for creates, so a cold burst spreads over every
|
|
12140
|
+
* resolved runner address instead of all of it landing on address 0.
|
|
12141
|
+
* Advanced once per create; the retry on `429 runtime_busy`/connect
|
|
12142
|
+
* failure still walks the other addresses from there.
|
|
12143
|
+
*/
|
|
12144
|
+
createCursor = 0;
|
|
11904
12145
|
constructor(options) {
|
|
11905
12146
|
this.apiKey = options.apiKey;
|
|
11906
12147
|
this.region = options.region ?? regionFromApiKey(options.apiKey);
|
|
11907
12148
|
this.baseDomain = options.baseDomain ?? DEFAULT_RUNNER_BASE_DOMAIN;
|
|
11908
12149
|
this.hostname = regionHostname(this.region, this.baseDomain);
|
|
11909
12150
|
this.sessionsPerAddress = options.sessionsPerAddress ?? DEFAULT_SESSIONS_PER_ADDRESS3;
|
|
11910
|
-
this.transport = options.transport ?? createBestTransport(
|
|
12151
|
+
this.transport = options.transport ?? createBestTransport({
|
|
12152
|
+
raceHttp3: options.http3,
|
|
12153
|
+
...options.pinAddresses === void 0 ? {} : {
|
|
12154
|
+
http2: { pinAddresses: options.pinAddresses },
|
|
12155
|
+
http3: { pinAddresses: options.pinAddresses }
|
|
12156
|
+
}
|
|
12157
|
+
}).transport;
|
|
11911
12158
|
this.fallbackCreate = options.fallbackCreate;
|
|
11912
12159
|
this.transport.prewarm(this.hostname, this.sessionsPerAddress);
|
|
11913
12160
|
void this.discoverTags();
|
|
@@ -11941,8 +12188,12 @@ var RunnerClient = class {
|
|
|
11941
12188
|
if (n === 0) {
|
|
11942
12189
|
throw new Error(`RunnerClient: no runner addresses for ${this.hostname}`);
|
|
11943
12190
|
}
|
|
12191
|
+
const start = this.createCursor % n;
|
|
12192
|
+
this.createCursor = (this.createCursor + 1) % n;
|
|
11944
12193
|
let lastError;
|
|
11945
|
-
|
|
12194
|
+
let rateLimitRetries = 0;
|
|
12195
|
+
for (let offset = 0; offset < n; offset++) {
|
|
12196
|
+
const attempt = (start + offset) % n;
|
|
11946
12197
|
let res;
|
|
11947
12198
|
try {
|
|
11948
12199
|
res = await this.transport.request(this.hostname, attempt, this.hostname, "/api/v1/sandboxes", {
|
|
@@ -11963,6 +12214,14 @@ var RunnerClient = class {
|
|
|
11963
12214
|
lastError = error;
|
|
11964
12215
|
continue;
|
|
11965
12216
|
}
|
|
12217
|
+
if (error.isRateLimited) {
|
|
12218
|
+
if (rateLimitRetries >= 1) throw error;
|
|
12219
|
+
rateLimitRetries += 1;
|
|
12220
|
+
lastError = error;
|
|
12221
|
+
const waitMs = retryAfterMs(res.headers);
|
|
12222
|
+
if (waitMs > 0) await sleep9(waitMs);
|
|
12223
|
+
continue;
|
|
12224
|
+
}
|
|
11966
12225
|
throw error;
|
|
11967
12226
|
}
|
|
11968
12227
|
const data = res.body ? JSON.parse(res.body) : {};
|
|
@@ -12512,6 +12771,7 @@ var Miosa = class {
|
|
|
12512
12771
|
apiKey: credential,
|
|
12513
12772
|
...runnerRegion ? { region: runnerRegion } : {},
|
|
12514
12773
|
...config.runner?.baseDomain ? { baseDomain: config.runner.baseDomain } : {},
|
|
12774
|
+
...config.runner?.pinAddresses === void 0 ? {} : { pinAddresses: config.runner.pinAddresses },
|
|
12515
12775
|
...config.tenant ? { tenant: config.tenant } : {},
|
|
12516
12776
|
// C2: creates the runner doesn't serve (template/image/snapshot/
|
|
12517
12777
|
// cwd/persistent/non-xs) go through the existing control-plane
|