@miosa/sdk 3.3.0 → 3.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/dist/index.d.ts +187 -12
- package/dist/index.js +271 -43
- 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.0";
|
|
189
189
|
var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
|
|
190
190
|
|
|
191
191
|
// src/http.ts
|
|
@@ -303,7 +303,7 @@ var HttpClient = class {
|
|
|
303
303
|
clearTimeout(timer);
|
|
304
304
|
if (!response.ok) {
|
|
305
305
|
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
306
|
-
const
|
|
306
|
+
const retryAfterMs2 = retryAfterHeaderMs(
|
|
307
307
|
response.headers.get("retry-after")
|
|
308
308
|
);
|
|
309
309
|
let errorBody = {};
|
|
@@ -315,7 +315,7 @@ var HttpClient = class {
|
|
|
315
315
|
response.status,
|
|
316
316
|
errorBody,
|
|
317
317
|
requestId,
|
|
318
|
-
|
|
318
|
+
retryAfterMs2
|
|
319
319
|
);
|
|
320
320
|
if (error.retryable && attempt < maxRetries) {
|
|
321
321
|
lastError = error;
|
|
@@ -11208,24 +11208,36 @@ function sandboxTag(sandboxId) {
|
|
|
11208
11208
|
}
|
|
11209
11209
|
return first.toLowerCase();
|
|
11210
11210
|
}
|
|
11211
|
+
async function lookupAllIpv4(hostname) {
|
|
11212
|
+
const entries = await lookup(hostname, { family: 4, all: true });
|
|
11213
|
+
return entries.map((entry) => entry.address);
|
|
11214
|
+
}
|
|
11215
|
+
function dedupe(addresses) {
|
|
11216
|
+
return [...new Set(addresses)];
|
|
11217
|
+
}
|
|
11211
11218
|
var DnsAddressResolver = class {
|
|
11212
|
-
constructor(ttlMs = 6e4) {
|
|
11219
|
+
constructor(ttlMs = 6e4, lookupAddresses = lookupAllIpv4) {
|
|
11213
11220
|
this.ttlMs = ttlMs;
|
|
11221
|
+
this.lookupAddresses = lookupAddresses;
|
|
11214
11222
|
}
|
|
11215
11223
|
ttlMs;
|
|
11224
|
+
lookupAddresses;
|
|
11216
11225
|
cache = /* @__PURE__ */ new Map();
|
|
11217
11226
|
async resolve(hostname) {
|
|
11218
11227
|
if (isIP(hostname) !== 0) return [hostname];
|
|
11219
11228
|
if (hostname === "localhost") return ["127.0.0.1"];
|
|
11220
11229
|
const cached = this.cache.get(hostname);
|
|
11221
11230
|
if (cached && cached.expiresAt > Date.now()) return cached.addresses;
|
|
11222
|
-
const addresses = await
|
|
11231
|
+
const addresses = dedupe(await this.lookupAddresses(hostname));
|
|
11223
11232
|
if (addresses.length === 0) {
|
|
11224
11233
|
throw new Error(`DnsAddressResolver: ${hostname} resolved to zero A records`);
|
|
11225
11234
|
}
|
|
11226
11235
|
this.cache.set(hostname, { addresses, expiresAt: Date.now() + this.ttlMs });
|
|
11227
11236
|
return addresses;
|
|
11228
11237
|
}
|
|
11238
|
+
invalidate(hostname) {
|
|
11239
|
+
this.cache.delete(hostname);
|
|
11240
|
+
}
|
|
11229
11241
|
};
|
|
11230
11242
|
var StaticAddressResolver = class {
|
|
11231
11243
|
constructor(table) {
|
|
@@ -11282,6 +11294,9 @@ var Http2Transport = class {
|
|
|
11282
11294
|
port;
|
|
11283
11295
|
resolver;
|
|
11284
11296
|
defaultTimeoutMs;
|
|
11297
|
+
pinAddresses;
|
|
11298
|
+
insecure;
|
|
11299
|
+
dial;
|
|
11285
11300
|
pools = /* @__PURE__ */ new Map();
|
|
11286
11301
|
constructor(options = {}) {
|
|
11287
11302
|
this.defaultSessionsPerAddress = options.defaultSessionsPerAddress ?? DEFAULT_SESSIONS_PER_ADDRESS;
|
|
@@ -11289,6 +11304,9 @@ var Http2Transport = class {
|
|
|
11289
11304
|
this.port = options.port ?? (this.tls ? 443 : 80);
|
|
11290
11305
|
this.resolver = options.resolver ?? new DnsAddressResolver();
|
|
11291
11306
|
this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11307
|
+
this.pinAddresses = options.pinAddresses ?? true;
|
|
11308
|
+
this.insecure = options.insecure ?? false;
|
|
11309
|
+
this.dial = options.connect ?? http2.connect;
|
|
11292
11310
|
}
|
|
11293
11311
|
prewarm(regionHostname2, sessionsPerAddress) {
|
|
11294
11312
|
this.poolFor(regionHostname2, sessionsPerAddress);
|
|
@@ -11328,6 +11346,7 @@ var Http2Transport = class {
|
|
|
11328
11346
|
try {
|
|
11329
11347
|
session = await group[cursor];
|
|
11330
11348
|
} catch (error) {
|
|
11349
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11331
11350
|
throw new RunnerConnectError(regionHostname2, address, error);
|
|
11332
11351
|
}
|
|
11333
11352
|
if (session.closed || session.destroyed) {
|
|
@@ -11336,6 +11355,7 @@ var Http2Transport = class {
|
|
|
11336
11355
|
try {
|
|
11337
11356
|
session = await fresh;
|
|
11338
11357
|
} catch (error) {
|
|
11358
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11339
11359
|
throw new RunnerConnectError(regionHostname2, address, error);
|
|
11340
11360
|
}
|
|
11341
11361
|
}
|
|
@@ -11366,28 +11386,104 @@ var Http2Transport = class {
|
|
|
11366
11386
|
return pool;
|
|
11367
11387
|
}
|
|
11368
11388
|
buildPool(regionHostname2, sessionsPerAddress) {
|
|
11369
|
-
const
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11389
|
+
const pool = {
|
|
11390
|
+
addresses: this.resolveAddresses(regionHostname2),
|
|
11391
|
+
groups: Promise.resolve([]),
|
|
11392
|
+
nextSlot: /* @__PURE__ */ new Map(),
|
|
11393
|
+
sessionsPerAddress,
|
|
11394
|
+
failed: /* @__PURE__ */ new Set(),
|
|
11395
|
+
rebuilding: void 0
|
|
11396
|
+
};
|
|
11397
|
+
pool.groups = pool.addresses.then(
|
|
11398
|
+
(resolved) => resolved.map((address) => this.openGroup(regionHostname2, address, sessionsPerAddress))
|
|
11399
|
+
);
|
|
11400
|
+
markHandled(pool.addresses);
|
|
11401
|
+
markHandled(pool.groups);
|
|
11402
|
+
return pool;
|
|
11403
|
+
}
|
|
11404
|
+
/**
|
|
11405
|
+
* The addresses this pool pins to. With pinning on, every IPv4 address
|
|
11406
|
+
* the hostname resolves to; with it off, the hostname itself - one entry,
|
|
11407
|
+
* dialled as-is so the OS resolver chooses.
|
|
11408
|
+
*/
|
|
11409
|
+
resolveAddresses(regionHostname2) {
|
|
11410
|
+
return this.pinAddresses ? this.resolver.resolve(regionHostname2) : Promise.resolve([regionHostname2]);
|
|
11411
|
+
}
|
|
11412
|
+
/**
|
|
11413
|
+
* Opens `sessionsPerAddress` sessions against one address, in parallel.
|
|
11414
|
+
* sessionsPerAddress: 0 is a deliberate "open nothing" (e.g. a count-only
|
|
11415
|
+
* probe).
|
|
11416
|
+
*/
|
|
11417
|
+
openGroup(regionHostname2, address, sessionsPerAddress) {
|
|
11418
|
+
return Array.from(
|
|
11419
|
+
{ length: sessionsPerAddress },
|
|
11420
|
+
() => markHandled(this.connectSession(regionHostname2, address))
|
|
11381
11421
|
);
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11422
|
+
}
|
|
11423
|
+
/** Closes a session slot once it connects, ignoring one that never did. */
|
|
11424
|
+
closeSlot(slot) {
|
|
11425
|
+
slot.then((session) => {
|
|
11426
|
+
if (!session.closed) session.close();
|
|
11427
|
+
}).catch(() => void 0);
|
|
11428
|
+
}
|
|
11429
|
+
/**
|
|
11430
|
+
* A session pinned to `address` failed to connect. Mark that address's
|
|
11431
|
+
* group for reopening and re-resolve in the background (single-flight),
|
|
11432
|
+
* so one dead address never costs every concurrent failure its own DNS
|
|
11433
|
+
* lookup. Never blocks or fails the request that is already on its way
|
|
11434
|
+
* out - the caller retries another index (C6's next-IP retry).
|
|
11435
|
+
*/
|
|
11436
|
+
noteAddressFailure(regionHostname2, address) {
|
|
11437
|
+
if (!this.pinAddresses) return;
|
|
11438
|
+
const pool = this.pools.get(regionHostname2);
|
|
11439
|
+
if (!pool) return;
|
|
11440
|
+
pool.failed.add(address);
|
|
11441
|
+
this.resolver.invalidate?.(regionHostname2);
|
|
11442
|
+
if (pool.rebuilding) return;
|
|
11443
|
+
pool.rebuilding = this.rebuildPool(regionHostname2, pool).finally(() => {
|
|
11444
|
+
pool.rebuilding = void 0;
|
|
11445
|
+
});
|
|
11446
|
+
}
|
|
11447
|
+
/**
|
|
11448
|
+
* Re-resolves `regionHostname` and redistributes the pool over the fresh
|
|
11449
|
+
* addresses: the failed address's sessions are dropped and reopened, a
|
|
11450
|
+
* newly advertised address gets a group of its own, an address DNS no
|
|
11451
|
+
* longer lists loses its sessions, and every healthy address keeps the
|
|
11452
|
+
* sessions it already has - so a failure on one host never costs the rest
|
|
11453
|
+
* their warm connections.
|
|
11454
|
+
*/
|
|
11455
|
+
async rebuildPool(regionHostname2, pool) {
|
|
11456
|
+
const failed = new Set(pool.failed);
|
|
11457
|
+
pool.failed.clear();
|
|
11458
|
+
const resolved = await this.resolveAddresses(regionHostname2).catch(() => void 0);
|
|
11459
|
+
if (!resolved) {
|
|
11460
|
+
for (const address of failed) pool.failed.add(address);
|
|
11461
|
+
return;
|
|
11462
|
+
}
|
|
11463
|
+
const previous = await pool.addresses.catch(() => []);
|
|
11464
|
+
const previousGroups = await pool.groups.catch(
|
|
11465
|
+
() => []
|
|
11466
|
+
);
|
|
11467
|
+
const groups = resolved.map((address) => {
|
|
11468
|
+
const index = previous.indexOf(address);
|
|
11469
|
+
const reusable = index >= 0 && !failed.has(address) ? previousGroups[index] : void 0;
|
|
11470
|
+
if (reusable) return reusable;
|
|
11471
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11472
|
+
return this.openGroup(regionHostname2, address, pool.sessionsPerAddress);
|
|
11473
|
+
});
|
|
11474
|
+
previous.forEach((address, index) => {
|
|
11475
|
+
if (resolved.includes(address)) return;
|
|
11476
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11477
|
+
});
|
|
11478
|
+
pool.addresses = Promise.resolve(resolved);
|
|
11479
|
+
pool.groups = Promise.resolve(groups);
|
|
11480
|
+
pool.nextSlot.clear();
|
|
11385
11481
|
}
|
|
11386
11482
|
connectSession(regionHostname2, address) {
|
|
11387
11483
|
return new Promise((resolve, reject) => {
|
|
11388
11484
|
const authority = `${this.tls ? "https" : "http"}://${address}:${this.port}`;
|
|
11389
|
-
const connectOptions = this.tls ? { servername: regionHostname2 } : {};
|
|
11390
|
-
const session =
|
|
11485
|
+
const connectOptions = this.tls ? { servername: regionHostname2, rejectUnauthorized: !this.insecure } : {};
|
|
11486
|
+
const session = this.dial(authority, connectOptions);
|
|
11391
11487
|
const onError = (error) => {
|
|
11392
11488
|
session.destroy();
|
|
11393
11489
|
reject(error);
|
|
@@ -11560,6 +11656,8 @@ var Http3Transport = class {
|
|
|
11560
11656
|
connectTimeoutMs;
|
|
11561
11657
|
insecure;
|
|
11562
11658
|
port;
|
|
11659
|
+
pinAddresses;
|
|
11660
|
+
dial;
|
|
11563
11661
|
pools = /* @__PURE__ */ new Map();
|
|
11564
11662
|
constructor(options = {}) {
|
|
11565
11663
|
const native = loadNative();
|
|
@@ -11573,6 +11671,8 @@ var Http3Transport = class {
|
|
|
11573
11671
|
this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
11574
11672
|
this.insecure = options.insecure ?? false;
|
|
11575
11673
|
this.port = options.port ?? 443;
|
|
11674
|
+
this.pinAddresses = options.pinAddresses ?? true;
|
|
11675
|
+
this.dial = options.connect ?? ((address, servername, port, insecure, timeoutMs) => this.native.h3Connect(address, servername, port, insecure, timeoutMs));
|
|
11576
11676
|
}
|
|
11577
11677
|
prewarm(regionHostname2, sessionsPerAddress) {
|
|
11578
11678
|
this.poolFor(regionHostname2, sessionsPerAddress);
|
|
@@ -11618,6 +11718,7 @@ var Http3Transport = class {
|
|
|
11618
11718
|
try {
|
|
11619
11719
|
handle = await group[cursor];
|
|
11620
11720
|
} catch (error) {
|
|
11721
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11621
11722
|
throw new RunnerConnectError(regionHostname2, address, error);
|
|
11622
11723
|
}
|
|
11623
11724
|
const reconnect = () => {
|
|
@@ -11652,21 +11753,99 @@ var Http3Transport = class {
|
|
|
11652
11753
|
return pool;
|
|
11653
11754
|
}
|
|
11654
11755
|
buildPool(regionHostname2, sessionsPerAddress) {
|
|
11655
|
-
const
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11756
|
+
const pool = {
|
|
11757
|
+
addresses: this.resolveAddresses(regionHostname2),
|
|
11758
|
+
groups: Promise.resolve([]),
|
|
11759
|
+
nextSlot: /* @__PURE__ */ new Map(),
|
|
11760
|
+
sessionsPerAddress,
|
|
11761
|
+
failed: /* @__PURE__ */ new Set(),
|
|
11762
|
+
rebuilding: void 0
|
|
11763
|
+
};
|
|
11764
|
+
pool.groups = pool.addresses.then(
|
|
11765
|
+
(resolved) => resolved.map((address) => this.openGroup(regionHostname2, address, sessionsPerAddress))
|
|
11663
11766
|
);
|
|
11664
|
-
markHandled2(addresses);
|
|
11665
|
-
markHandled2(groups);
|
|
11666
|
-
return
|
|
11767
|
+
markHandled2(pool.addresses);
|
|
11768
|
+
markHandled2(pool.groups);
|
|
11769
|
+
return pool;
|
|
11770
|
+
}
|
|
11771
|
+
/**
|
|
11772
|
+
* The addresses this pool pins to. With pinning on, every IPv4 address
|
|
11773
|
+
* the hostname resolves to. With it off, the hostname is resolved once
|
|
11774
|
+
* and only its first address is kept - one target, chosen by the OS
|
|
11775
|
+
* resolver, which is the closest this transport can get to HTTP/2's
|
|
11776
|
+
* hostname dial: the native addon takes a literal socket address, never
|
|
11777
|
+
* a hostname.
|
|
11778
|
+
*/
|
|
11779
|
+
async resolveAddresses(regionHostname2) {
|
|
11780
|
+
if (this.pinAddresses) return this.resolver.resolve(regionHostname2);
|
|
11781
|
+
const [first] = await this.resolver.resolve(regionHostname2);
|
|
11782
|
+
if (!first) {
|
|
11783
|
+
throw new Error(`Http3Transport: ${regionHostname2} resolved to zero addresses`);
|
|
11784
|
+
}
|
|
11785
|
+
return [first];
|
|
11786
|
+
}
|
|
11787
|
+
/** Opens `sessionsPerAddress` connections against one address, in parallel. */
|
|
11788
|
+
openGroup(regionHostname2, address, sessionsPerAddress) {
|
|
11789
|
+
return Array.from(
|
|
11790
|
+
{ length: sessionsPerAddress },
|
|
11791
|
+
() => markHandled2(this.connectSession(regionHostname2, address))
|
|
11792
|
+
);
|
|
11793
|
+
}
|
|
11794
|
+
/** Closes a connection slot once it connects, ignoring one that never did. */
|
|
11795
|
+
closeSlot(slot) {
|
|
11796
|
+
slot.then((handle) => this.native.h3Close(handle)).catch(() => void 0);
|
|
11797
|
+
}
|
|
11798
|
+
/**
|
|
11799
|
+
* A connection pinned to `address` failed to connect. Mark that address's
|
|
11800
|
+
* group for reopening and re-resolve in the background (single-flight),
|
|
11801
|
+
* so one dead address never costs every concurrent failure its own DNS
|
|
11802
|
+
* lookup. Never blocks or fails the request already on its way out.
|
|
11803
|
+
*/
|
|
11804
|
+
noteAddressFailure(regionHostname2, address) {
|
|
11805
|
+
if (!this.pinAddresses) return;
|
|
11806
|
+
const pool = this.pools.get(regionHostname2);
|
|
11807
|
+
if (!pool) return;
|
|
11808
|
+
pool.failed.add(address);
|
|
11809
|
+
this.resolver.invalidate?.(regionHostname2);
|
|
11810
|
+
if (pool.rebuilding) return;
|
|
11811
|
+
pool.rebuilding = this.rebuildPool(regionHostname2, pool).finally(() => {
|
|
11812
|
+
pool.rebuilding = void 0;
|
|
11813
|
+
});
|
|
11814
|
+
}
|
|
11815
|
+
/**
|
|
11816
|
+
* Re-resolves `regionHostname` and redistributes the pool over the fresh
|
|
11817
|
+
* addresses: the failed address's connections are dropped and reopened, a
|
|
11818
|
+
* newly advertised address gets a group of its own, an address DNS no
|
|
11819
|
+
* longer lists loses its connections, and every healthy address keeps the
|
|
11820
|
+
* connections it already has.
|
|
11821
|
+
*/
|
|
11822
|
+
async rebuildPool(regionHostname2, pool) {
|
|
11823
|
+
const failed = new Set(pool.failed);
|
|
11824
|
+
pool.failed.clear();
|
|
11825
|
+
const resolved = await this.resolveAddresses(regionHostname2).catch(() => void 0);
|
|
11826
|
+
if (!resolved) {
|
|
11827
|
+
for (const address of failed) pool.failed.add(address);
|
|
11828
|
+
return;
|
|
11829
|
+
}
|
|
11830
|
+
const previous = await pool.addresses.catch(() => []);
|
|
11831
|
+
const previousGroups = await pool.groups.catch(() => []);
|
|
11832
|
+
const groups = resolved.map((address) => {
|
|
11833
|
+
const index = previous.indexOf(address);
|
|
11834
|
+
const reusable = index >= 0 && !failed.has(address) ? previousGroups[index] : void 0;
|
|
11835
|
+
if (reusable) return reusable;
|
|
11836
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11837
|
+
return this.openGroup(regionHostname2, address, pool.sessionsPerAddress);
|
|
11838
|
+
});
|
|
11839
|
+
previous.forEach((address, index) => {
|
|
11840
|
+
if (resolved.includes(address)) return;
|
|
11841
|
+
for (const slot of previousGroups[index] ?? []) this.closeSlot(slot);
|
|
11842
|
+
});
|
|
11843
|
+
pool.addresses = Promise.resolve(resolved);
|
|
11844
|
+
pool.groups = Promise.resolve(groups);
|
|
11845
|
+
pool.nextSlot.clear();
|
|
11667
11846
|
}
|
|
11668
11847
|
connectSession(regionHostname2, address) {
|
|
11669
|
-
return this.
|
|
11848
|
+
return this.dial(address, regionHostname2, this.port, this.insecure, this.connectTimeoutMs);
|
|
11670
11849
|
}
|
|
11671
11850
|
async sendRequest(regionHostname2, address, authority, path, handle, init, reconnect) {
|
|
11672
11851
|
const timeoutMs = init.timeoutMs ?? this.defaultTimeoutMs;
|
|
@@ -11694,6 +11873,7 @@ var Http3Transport = class {
|
|
|
11694
11873
|
try {
|
|
11695
11874
|
fresh = await reconnect();
|
|
11696
11875
|
} catch (reconnectError) {
|
|
11876
|
+
this.noteAddressFailure(regionHostname2, address);
|
|
11697
11877
|
throw new RunnerConnectError(regionHostname2, address, reconnectError);
|
|
11698
11878
|
}
|
|
11699
11879
|
return attempt(fresh);
|
|
@@ -11722,7 +11902,7 @@ async function* singleChunk(body5) {
|
|
|
11722
11902
|
// src/runner/transport-factory.ts
|
|
11723
11903
|
function createBestTransport(options = {}) {
|
|
11724
11904
|
const http3 = detectHttp3Support();
|
|
11725
|
-
if (!http3.available) {
|
|
11905
|
+
if (!http3.available || options.raceHttp3 !== true) {
|
|
11726
11906
|
return { transport: new Http2Transport(options.http2), http3 };
|
|
11727
11907
|
}
|
|
11728
11908
|
const transport = new RacingTransport(new Http2Transport(options.http2), new Http3Transport(options.http3));
|
|
@@ -11830,6 +12010,16 @@ var RunnerError = class _RunnerError extends Error {
|
|
|
11830
12010
|
get isBusy() {
|
|
11831
12011
|
return this.code === "runtime_busy" || this.code === "feed_stale";
|
|
11832
12012
|
}
|
|
12013
|
+
/**
|
|
12014
|
+
* `429 rate_limited`: the key's own window on this runner has no room.
|
|
12015
|
+
*
|
|
12016
|
+
* Separate from `isBusy`, which means this runner's admission is full for
|
|
12017
|
+
* everyone. The limiter counts per key per runner, so a refusal here says
|
|
12018
|
+
* nothing about the next address, and `createSandbox` may try that one.
|
|
12019
|
+
*/
|
|
12020
|
+
get isRateLimited() {
|
|
12021
|
+
return this.code === "rate_limited";
|
|
12022
|
+
}
|
|
11833
12023
|
/** C2: the runner doesn't serve this create shape at all - send it to api.miosa.ai instead. */
|
|
11834
12024
|
get isUnsupportedRequest() {
|
|
11835
12025
|
return this.code === "RUNNER_UNSUPPORTED_REQUEST";
|
|
@@ -11889,6 +12079,18 @@ function createSupportedByRunner(params) {
|
|
|
11889
12079
|
const isNonXs = size !== void 0 && size !== "xs";
|
|
11890
12080
|
return !(hasTemplate || hasSnapshot || hasCwd || isPersistent || isNonXs);
|
|
11891
12081
|
}
|
|
12082
|
+
var RATE_LIMIT_RETRY_CAP_MS = 250;
|
|
12083
|
+
function retryAfterMs(headers, capMs = RATE_LIMIT_RETRY_CAP_MS) {
|
|
12084
|
+
const raw = headers["retry-after"] ?? headers["Retry-After"];
|
|
12085
|
+
if (raw === void 0) return 0;
|
|
12086
|
+
const seconds = Number(raw.trim());
|
|
12087
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, capMs);
|
|
12088
|
+
const at = Date.parse(raw);
|
|
12089
|
+
return Number.isNaN(at) ? 0 : Math.min(Math.max(0, at - Date.now()), capMs);
|
|
12090
|
+
}
|
|
12091
|
+
function sleep9(ms) {
|
|
12092
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12093
|
+
}
|
|
11892
12094
|
var RunnerClient = class {
|
|
11893
12095
|
region;
|
|
11894
12096
|
hostname;
|
|
@@ -11901,13 +12103,26 @@ var RunnerClient = class {
|
|
|
11901
12103
|
addressIndexByTag = /* @__PURE__ */ new Map();
|
|
11902
12104
|
/** sandbox id -> tag, set only once a 421 corrects the id-derived guess. */
|
|
11903
12105
|
correctedTag = /* @__PURE__ */ new Map();
|
|
12106
|
+
/**
|
|
12107
|
+
* Rotating start address for creates, so a cold burst spreads over every
|
|
12108
|
+
* resolved runner address instead of all of it landing on address 0.
|
|
12109
|
+
* Advanced once per create; the retry on `429 runtime_busy`/connect
|
|
12110
|
+
* failure still walks the other addresses from there.
|
|
12111
|
+
*/
|
|
12112
|
+
createCursor = 0;
|
|
11904
12113
|
constructor(options) {
|
|
11905
12114
|
this.apiKey = options.apiKey;
|
|
11906
12115
|
this.region = options.region ?? regionFromApiKey(options.apiKey);
|
|
11907
12116
|
this.baseDomain = options.baseDomain ?? DEFAULT_RUNNER_BASE_DOMAIN;
|
|
11908
12117
|
this.hostname = regionHostname(this.region, this.baseDomain);
|
|
11909
12118
|
this.sessionsPerAddress = options.sessionsPerAddress ?? DEFAULT_SESSIONS_PER_ADDRESS3;
|
|
11910
|
-
this.transport = options.transport ?? createBestTransport(
|
|
12119
|
+
this.transport = options.transport ?? createBestTransport({
|
|
12120
|
+
raceHttp3: options.http3,
|
|
12121
|
+
...options.pinAddresses === void 0 ? {} : {
|
|
12122
|
+
http2: { pinAddresses: options.pinAddresses },
|
|
12123
|
+
http3: { pinAddresses: options.pinAddresses }
|
|
12124
|
+
}
|
|
12125
|
+
}).transport;
|
|
11911
12126
|
this.fallbackCreate = options.fallbackCreate;
|
|
11912
12127
|
this.transport.prewarm(this.hostname, this.sessionsPerAddress);
|
|
11913
12128
|
void this.discoverTags();
|
|
@@ -11941,8 +12156,12 @@ var RunnerClient = class {
|
|
|
11941
12156
|
if (n === 0) {
|
|
11942
12157
|
throw new Error(`RunnerClient: no runner addresses for ${this.hostname}`);
|
|
11943
12158
|
}
|
|
12159
|
+
const start = this.createCursor % n;
|
|
12160
|
+
this.createCursor = (this.createCursor + 1) % n;
|
|
11944
12161
|
let lastError;
|
|
11945
|
-
|
|
12162
|
+
let rateLimitRetries = 0;
|
|
12163
|
+
for (let offset = 0; offset < n; offset++) {
|
|
12164
|
+
const attempt = (start + offset) % n;
|
|
11946
12165
|
let res;
|
|
11947
12166
|
try {
|
|
11948
12167
|
res = await this.transport.request(this.hostname, attempt, this.hostname, "/api/v1/sandboxes", {
|
|
@@ -11963,6 +12182,14 @@ var RunnerClient = class {
|
|
|
11963
12182
|
lastError = error;
|
|
11964
12183
|
continue;
|
|
11965
12184
|
}
|
|
12185
|
+
if (error.isRateLimited) {
|
|
12186
|
+
if (rateLimitRetries >= 1) throw error;
|
|
12187
|
+
rateLimitRetries += 1;
|
|
12188
|
+
lastError = error;
|
|
12189
|
+
const waitMs = retryAfterMs(res.headers);
|
|
12190
|
+
if (waitMs > 0) await sleep9(waitMs);
|
|
12191
|
+
continue;
|
|
12192
|
+
}
|
|
11966
12193
|
throw error;
|
|
11967
12194
|
}
|
|
11968
12195
|
const data = res.body ? JSON.parse(res.body) : {};
|
|
@@ -12512,6 +12739,7 @@ var Miosa = class {
|
|
|
12512
12739
|
apiKey: credential,
|
|
12513
12740
|
...runnerRegion ? { region: runnerRegion } : {},
|
|
12514
12741
|
...config.runner?.baseDomain ? { baseDomain: config.runner.baseDomain } : {},
|
|
12742
|
+
...config.runner?.pinAddresses === void 0 ? {} : { pinAddresses: config.runner.pinAddresses },
|
|
12515
12743
|
...config.tenant ? { tenant: config.tenant } : {},
|
|
12516
12744
|
// C2: creates the runner doesn't serve (template/image/snapshot/
|
|
12517
12745
|
// cwd/persistent/non-xs) go through the existing control-plane
|