@promigence/sdk 0.1.0-beta.12 → 0.1.0-beta.14

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/http2.js CHANGED
@@ -6,11 +6,12 @@ import * as zlib from "node:zlib";
6
6
  import { __caGuardInternals, guardCertChain } from "./ca-guard.js";
7
7
  import { DEFAULT_API_URL } from "./core.js";
8
8
  import { customDispatcher } from "./dispatcher.js";
9
- import { mark, TRACE_ON, traceEvent, traceEventAt } from "./trace.js";
9
+ import { mark, onTraceFlush, TRACE_ON, traceEvent, traceEventAt } from "./trace.js";
10
10
  const MAX_SESSIONS = 4;
11
11
  const FIRST_SESSIONS = 2;
12
12
  const BURST_MIN = 8;
13
13
  const HOLD_MS = 10;
14
+ const STANDBY_AFTER_MS = 300;
14
15
  const LOAD_FIRST_USE_MS = 120_000;
15
16
  const CONNECT_TIMEOUT_MS = 10_000;
16
17
  const IDLE_CLOSE_MS = 10_000;
@@ -561,6 +562,7 @@ export const tuning = {
561
562
  holdMs: HOLD_MS,
562
563
  burstMin: BURST_MIN,
563
564
  loadFirstUseMs: LOAD_FIRST_USE_MS,
565
+ standbyAfterMs: STANDBY_AFTER_MS,
564
566
  earlyPlatform: undefined,
565
567
  earlyServers: undefined,
566
568
  earlyHostsFile: undefined,
@@ -844,19 +846,21 @@ function apiLookup(asked, observe, ipv4First = v4First(), earlyOk = true) {
844
846
  const earlies = new Map();
845
847
  const MAX_EARLIES = 4;
846
848
  const EARLY_MAX_AGE_MS = 60_000;
847
- const EARLY_TIMEOUT_MS = 2_000;
848
- const LOOKUP_AT_LOAD = (() => {
849
+ const LOOKUP_FIRST_MS = 150;
850
+ const LOOKUP_TRIES = 3;
851
+ const LOOKUP_AT_MS = [0, 100, 250];
852
+ const DNS_AT_LOAD = (() => {
849
853
  try {
850
- return nodeDns()?.lookup;
854
+ return nodeDns();
851
855
  }
852
856
  catch {
853
857
  return undefined;
854
858
  }
855
859
  })();
860
+ const LOOKUP_AT_LOAD = DNS_AT_LOAD?.lookup;
856
861
  function lookupReplaced() {
857
862
  try {
858
- const d = nodeDns();
859
- return d !== undefined && d.lookup !== LOOKUP_AT_LOAD;
863
+ return DNS_AT_LOAD !== undefined && DNS_AT_LOAD.lookup !== LOOKUP_AT_LOAD;
860
864
  }
861
865
  catch {
862
866
  return true;
@@ -865,8 +869,8 @@ function lookupReplaced() {
865
869
  const EARLY_WORKER = `
866
870
  const { workerData } = require("node:worker_threads");
867
871
  const { isIPv4 } = require("node:net");
868
- const { port, host, timeoutMs, servers, hostsFile } = workerData;
869
- const done = (m) => { try { port.postMessage(m); } catch {} try { port.close(); } catch {} };
872
+ const { port, host, timeoutMs, tries, atMs, servers, hostsFile } = workerData;
873
+ const done = (m) => { m.hr = Number(process.hrtime.bigint()) / 1e6; try { port.postMessage(m); } catch {} try { port.close(); } catch {} };
870
874
  try {
871
875
  let named = false;
872
876
  const v4 = [];
@@ -880,12 +884,28 @@ try {
880
884
  } catch {}
881
885
  if (named) done(v4.length > 0 ? { host, addrs: v4, ttl: 60, at: Date.now() } : { host, none: "hosts" });
882
886
  else {
883
- const r = new (require("node:dns").Resolver)({ timeout: timeoutMs, tries: 1 });
884
- if (Array.isArray(servers) && servers.length > 0) r.setServers(servers);
885
- r.resolve4(host, { ttl: true }, (err, recs) => {
886
- if (err || !Array.isArray(recs) || recs.length === 0) return done({ host, none: (err && err.code) || "empty" });
887
- done({ host, addrs: recs.map((x) => x.address), ttl: Math.min(...recs.map((x) => x.ttl)), at: Date.now() });
888
- });
887
+ const { Resolver } = require("node:dns");
888
+ const rs = [];
889
+ const timers = [];
890
+ let fin = false, asked = 0, failed = 0;
891
+ const finish = (m) => { if (fin) return; fin = true; for (const t of timers) clearTimeout(t); for (const r of rs) { try { r.cancel(); } catch {} } done(m); };
892
+ const gaveUp = (why) => { if (++failed < asked) return; if (asked < atMs.length) ask(); else finish({ host, none: why }); };
893
+ const ask = () => {
894
+ if (fin || asked >= atMs.length) return;
895
+ const k = asked++;
896
+ try {
897
+ const r = new Resolver({ timeout: timeoutMs, tries });
898
+ const list = Array.isArray(servers) && servers.length > 0 ? servers : r.getServers();
899
+ if (list.length > 0) r.setServers(list.map((_, i) => list[(i + k) % list.length]));
900
+ rs.push(r);
901
+ r.resolve4(host, { ttl: true }, (err, recs) => {
902
+ if (fin) return;
903
+ if (!err && Array.isArray(recs) && recs.length > 0) return finish({ host, addrs: recs.map((x) => x.address), ttl: Math.min(...recs.map((x) => x.ttl)), at: Date.now() });
904
+ gaveUp((err && err.code) || "empty");
905
+ });
906
+ } catch { gaveUp("error"); }
907
+ };
908
+ for (const ms of atMs) { if (ms <= 0) ask(); else timers.push(setTimeout(ask, ms)); }
889
909
  }
890
910
  } catch { done({ host, none: "error" }); }
891
911
  `;
@@ -916,7 +936,7 @@ function earlyLookupFor(host) {
916
936
  const worker = new Worker(EARLY_WORKER, {
917
937
  eval: true,
918
938
  workerData: {
919
- port: port2, host, timeoutMs: EARLY_TIMEOUT_MS,
939
+ port: port2, host, timeoutMs: LOOKUP_FIRST_MS, tries: LOOKUP_TRIES, atMs: LOOKUP_AT_MS,
920
940
  servers: tuning.earlyServers ?? (Array.isArray(earlySeam().servers) ? earlySeam().servers : undefined),
921
941
  hostsFile: tuning.earlyHostsFile ?? (typeof earlySeam().hostsFile === "string" ? earlySeam().hostsFile : undefined),
922
942
  },
@@ -964,6 +984,10 @@ function settleEarly(e, message) {
964
984
  else {
965
985
  e.state = "none";
966
986
  }
987
+ if (TRACE_ON) {
988
+ const at = typeof m?.hr === "number" ? m.hr - (Number(process.hrtime.bigint()) / 1e6 - performance.now()) : performance.now();
989
+ traceEventAt(e.state === "answered" ? `dns:early-answer ${addrs.length}` : `dns:early-none ${typeof m?.none === "string" ? m.none : "gone"}`, at);
990
+ }
967
991
  safely(() => { e.port.close(); });
968
992
  const waiters = e.waiters.splice(0);
969
993
  for (const w of waiters)
@@ -1290,12 +1314,13 @@ function targetOf(url, known, remember) {
1290
1314
  }
1291
1315
  function fetchInit(init, deadline) {
1292
1316
  const timeout = AbortSignal.timeout(Math.max(0, Math.ceil(deadline - performance.now())));
1317
+ const dispatcher = init.dispatcher ?? init.dispatcherFor?.(init.timeoutMs);
1293
1318
  return {
1294
1319
  method: init.method,
1295
1320
  headers: init.headers,
1296
1321
  signal: init.signal === undefined ? timeout : AbortSignal.any([timeout, init.signal]),
1297
1322
  ...(init.body === undefined ? {} : { body: init.body }),
1298
- ...(init.dispatcher === undefined ? {} : { dispatcher: init.dispatcher }),
1323
+ ...(dispatcher === undefined ? {} : { dispatcher }),
1299
1324
  };
1300
1325
  }
1301
1326
  function fetchFailed(cause) {
@@ -1312,27 +1337,45 @@ class HandBack {
1312
1337
  const UNSENT = new HandBack(0, 0);
1313
1338
  const VIA_FETCH = Symbol("via-fetch");
1314
1339
  const THREAD_GRACE_MS = 1_000;
1340
+ const THREAD_SWEEP_MS = 250;
1341
+ const THREAD_MARKS = 4096;
1315
1342
  const THREAD_PHASES = ["w.got", "w.sent", "w.head", "w.end"];
1316
1343
  class TransportThread {
1317
1344
  origin;
1318
1345
  worker;
1319
1346
  port;
1347
+ acks;
1320
1348
  state = "starting";
1321
1349
  sessions = 0;
1322
1350
  limit = DEFAULT_STREAM_LIMIT;
1323
1351
  everReady = false;
1324
1352
  closedReason = "";
1325
1353
  onFail;
1354
+ startedAt = performance.now();
1355
+ startWaiters = [];
1326
1356
  inflight = new Map();
1327
1357
  nextId = 1;
1328
1358
  listening = false;
1359
+ refd = false;
1360
+ sweep;
1361
+ freeMarks = [];
1362
+ messages = 0;
1363
+ outbox = [];
1364
+ turnOpen = false;
1365
+ posts = 0;
1329
1366
  answered = 0;
1330
1367
  pace = THREAD_DEFAULTS.pace;
1331
1368
  hrOffset = Number(process.hrtime.bigint()) / 1e6 - performance.now();
1332
- constructor(origin, worker, port) {
1369
+ constructor(origin, worker, port, acks) {
1333
1370
  this.origin = origin;
1334
1371
  this.worker = worker;
1335
1372
  this.port = port;
1373
+ this.acks = acks;
1374
+ for (let i = acks.length - 1; i >= 0; i--)
1375
+ this.freeMarks.push(i);
1376
+ }
1377
+ acked(p) {
1378
+ return Atomics.load(this.acks, p.mark) === p.id;
1336
1379
  }
1337
1380
  poll() {
1338
1381
  for (;;) {
@@ -1349,12 +1392,44 @@ class TransportThread {
1349
1392
  }
1350
1393
  }
1351
1394
  ready() {
1352
- if (this.state !== "closed")
1395
+ if ((!this.listening || this.state === "starting") && this.state !== "closed")
1353
1396
  this.poll();
1354
- return this.state === "ready" && this.sessions > 0 && this.inflight.size < this.sessions * this.limit;
1397
+ return this.state === "ready" && this.sessions > 0 && this.inflight.size < this.sessions * this.limit && this.freeMarks.length > 0;
1398
+ }
1399
+ starting() {
1400
+ if (this.state === "starting")
1401
+ this.poll();
1402
+ return this.state === "starting";
1403
+ }
1404
+ whenStarted(cb) {
1405
+ if (!this.starting()) {
1406
+ queueMicrotask(cb);
1407
+ return () => { };
1408
+ }
1409
+ this.startWaiters.push(cb);
1410
+ this.listen();
1411
+ return () => {
1412
+ const i = this.startWaiters.indexOf(cb);
1413
+ if (i >= 0)
1414
+ this.startWaiters.splice(i, 1);
1415
+ };
1416
+ }
1417
+ started() {
1418
+ for (const cb of this.startWaiters.splice(0))
1419
+ safely(cb);
1420
+ }
1421
+ listen() {
1422
+ if (this.listening)
1423
+ return;
1424
+ this.listening = true;
1425
+ this.poll();
1426
+ this.port.on("message", (m) => { this.onMessage(m); });
1427
+ if (!this.refd)
1428
+ safely(() => { this.port.unref(); });
1355
1429
  }
1356
1430
  onMessage(m) {
1357
1431
  const msg = m;
1432
+ this.messages++;
1358
1433
  switch (msg?.t) {
1359
1434
  case "ready":
1360
1435
  if (this.state === "starting" || this.state === "ready") {
@@ -1374,8 +1449,16 @@ class TransportThread {
1374
1449
  if (typeof c.index === "number" && typeof c.addr === "string" && typeof c.localPort === "number")
1375
1450
  traceEventAt(`thread:conn${c.index} ${c.addr} ${c.localPort}`, t);
1376
1451
  }
1452
+ if (first)
1453
+ this.started();
1377
1454
  }
1378
1455
  return;
1456
+ case "ev": {
1457
+ const e = msg;
1458
+ if (typeof e.n === "string" && typeof e.hr === "number")
1459
+ traceEventAt(`thread:w-${e.n}`, e.hr - this.hrOffset);
1460
+ return;
1461
+ }
1379
1462
  case "sessions":
1380
1463
  if (typeof msg.sessions === "number")
1381
1464
  this.sessions = msg.sessions;
@@ -1386,17 +1469,12 @@ class TransportThread {
1386
1469
  if (this.closedReason === "")
1387
1470
  this.closedReason = msg.reason ?? "draining";
1388
1471
  traceEvent("thread:draining");
1472
+ this.started();
1389
1473
  }
1390
1474
  return;
1391
1475
  case "closed":
1392
1476
  this.close(msg.reason ?? "closed");
1393
1477
  return;
1394
- case "ack": {
1395
- const p = this.inflight.get(msg.id ?? -1);
1396
- if (p !== undefined)
1397
- p.acked = true;
1398
- return;
1399
- }
1400
1478
  case "res":
1401
1479
  case "err":
1402
1480
  this.settle(m);
@@ -1472,6 +1550,7 @@ class TransportThread {
1472
1550
  this.port.postMessage({ t: "drain" });
1473
1551
  }
1474
1552
  catch { }
1553
+ this.started();
1475
1554
  }
1476
1555
  close(reason) {
1477
1556
  if (this.state === "closed")
@@ -1480,10 +1559,13 @@ class TransportThread {
1480
1559
  if (this.closedReason === "")
1481
1560
  this.closedReason = reason;
1482
1561
  traceEvent("thread:closed");
1562
+ this.started();
1563
+ this.outbox = [];
1483
1564
  for (const [id, p] of [...this.inflight]) {
1565
+ const sent = this.acked(p);
1484
1566
  this.inflight.delete(id);
1485
1567
  this.release(p);
1486
- if (!p.acked)
1568
+ if (!sent)
1487
1569
  p.resolve(UNSENT);
1488
1570
  else if (p.replayable)
1489
1571
  p.resolve(new HandBack(0, 1));
@@ -1500,23 +1582,50 @@ class TransportThread {
1500
1582
  }
1501
1583
  }
1502
1584
  ref() {
1503
- if (!this.listening) {
1504
- this.listening = true;
1505
- this.poll();
1506
- this.port.on("message", (m) => { this.onMessage(m); });
1507
- }
1585
+ this.listen();
1586
+ if (this.refd)
1587
+ return;
1588
+ this.refd = true;
1508
1589
  safely(() => { this.port.ref(); });
1509
1590
  safely(() => { this.worker.ref(); });
1591
+ this.sweep = setInterval(() => { this.sweepBackstops(); }, THREAD_SWEEP_MS);
1592
+ safely(() => { this.sweep?.unref?.(); });
1510
1593
  }
1511
1594
  unref() {
1595
+ if (this.sweep !== undefined) {
1596
+ clearInterval(this.sweep);
1597
+ this.sweep = undefined;
1598
+ }
1599
+ if (!this.refd)
1600
+ return;
1601
+ this.refd = false;
1512
1602
  safely(() => { this.port.unref(); });
1513
1603
  safely(() => { this.worker.unref(); });
1514
1604
  }
1605
+ sweepBackstops() {
1606
+ this.poll();
1607
+ const now = performance.now();
1608
+ let late = false;
1609
+ for (const [id, p] of [...this.inflight]) {
1610
+ if (p.backstopAt > now)
1611
+ continue;
1612
+ late = true;
1613
+ this.inflight.delete(id);
1614
+ this.release(p);
1615
+ try {
1616
+ this.port.postMessage({ t: "cancel", id });
1617
+ }
1618
+ catch { }
1619
+ p.reject(timeoutReason());
1620
+ }
1621
+ if (late)
1622
+ this.close("no answer from the transport thread");
1623
+ }
1515
1624
  release(p) {
1516
1625
  if (p.signal !== undefined && p.onAbort !== undefined)
1517
1626
  p.signal.removeEventListener("abort", p.onAbort);
1518
- if (p.backstop !== undefined)
1519
- clearTimeout(p.backstop);
1627
+ Atomics.store(this.acks, p.mark, 0);
1628
+ this.freeMarks.push(p.mark);
1520
1629
  if (this.inflight.size === 0)
1521
1630
  this.unref();
1522
1631
  }
@@ -1541,8 +1650,11 @@ class TransportThread {
1541
1650
  headers["content-length"] = Buffer.byteLength(init.body, "utf8");
1542
1651
  const id = this.nextId++;
1543
1652
  const tr = init.trace;
1653
+ const place = this.freeMarks.pop();
1654
+ if (place === undefined)
1655
+ return Promise.resolve(UNSENT);
1544
1656
  return new Promise((resolve, reject) => {
1545
- const p = { resolve, reject, method: init.method, replayable, acked: false, trace: tr };
1657
+ const p = { id, resolve, reject, method: init.method, replayable, mark: place, backstopAt: performance.now() + remaining + THREAD_GRACE_MS, trace: tr };
1546
1658
  const signal = init.signal;
1547
1659
  if (signal !== undefined) {
1548
1660
  p.signal = signal;
@@ -1551,47 +1663,59 @@ class TransportThread {
1551
1663
  return;
1552
1664
  this.inflight.delete(id);
1553
1665
  this.release(p);
1554
- try {
1555
- this.port.postMessage({ t: "cancel", id });
1666
+ const queued = this.outbox.findIndex((b) => b.id === id);
1667
+ if (queued >= 0)
1668
+ this.outbox.splice(queued, 1);
1669
+ else {
1670
+ try {
1671
+ this.port.postMessage({ t: "cancel", id });
1672
+ }
1673
+ catch { }
1556
1674
  }
1557
- catch { }
1558
1675
  reject(signal.reason);
1559
1676
  };
1560
1677
  signal.addEventListener("abort", p.onAbort, { once: true });
1561
1678
  }
1562
- p.backstop = setTimeout(() => {
1563
- this.poll();
1564
- if (!this.inflight.has(id))
1565
- return;
1566
- this.inflight.delete(id);
1567
- this.release(p);
1568
- try {
1569
- this.port.postMessage({ t: "cancel", id });
1570
- }
1571
- catch { }
1572
- reject(timeoutReason());
1573
- this.close("no answer from the transport thread");
1574
- }, remaining + THREAD_GRACE_MS);
1575
- safely(() => { p.backstop?.unref?.(); });
1576
1679
  this.inflight.set(id, p);
1577
1680
  this.ref();
1578
- try {
1579
- const paced = this.pace === "all" || (this.pace === "creates" && init.method === "POST" && CREATE_PATH.test(target.path));
1580
- this.port.postMessage({ t: "req", id, headers, sensitive: ["authorization"], body: init.body, timeoutMs: remaining, replayable, paced });
1681
+ const paced = this.pace === "all" || (this.pace === "creates" && init.method === "POST" && CREATE_PATH.test(target.path));
1682
+ this.outbox.push({ id, msg: { t: "req", id, mark: place, headers, sensitive: ["authorization"], body: init.body, timeoutMs: remaining, replayable, paced }, p, resolve, trace: tr });
1683
+ if (!this.turnOpen) {
1684
+ this.turnOpen = true;
1685
+ queueMicrotask(() => { this.turnOpen = false; this.handOver(); });
1686
+ this.handOver();
1581
1687
  }
1582
- catch {
1583
- this.inflight.delete(id);
1584
- this.release(p);
1585
- resolve(UNSENT);
1586
- return;
1688
+ else if (this.outbox.length >= OUTBOX_GROUP)
1689
+ this.handOver();
1690
+ });
1691
+ }
1692
+ handOver() {
1693
+ if (this.outbox.length === 0)
1694
+ return;
1695
+ const batch = this.outbox;
1696
+ this.outbox = [];
1697
+ try {
1698
+ this.port.postMessage(batch.length === 1 ? batch[0].msg : { t: "reqs", list: batch.map((b) => b.msg) });
1699
+ this.posts++;
1700
+ }
1701
+ catch {
1702
+ for (const b of batch) {
1703
+ if (!this.inflight.has(b.id))
1704
+ continue;
1705
+ this.inflight.delete(b.id);
1706
+ this.release(b.p);
1707
+ b.resolve(UNSENT);
1587
1708
  }
1588
- if (tr !== undefined) {
1589
- mark(tr, "sub");
1590
- tr.via = "thread";
1709
+ return;
1710
+ }
1711
+ for (const b of batch)
1712
+ if (b.trace !== undefined) {
1713
+ mark(b.trace, "sub");
1714
+ b.trace.via = "thread";
1591
1715
  }
1592
- });
1593
1716
  }
1594
1717
  }
1718
+ const OUTBOX_GROUP = 8;
1595
1719
  let transportThread;
1596
1720
  function threadFor(origin) {
1597
1721
  const t = transportThread;
@@ -1599,6 +1723,55 @@ function threadFor(origin) {
1599
1723
  return undefined;
1600
1724
  return t.ready() && !lookupReplaced() ? t : undefined;
1601
1725
  }
1726
+ function startingThread(origin) {
1727
+ const t = transportThread;
1728
+ if (t === undefined || t.origin !== origin || lookupReplaced())
1729
+ return undefined;
1730
+ return t.starting() ? t : undefined;
1731
+ }
1732
+ let raceTraced = false;
1733
+ function firstUp(t, pool, deadline, signal) {
1734
+ const first = !raceTraced;
1735
+ if (first) {
1736
+ raceTraced = true;
1737
+ traceEvent("h2:race");
1738
+ }
1739
+ const stop = new AbortController();
1740
+ const both = signal === undefined ? stop.signal : AbortSignal.any([signal, stop.signal]);
1741
+ return new Promise((resolve, reject) => {
1742
+ let done = false;
1743
+ const forget = t.whenStarted(() => {
1744
+ if (done)
1745
+ return;
1746
+ const ready = threadFor(t.origin);
1747
+ if (ready === undefined)
1748
+ return;
1749
+ done = true;
1750
+ if (first)
1751
+ traceEvent("h2:race-thread");
1752
+ stop.abort();
1753
+ resolve({ thread: ready });
1754
+ });
1755
+ pool.acquire(deadline, both).then((entry) => {
1756
+ if (done) {
1757
+ if (entry !== null)
1758
+ pool.end(entry);
1759
+ return;
1760
+ }
1761
+ done = true;
1762
+ forget();
1763
+ if (first)
1764
+ traceEvent("h2:race-pool");
1765
+ resolve({ entry });
1766
+ }, (err) => {
1767
+ if (done)
1768
+ return;
1769
+ done = true;
1770
+ forget();
1771
+ reject(err);
1772
+ });
1773
+ });
1774
+ }
1602
1775
  function ownFile() {
1603
1776
  const frame = (new Error().stack ?? "").split("\n")[1] ?? "";
1604
1777
  const m = /\((.*):\d+:\d+\)\s*$/.exec(frame) ?? /at (.*):\d+:\d+\s*$/.exec(frame);
@@ -1636,11 +1809,12 @@ function startTransportThread(origin, onFail) {
1636
1809
  const knobs = threadKnobs();
1637
1810
  const file = typeof seam.transportFile === "string" ? seam.transportFile : here.replace(/http2\.js$/, "transport-worker.js");
1638
1811
  const { port1, port2 } = new MessageChannel();
1812
+ const acks = new Int32Array(new SharedArrayBuffer(4 * THREAD_MARKS));
1639
1813
  const worker = new Worker(file.startsWith("file:") ? new URL(file) : file, {
1640
1814
  workerData: {
1641
- promigenceTransport: true, port: port2, origin: origin.origin, sessions: knobs.sessions, gapUs: knobs.gapUs,
1815
+ promigenceTransport: true, port: port2, acks, origin: origin.origin, sessions: knobs.sessions, gapUs: knobs.gapUs,
1642
1816
  firstUseMs: tuning.loadFirstUseMs, idleMs: IDLE_CLOSE_MS, connectTimeoutMs: CONNECT_TIMEOUT_MS,
1643
- lookupTimeoutMs: EARLY_TIMEOUT_MS, trace: TRACE_ON,
1817
+ lookupTimeoutMs: LOOKUP_FIRST_MS, lookupTries: LOOKUP_TRIES, lookupAtMs: LOOKUP_AT_MS, trace: TRACE_ON,
1644
1818
  servers: tuning.earlyServers ?? (Array.isArray(seam.servers) ? seam.servers : undefined),
1645
1819
  hostsFile: tuning.earlyHostsFile ?? (typeof seam.hostsFile === "string" ? seam.hostsFile : undefined),
1646
1820
  distrustedForTest: __caGuardInternals.testHashes(),
@@ -1651,13 +1825,18 @@ function startTransportThread(origin, onFail) {
1651
1825
  });
1652
1826
  worker.unref();
1653
1827
  port1.unref();
1654
- const t = new TransportThread(origin.origin, worker, port1);
1828
+ const t = new TransportThread(origin.origin, worker, port1, acks);
1655
1829
  t.pace = knobs.pace;
1656
1830
  t.onFail = onFail;
1657
1831
  worker.on("error", () => { t.poll(); t.close("error"); });
1658
1832
  worker.on("exit", () => { t.poll(); t.close("exit"); });
1659
1833
  transportThread = t;
1660
1834
  traceEvent("thread:start");
1835
+ onTraceFlush(() => {
1836
+ t.poll();
1837
+ for (const e of earlies.values())
1838
+ pollEarly(e);
1839
+ });
1661
1840
  }
1662
1841
  catch {
1663
1842
  transportThread = undefined;
@@ -1673,6 +1852,18 @@ function knownPrefix(url) {
1673
1852
  }
1674
1853
  return undefined;
1675
1854
  }
1855
+ let lastTargetUrl;
1856
+ let lastTargetRead;
1857
+ let targetReads = 0;
1858
+ function lastTarget(url) {
1859
+ if (url === lastTargetUrl)
1860
+ return lastTargetRead;
1861
+ targetReads++;
1862
+ const target = targetOf(url, knownPrefix(url), remember);
1863
+ lastTargetUrl = url;
1864
+ lastTargetRead = target;
1865
+ return target;
1866
+ }
1676
1867
  function remember(p) {
1677
1868
  for (let i = 0; i < prefixes.length; i++)
1678
1869
  if (prefixes[i].text === p.text)
@@ -1708,7 +1899,7 @@ export class Http2Transport {
1708
1899
  const deadline = performance.now() + init.timeoutMs;
1709
1900
  let target;
1710
1901
  try {
1711
- target = targetOf(url, knownPrefix(url), remember);
1902
+ target = lastTarget(url);
1712
1903
  }
1713
1904
  catch {
1714
1905
  return this.viaFetch(url, init, deadline);
@@ -1732,8 +1923,34 @@ export class Http2Transport {
1732
1923
  const pool = poolFor(target.origin);
1733
1924
  if (!pool.usable())
1734
1925
  return this.viaFetch(url, init, deadline);
1926
+ let raced = false;
1735
1927
  for (;;) {
1736
- const entry = pool.tryAcquire() ?? await pool.acquire(deadline, init.signal);
1928
+ let entry = pool.tryAcquire();
1929
+ if (entry === undefined) {
1930
+ const starting = raced || resends > 0 || replays > 0 ? undefined : startingThread(target.origin);
1931
+ if (starting !== undefined) {
1932
+ raced = true;
1933
+ const won = await firstUp(starting, pool, deadline, init.signal);
1934
+ if (won.thread !== undefined) {
1935
+ if (tr !== undefined)
1936
+ mark(tr, "race");
1937
+ const out = await won.thread.send(target, init, replayable, deadline);
1938
+ if (out === VIA_FETCH)
1939
+ return this.viaFetch(url, init, deadline);
1940
+ if (!(out instanceof HandBack))
1941
+ return out;
1942
+ resends = out.resends;
1943
+ replays = out.replays;
1944
+ if (tr !== undefined)
1945
+ delete tr.via;
1946
+ continue;
1947
+ }
1948
+ entry = won.entry;
1949
+ }
1950
+ else {
1951
+ entry = await pool.acquire(deadline, init.signal);
1952
+ }
1953
+ }
1737
1954
  if (tr !== undefined)
1738
1955
  mark(tr, "acq");
1739
1956
  if (entry === null)
@@ -1768,6 +1985,15 @@ export class Http2Transport {
1768
1985
  }
1769
1986
  }
1770
1987
  }
1988
+ let sharedFallback;
1989
+ let shared;
1990
+ export function sharedTransport(fallback) {
1991
+ if (shared === undefined || sharedFallback !== fallback) {
1992
+ shared = new Http2Transport(fallback);
1993
+ sharedFallback = fallback;
1994
+ }
1995
+ return shared;
1996
+ }
1771
1997
  export function warmTransport() {
1772
1998
  const res = toResponse(200, { "content-type": "application/json" }, Buffer.from("{}"), "POST");
1773
1999
  res.header("server-timing");
@@ -1780,6 +2006,9 @@ export function warmTransport() {
1780
2006
  exchange({ end() { } }, undefined, { origin: none, host: none, path: none }, undefined, 0).catch(() => { });
1781
2007
  }
1782
2008
  export const __http2Internals = {
2009
+ targetReads() {
2010
+ return targetReads;
2011
+ },
1783
2012
  pools() {
1784
2013
  return [...pools.values()].map((p) => ({
1785
2014
  origin: p.origin.origin,
@@ -1820,11 +2049,17 @@ export const __http2Internals = {
1820
2049
  if (t === undefined)
1821
2050
  return undefined;
1822
2051
  t.poll();
1823
- return { state: t.state, sessions: t.sessions, limit: t.limit, answered: t.answered, everReady: t.everReady, closedReason: t.closedReason };
2052
+ return { state: t.state, sessions: t.sessions, limit: t.limit, answered: t.answered, everReady: t.everReady, closedReason: t.closedReason, messages: t.messages, posts: t.posts, startedAt: t.startedAt };
1824
2053
  },
1825
2054
  threadKnobs(env) {
1826
2055
  return threadKnobs(env);
1827
2056
  },
2057
+ wedgeThreadForTest() {
2058
+ try {
2059
+ transportThread?.port.postMessage({ t: "wedge-for-test" });
2060
+ }
2061
+ catch { }
2062
+ },
1828
2063
  crashThreadForTest() {
1829
2064
  try {
1830
2065
  transportThread?.port.postMessage({ t: "crash-for-test" });
@@ -1863,6 +2098,15 @@ function isMainThread() {
1863
2098
  return false;
1864
2099
  }
1865
2100
  }
2101
+ function standby(url) {
2102
+ try {
2103
+ traceEvent("h2:standby");
2104
+ warmTls();
2105
+ poolFor(url.origin).warm(tuning.loadFirstUseMs);
2106
+ }
2107
+ catch {
2108
+ }
2109
+ }
1866
2110
  function connectAtLoad() {
1867
2111
  try {
1868
2112
  if (!isNode() || Number(process.versions.node.split(".")[0]) < 22)
@@ -1883,6 +2127,8 @@ function connectAtLoad() {
1883
2127
  if (url.protocol === "https:" && !customDispatcher() && earlyPlatform() && v4First() && !threadOff && earlySeam().transportThread !== false) {
1884
2128
  startTransportThread(url, () => { if (loadTurnRan)
1885
2129
  warmAtLoad(); });
2130
+ if (isIP(host) === 0)
2131
+ earlyLookupFor(host);
1886
2132
  }
1887
2133
  else if (url.protocol === "https:" && !customDispatcher() && isIP(host) === 0 && earlyPlatform() && v4First())
1888
2134
  earlyLookupFor(host);
@@ -1901,10 +2147,27 @@ function connectAtLoad() {
1901
2147
  const url = new URL(raw ? raw : DEFAULT_API_URL);
1902
2148
  if (url.protocol !== "https:" || customDispatcher())
1903
2149
  return;
1904
- if (transportThread !== undefined && transportThread.origin === url.origin) {
1905
- transportThread.poll();
1906
- if (transportThread.state !== "closed" || transportThread.everReady)
2150
+ const t = transportThread;
2151
+ if (t !== undefined && t.origin === url.origin) {
2152
+ t.poll();
2153
+ if (t.everReady)
1907
2154
  return;
2155
+ if (t.state !== "closed") {
2156
+ const check = () => {
2157
+ t.poll();
2158
+ if (t.everReady || t.state === "closed")
2159
+ return;
2160
+ const wait = t.startedAt + tuning.standbyAfterMs - performance.now();
2161
+ if (wait > 0) {
2162
+ const timer = setTimeout(check, wait);
2163
+ timer.unref?.();
2164
+ return;
2165
+ }
2166
+ standby(url);
2167
+ };
2168
+ check();
2169
+ return;
2170
+ }
1908
2171
  }
1909
2172
  warmTls();
1910
2173
  poolFor(url.origin).warm(tuning.loadFirstUseMs);