@promigence/sdk 0.1.0-beta.13 → 0.1.0-beta.15

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,7 +846,9 @@ 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;
849
+ const LOOKUP_FIRST_MS = 150;
850
+ const LOOKUP_TRIES = 3;
851
+ const LOOKUP_AT_MS = [0, 100, 250];
848
852
  const DNS_AT_LOAD = (() => {
849
853
  try {
850
854
  return nodeDns();
@@ -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)
@@ -1327,6 +1351,8 @@ class TransportThread {
1327
1351
  everReady = false;
1328
1352
  closedReason = "";
1329
1353
  onFail;
1354
+ startedAt = performance.now();
1355
+ startWaiters = [];
1330
1356
  inflight = new Map();
1331
1357
  nextId = 1;
1332
1358
  listening = false;
@@ -1366,10 +1392,41 @@ class TransportThread {
1366
1392
  }
1367
1393
  }
1368
1394
  ready() {
1369
- if (!this.listening && this.state !== "closed")
1395
+ if ((!this.listening || this.state === "starting") && this.state !== "closed")
1370
1396
  this.poll();
1371
1397
  return this.state === "ready" && this.sessions > 0 && this.inflight.size < this.sessions * this.limit && this.freeMarks.length > 0;
1372
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(); });
1429
+ }
1373
1430
  onMessage(m) {
1374
1431
  const msg = m;
1375
1432
  this.messages++;
@@ -1392,8 +1449,16 @@ class TransportThread {
1392
1449
  if (typeof c.index === "number" && typeof c.addr === "string" && typeof c.localPort === "number")
1393
1450
  traceEventAt(`thread:conn${c.index} ${c.addr} ${c.localPort}`, t);
1394
1451
  }
1452
+ if (first)
1453
+ this.started();
1395
1454
  }
1396
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
+ }
1397
1462
  case "sessions":
1398
1463
  if (typeof msg.sessions === "number")
1399
1464
  this.sessions = msg.sessions;
@@ -1404,6 +1469,7 @@ class TransportThread {
1404
1469
  if (this.closedReason === "")
1405
1470
  this.closedReason = msg.reason ?? "draining";
1406
1471
  traceEvent("thread:draining");
1472
+ this.started();
1407
1473
  }
1408
1474
  return;
1409
1475
  case "closed":
@@ -1484,6 +1550,7 @@ class TransportThread {
1484
1550
  this.port.postMessage({ t: "drain" });
1485
1551
  }
1486
1552
  catch { }
1553
+ this.started();
1487
1554
  }
