@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.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 { resolve4 } from 'dns/promises';
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), retryAfterMs) {
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 = 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 retryAfterMs = nested?.retry_after_ms ?? body5.retry_after_ms ?? retryAfterHeaderMs2;
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
- retryAfterMs === void 0 ? void 0 : retryAfterMs / 1e3,
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 = 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.3.0";
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 retryAfterMs = retryAfterHeaderMs(
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
- retryAfterMs
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 resolve4(hostname);
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 addresses = this.resolver.resolve(regionHostname2);
11370
- const groups = addresses.then(
11371
- (resolved) => resolved.map(
11372
- (address) => (
11373
- // sessionsPerAddress: 0 is a deliberate "open nothing" (e.g. a
11374
- // count-only probe).
11375
- Array.from(
11376
- { length: sessionsPerAddress },
11377
- () => markHandled(this.connectSession(regionHostname2, address))
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
- markHandled(addresses);
11383
- markHandled(groups);
11384
- return { addresses, groups, nextSlot: /* @__PURE__ */ new Map() };
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 = http2.connect(authority, connectOptions);
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 addresses = this.resolver.resolve(regionHostname2);
11656
- const groups = addresses.then(
11657
- (resolved) => resolved.map(
11658
- (address) => Array.from(
11659
- { length: sessionsPerAddress },
11660
- () => markHandled2(this.connectSession(regionHostname2, address))
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 { addresses, groups, nextSlot: /* @__PURE__ */ new Map() };
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.native.h3Connect(address, regionHostname2, this.port, this.insecure, this.connectTimeoutMs);
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().transport;
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
- for (let attempt = 0; attempt < n; attempt++) {
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