1488
1555
  close(reason) {
1489
1556
  if (this.state === "closed")
@@ -1492,6 +1559,7 @@ class TransportThread {
1492
1559
  if (this.closedReason === "")
1493
1560
  this.closedReason = reason;
1494
1561
  traceEvent("thread:closed");
1562
+ this.started();
1495
1563
  this.outbox = [];
1496
1564
  for (const [id, p] of [...this.inflight]) {
1497
1565
  const sent = this.acked(p);
@@ -1514,11 +1582,7 @@ class TransportThread {
1514
1582
  }
1515
1583
  }
1516
1584
  ref() {
1517
- if (!this.listening) {
1518
- this.listening = true;
1519
- this.poll();
1520
- this.port.on("message", (m) => { this.onMessage(m); });
1521
- }
1585
+ this.listen();
1522
1586
  if (this.refd)
1523
1587
  return;
1524
1588
  this.refd = true;
@@ -1659,6 +1723,55 @@ function threadFor(origin) {
1659
1723
  return undefined;
1660
1724
  return t.ready() && !lookupReplaced() ? t : undefined;
1661
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
+ }
1662
1775
  function ownFile() {
1663
1776
  const frame = (new Error().stack ?? "").split("\n")[1] ?? "";
1664
1777
  const m = /\((.*):\d+:\d+\)\s*$/.exec(frame) ?? /at (.*):\d+:\d+\s*$/.exec(frame);
@@ -1701,7 +1814,7 @@ function startTransportThread(origin, onFail) {
1701
1814
  workerData: {
1702
1815
  promigenceTransport: true, port: port2, acks, origin: origin.origin, sessions: knobs.sessions, gapUs: knobs.gapUs,
1703
1816
  firstUseMs: tuning.loadFirstUseMs, idleMs: IDLE_CLOSE_MS, connectTimeoutMs: CONNECT_TIMEOUT_MS,
1704
- lookupTimeoutMs: EARLY_TIMEOUT_MS, trace: TRACE_ON,
1817
+ lookupTimeoutMs: LOOKUP_FIRST_MS, lookupTries: LOOKUP_TRIES, lookupAtMs: LOOKUP_AT_MS, trace: TRACE_ON,
1705
1818
  servers: tuning.earlyServers ?? (Array.isArray(seam.servers) ? seam.servers : undefined),
1706
1819
  hostsFile: tuning.earlyHostsFile ?? (typeof seam.hostsFile === "string" ? seam.hostsFile : undefined),
1707
1820
  distrustedForTest: __caGuardInternals.testHashes(),
@@ -1719,6 +1832,11 @@ function startTransportThread(origin, onFail) {
1719
1832
  worker.on("exit", () => { t.poll(); t.close("exit"); });
1720
1833
  transportThread = t;
1721
1834
  traceEvent("thread:start");
1835
+ onTraceFlush(() => {
1836
+ t.poll();
1837
+ for (const e of earlies.values())
1838
+ pollEarly(e);
1839
+ });
1722
1840
  }
1723
1841
  catch {
1724
1842
  transportThread = undefined;
@@ -1805,8 +1923,34 @@ export class Http2Transport {
1805
1923
  const pool = poolFor(target.origin);
1806
1924
  if (!pool.usable())
1807
1925
  return this.viaFetch(url, init, deadline);
1926
+ let raced = false;
1808
1927
  for (;;) {
1809
- 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
+ }
1810
1954
  if (tr !== undefined)
1811
1955
  mark(tr, "acq");
1812
1956
  if (entry === null)
@@ -1905,7 +2049,7 @@ export const __http2Internals = {
1905
2049
  if (t === undefined)
1906
2050
  return undefined;
1907
2051
  t.poll();
1908
- 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 };
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 };
1909
2053
  },
1910
2054
  threadKnobs(env) {
1911
2055
  return threadKnobs(env);
@@ -1954,6 +2098,15 @@ function isMainThread() {
1954
2098
  return false;
1955
2099
  }
1956
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
+ }
1957
2110
  function connectAtLoad() {
1958
2111
  try {
1959
2112
  if (!isNode() || Number(process.versions.node.split(".")[0]) < 22)
@@ -1974,6 +2127,8 @@ function connectAtLoad() {
1974
2127
  if (url.protocol === "https:" && !customDispatcher() && earlyPlatform() && v4First() && !threadOff && earlySeam().transportThread !== false) {
1975
2128
  startTransportThread(url, () => { if (loadTurnRan)
1976
2129
  warmAtLoad(); });
2130
+ if (isIP(host) === 0)
2131
+ earlyLookupFor(host);
1977
2132
  }
1978
2133
  else if (url.protocol === "https:" && !customDispatcher() && isIP(host) === 0 && earlyPlatform() && v4First())
1979
2134
  earlyLookupFor(host);
@@ -1992,10 +2147,27 @@ function connectAtLoad() {
1992
2147
  const url = new URL(raw ? raw : DEFAULT_API_URL);
1993
2148
  if (url.protocol !== "https:" || customDispatcher())
1994
2149
  return;
1995
- if (transportThread !== undefined && transportThread.origin === url.origin) {
1996
- transportThread.poll();
1997
- 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)
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();
1998
2169
  return;
2170
+ }
1999
2171
  }
2000
2172
  warmTls();
2001
2173
  poolFor(url.origin).warm(tuning.loadFirstUseMs);