@promigence/sdk 0.1.0-beta.13 → 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.
@@ -1,5 +1,5 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.13";
2
+ export declare const VERSION = "0.1.0-beta.14";
3
3
  export declare const DOCS_URL = "https://promigence.ai";
4
4
  /** The error reference; each code is an anchor on it (the API's `docs` link has the same form). */
5
5
  export declare const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
package/dist/cjs/core.js CHANGED
@@ -7,7 +7,7 @@ exports.fixBeyondMessage = fixBeyondMessage;
7
7
  exports.previewsOff = previewsOff;
8
8
  exports.exitForCode = exitForCode;
9
9
  const hint_js_1 = require("./hint.js");
10
- exports.VERSION = "0.1.0-beta.13";
10
+ exports.VERSION = "0.1.0-beta.14";
11
11
  exports.DOCS_URL = "https://promigence.ai";
12
12
  exports.ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
13
13
  exports.DEFAULT_API_URL = "https://api.promigence.ai/v1";
@@ -189,6 +189,7 @@ export declare const __http2Internals: {
189
189
  closedReason: string;
190
190
  messages: number;
191
191
  posts: number;
192
+ startedAt: number;
192
193
  } | undefined;
193
194
  /** The transport thread's number of connections and spacing between new requests on one, for an environment (`threadKnobs`). */
194
195
  threadKnobs(env: NodeJS.ProcessEnv): {
package/dist/cjs/http2.js CHANGED
@@ -21,6 +21,7 @@ const MAX_SESSIONS = 4;
21
21
  const FIRST_SESSIONS = 2;
22
22
  const BURST_MIN = 8;
23
23
  const HOLD_MS = 10;
24
+ const STANDBY_AFTER_MS = 300;
24
25
  const LOAD_FIRST_USE_MS = 120_000;
25
26
  const CONNECT_TIMEOUT_MS = 10_000;
26
27
  const IDLE_CLOSE_MS = 10_000;
@@ -572,6 +573,7 @@ exports.tuning = {
572
573
  holdMs: HOLD_MS,
573
574
  burstMin: BURST_MIN,
574
575
  loadFirstUseMs: LOAD_FIRST_USE_MS,
576
+ standbyAfterMs: STANDBY_AFTER_MS,
575
577
  earlyPlatform: undefined,
576
578
  earlyServers: undefined,
577
579
  earlyHostsFile: undefined,
@@ -855,7 +857,9 @@ function apiLookup(asked, observe, ipv4First = v4First(), earlyOk = true) {
855
857
  const earlies = new Map();
856
858
  const MAX_EARLIES = 4;
857
859
  const EARLY_MAX_AGE_MS = 60_000;
858
- const EARLY_TIMEOUT_MS = 2_000;
860
+ const LOOKUP_FIRST_MS = 150;
861
+ const LOOKUP_TRIES = 3;
862
+ const LOOKUP_AT_MS = [0, 100, 250];
859
863
  const DNS_AT_LOAD = (() => {
860
864
  try {
861
865
  return nodeDns();
@@ -876,8 +880,8 @@ function lookupReplaced() {
876
880
  const EARLY_WORKER = `
877
881
  const { workerData } = require("node:worker_threads");
878
882
  const { isIPv4 } = require("node:net");
879
- const { port, host, timeoutMs, servers, hostsFile } = workerData;
880
- const done = (m) => { try { port.postMessage(m); } catch {} try { port.close(); } catch {} };
883
+ const { port, host, timeoutMs, tries, atMs, servers, hostsFile } = workerData;
884
+ const done = (m) => { m.hr = Number(process.hrtime.bigint()) / 1e6; try { port.postMessage(m); } catch {} try { port.close(); } catch {} };
881
885
  try {
882
886
  let named = false;
883
887
  const v4 = [];
@@ -891,12 +895,28 @@ try {
891
895
  } catch {}
892
896
  if (named) done(v4.length > 0 ? { host, addrs: v4, ttl: 60, at: Date.now() } : { host, none: "hosts" });
893
897
  else {
894
- const r = new (require("node:dns").Resolver)({ timeout: timeoutMs, tries: 1 });
895
- if (Array.isArray(servers) && servers.length > 0) r.setServers(servers);
896
- r.resolve4(host, { ttl: true }, (err, recs) => {
897
- if (err || !Array.isArray(recs) || recs.length === 0) return done({ host, none: (err && err.code) || "empty" });
898
- done({ host, addrs: recs.map((x) => x.address), ttl: Math.min(...recs.map((x) => x.ttl)), at: Date.now() });
899
- });
898
+ const { Resolver } = require("node:dns");
899
+ const rs = [];
900
+ const timers = [];
901
+ let fin = false, asked = 0, failed = 0;
902
+ 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); };
903
+ const gaveUp = (why) => { if (++failed < asked) return; if (asked < atMs.length) ask(); else finish({ host, none: why }); };
904
+ const ask = () => {
905
+ if (fin || asked >= atMs.length) return;
906
+ const k = asked++;
907
+ try {
908
+ const r = new Resolver({ timeout: timeoutMs, tries });
909
+ const list = Array.isArray(servers) && servers.length > 0 ? servers : r.getServers();
910
+ if (list.length > 0) r.setServers(list.map((_, i) => list[(i + k) % list.length]));
911
+ rs.push(r);
912
+ r.resolve4(host, { ttl: true }, (err, recs) => {
913
+ if (fin) return;
914
+ 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() });
915
+ gaveUp((err && err.code) || "empty");
916
+ });
917
+ } catch { gaveUp("error"); }
918
+ };
919
+ for (const ms of atMs) { if (ms <= 0) ask(); else timers.push(setTimeout(ask, ms)); }
900
920
  }
901
921
  } catch { done({ host, none: "error" }); }
902
922
  `;
@@ -927,7 +947,7 @@ function earlyLookupFor(host) {
927
947
  const worker = new node_worker_threads_1.Worker(EARLY_WORKER, {
928
948
  eval: true,
929
949
  workerData: {
930
- port: port2, host, timeoutMs: EARLY_TIMEOUT_MS,
950
+ port: port2, host, timeoutMs: LOOKUP_FIRST_MS, tries: LOOKUP_TRIES, atMs: LOOKUP_AT_MS,
931
951
  servers: exports.tuning.earlyServers ?? (Array.isArray(earlySeam().servers) ? earlySeam().servers : undefined),
932
952
  hostsFile: exports.tuning.earlyHostsFile ?? (typeof earlySeam().hostsFile === "string" ? earlySeam().hostsFile : undefined),
933
953
  },
@@ -975,6 +995,10 @@ function settleEarly(e, message) {
975
995
  else {
976
996
  e.state = "none";
977
997
  }
998
+ if (trace_js_1.TRACE_ON) {
999
+ const at = typeof m?.hr === "number" ? m.hr - (Number(process.hrtime.bigint()) / 1e6 - performance.now()) : performance.now();
1000
+ (0, trace_js_1.traceEventAt)(e.state === "answered" ? `dns:early-answer ${addrs.length}` : `dns:early-none ${typeof m?.none === "string" ? m.none : "gone"}`, at);
1001
+ }
978
1002
  safely(() => { e.port.close(); });
979
1003
  const waiters = e.waiters.splice(0);
980
1004
  for (const w of waiters)
@@ -1338,6 +1362,8 @@ class TransportThread {
1338
1362
  everReady = false;
1339
1363
  closedReason = "";
1340
1364
  onFail;
1365
+ startedAt = performance.now();
1366
+ startWaiters = [];
1341
1367
  inflight = new Map();
1342
1368
  nextId = 1;
1343
1369
  listening = false;
@@ -1377,10 +1403,41 @@ class TransportThread {
1377
1403
  }
1378
1404
  }
1379
1405
  ready() {
1380
- if (!this.listening && this.state !== "closed")
1406
+ if ((!this.listening || this.state === "starting") && this.state !== "closed")
1381
1407
  this.poll();
1382
1408
  return this.state === "ready" && this.sessions > 0 && this.inflight.size < this.sessions * this.limit && this.freeMarks.length > 0;
1383
1409
  }
1410
+ starting() {
1411
+ if (this.state === "starting")
1412
+ this.poll();
1413
+ return this.state === "starting";
1414
+ }
1415
+ whenStarted(cb) {
1416
+ if (!this.starting()) {
1417
+ queueMicrotask(cb);
1418
+ return () => { };
1419
+ }
1420
+ this.startWaiters.push(cb);
1421
+ this.listen();
1422
+ return () => {
1423
+ const i = this.startWaiters.indexOf(cb);
1424
+ if (i >= 0)
1425
+ this.startWaiters.splice(i, 1);
1426
+ };
1427
+ }
1428
+ started() {
1429
+ for (const cb of this.startWaiters.splice(0))
1430
+ safely(cb);
1431
+ }
1432
+ listen() {
1433
+ if (this.listening)
1434
+ return;
1435
+ this.listening = true;
1436
+ this.poll();
1437
+ this.port.on("message", (m) => { this.onMessage(m); });
1438
+ if (!this.refd)
1439
+ safely(() => { this.port.unref(); });
1440
+ }
1384
1441
  onMessage(m) {
1385
1442
  const msg = m;
1386
1443
  this.messages++;
@@ -1403,8 +1460,16 @@ class TransportThread {
1403
1460
  if (typeof c.index === "number" && typeof c.addr === "string" && typeof c.localPort === "number")
1404
1461
  (0, trace_js_1.traceEventAt)(`thread:conn${c.index} ${c.addr} ${c.localPort}`, t);
1405
1462
  }
1463
+ if (first)
1464
+ this.started();
1406
1465
  }
1407
1466
  return;
1467
+ case "ev": {
1468
+ const e = msg;
1469
+ if (typeof e.n === "string" && typeof e.hr === "number")
1470
+ (0, trace_js_1.traceEventAt)(`thread:w-${e.n}`, e.hr - this.hrOffset);
1471
+ return;
1472
+ }
1408
1473
  case "sessions":
1409
1474
  if (typeof msg.sessions === "number")
1410
1475
  this.sessions = msg.sessions;
@@ -1415,6 +1480,7 @@ class TransportThread {
1415
1480
  if (this.closedReason === "")
1416
1481
  this.closedReason = msg.reason ?? "draining";
1417
1482
  (0, trace_js_1.traceEvent)("thread:draining");
1483
+ this.started();
1418
1484
  }
1419
1485
  return;
1420
1486
  case "closed":
@@ -1495,6 +1561,7 @@ class TransportThread {
1495
1561
  this.port.postMessage({ t: "drain" });
1496
1562
  }
1497
1563
  catch { }
1564
+ this.started();
1498
1565
  }
1499
1566
  close(reason) {
1500
1567
  if (this.state === "closed")
@@ -1503,6 +1570,7 @@ class TransportThread {
1503
1570
  if (this.closedReason === "")
1504
1571
  this.closedReason = reason;
1505
1572
  (0, trace_js_1.traceEvent)("thread:closed");
1573
+ this.started();
1506
1574
  this.outbox = [];
1507
1575
  for (const [id, p] of [...this.inflight]) {
1508
1576
  const sent = this.acked(p);
@@ -1525,11 +1593,7 @@ class TransportThread {
1525
1593
  }
1526
1594
  }
1527
1595
  ref() {
1528
- if (!this.listening) {
1529
- this.listening = true;
1530
- this.poll();
1531
- this.port.on("message", (m) => { this.onMessage(m); });
1532
- }
1596
+ this.listen();
1533
1597
  if (this.refd)
1534
1598
  return;
1535
1599
  this.refd = true;
@@ -1670,6 +1734,55 @@ function threadFor(origin) {
1670
1734
  return undefined;
1671
1735
  return t.ready() && !lookupReplaced() ? t : undefined;
1672
1736
  }
1737
+ function startingThread(origin) {
1738
+ const t = transportThread;
1739
+ if (t === undefined || t.origin !== origin || lookupReplaced())
1740
+ return undefined;
1741
+ return t.starting() ? t : undefined;
1742
+ }
1743
+ let raceTraced = false;
1744
+ function firstUp(t, pool, deadline, signal) {
1745
+ const first = !raceTraced;
1746
+ if (first) {
1747
+ raceTraced = true;
1748
+ (0, trace_js_1.traceEvent)("h2:race");
1749
+ }
1750
+ const stop = new AbortController();
1751
+ const both = signal === undefined ? stop.signal : AbortSignal.any([signal, stop.signal]);
1752
+ return new Promise((resolve, reject) => {
1753
+ let done = false;
1754
+ const forget = t.whenStarted(() => {
1755
+ if (done)
1756
+ return;
1757
+ const ready = threadFor(t.origin);
1758
+ if (ready === undefined)
1759
+ return;
1760
+ done = true;
1761
+ if (first)
1762
+ (0, trace_js_1.traceEvent)("h2:race-thread");
1763
+ stop.abort();
1764
+ resolve({ thread: ready });
1765
+ });
1766
+ pool.acquire(deadline, both).then((entry) => {
1767
+ if (done) {
1768
+ if (entry !== null)
1769
+ pool.end(entry);
1770
+ return;
1771
+ }
1772
+ done = true;
1773
+ forget();
1774
+ if (first)
1775
+ (0, trace_js_1.traceEvent)("h2:race-pool");
1776
+ resolve({ entry });
1777
+ }, (err) => {
1778
+ if (done)
1779
+ return;
1780
+ done = true;
1781
+ forget();
1782
+ reject(err);
1783
+ });
1784
+ });
1785
+ }
1673
1786
  function ownFile() {
1674
1787
  const frame = (new Error().stack ?? "").split("\n")[1] ?? "";
1675
1788
  const m = /\((.*):\d+:\d+\)\s*$/.exec(frame) ?? /at (.*):\d+:\d+\s*$/.exec(frame);
@@ -1712,7 +1825,7 @@ function startTransportThread(origin, onFail) {
1712
1825
  workerData: {
1713
1826
  promigenceTransport: true, port: port2, acks, origin: origin.origin, sessions: knobs.sessions, gapUs: knobs.gapUs,
1714
1827
  firstUseMs: exports.tuning.loadFirstUseMs, idleMs: IDLE_CLOSE_MS, connectTimeoutMs: CONNECT_TIMEOUT_MS,
1715
- lookupTimeoutMs: EARLY_TIMEOUT_MS, trace: trace_js_1.TRACE_ON,
1828
+ lookupTimeoutMs: LOOKUP_FIRST_MS, lookupTries: LOOKUP_TRIES, lookupAtMs: LOOKUP_AT_MS, trace: trace_js_1.TRACE_ON,
1716
1829
  servers: exports.tuning.earlyServers ?? (Array.isArray(seam.servers) ? seam.servers : undefined),
1717
1830
  hostsFile: exports.tuning.earlyHostsFile ?? (typeof seam.hostsFile === "string" ? seam.hostsFile : undefined),
1718
1831
  distrustedForTest: ca_guard_js_1.__caGuardInternals.testHashes(),
@@ -1730,6 +1843,11 @@ function startTransportThread(origin, onFail) {
1730
1843
  worker.on("exit", () => { t.poll(); t.close("exit"); });
1731
1844
  transportThread = t;
1732
1845
  (0, trace_js_1.traceEvent)("thread:start");
1846
+ (0, trace_js_1.onTraceFlush)(() => {
1847
+ t.poll();
1848
+ for (const e of earlies.values())
1849
+ pollEarly(e);
1850
+ });
1733
1851
  }
1734
1852
  catch {
1735
1853
  transportThread = undefined;
@@ -1816,8 +1934,34 @@ class Http2Transport {
1816
1934
  const pool = poolFor(target.origin);
1817
1935
  if (!pool.usable())
1818
1936
  return this.viaFetch(url, init, deadline);
1937
+ let raced = false;
1819
1938
  for (;;) {
1820
- const entry = pool.tryAcquire() ?? await pool.acquire(deadline, init.signal);
1939
+ let entry = pool.tryAcquire();
1940
+ if (entry === undefined) {
1941
+ const starting = raced || resends > 0 || replays > 0 ? undefined : startingThread(target.origin);
1942
+ if (starting !== undefined) {
1943
+ raced = true;
1944
+ const won = await firstUp(starting, pool, deadline, init.signal);
1945
+ if (won.thread !== undefined) {
1946
+ if (tr !== undefined)
1947
+ (0, trace_js_1.mark)(tr, "race");
1948
+ const out = await won.thread.send(target, init, replayable, deadline);
1949
+ if (out === VIA_FETCH)
1950
+ return this.viaFetch(url, init, deadline);
1951
+ if (!(out instanceof HandBack))
1952
+ return out;
1953
+ resends = out.resends;
1954
+ replays = out.replays;
1955
+ if (tr !== undefined)
1956
+ delete tr.via;
1957
+ continue;
1958
+ }
1959
+ entry = won.entry;
1960
+ }
1961
+ else {
1962
+ entry = await pool.acquire(deadline, init.signal);
1963
+ }
1964
+ }
1821
1965
  if (tr !== undefined)
1822
1966
  (0, trace_js_1.mark)(tr, "acq");
1823
1967
  if (entry === null)
@@ -1917,7 +2061,7 @@ exports.__http2Internals = {
1917
2061
  if (t === undefined)
1918
2062
  return undefined;
1919
2063
  t.poll();
1920
- 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 };
2064
+ 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 };
1921
2065
  },
1922
2066
  threadKnobs(env) {
1923
2067
  return threadKnobs(env);
@@ -1966,6 +2110,15 @@ function isMainThread() {
1966
2110
  return false;
1967
2111
  }
1968
2112
  }
2113
+ function standby(url) {
2114
+ try {
2115
+ (0, trace_js_1.traceEvent)("h2:standby");
2116
+ warmTls();
2117
+ poolFor(url.origin).warm(exports.tuning.loadFirstUseMs);
2118
+ }
2119
+ catch {
2120
+ }
2121
+ }
1969
2122
  function connectAtLoad() {
1970
2123
  try {
1971
2124
  if (!isNode() || Number(process.versions.node.split(".")[0]) < 22)
@@ -1986,6 +2139,8 @@ function connectAtLoad() {
1986
2139
  if (url.protocol === "https:" && !(0, dispatcher_js_1.customDispatcher)() && earlyPlatform() && v4First() && !threadOff && earlySeam().transportThread !== false) {
1987
2140
  startTransportThread(url, () => { if (loadTurnRan)
1988
2141
  warmAtLoad(); });
2142
+ if ((0, node_net_1.isIP)(host) === 0)
2143
+ earlyLookupFor(host);
1989
2144
  }
1990
2145
  else if (url.protocol === "https:" && !(0, dispatcher_js_1.customDispatcher)() && (0, node_net_1.isIP)(host) === 0 && earlyPlatform() && v4First())
1991
2146
  earlyLookupFor(host);
@@ -2004,10 +2159,27 @@ function connectAtLoad() {
2004
2159
  const url = new URL(raw ? raw : core_js_1.DEFAULT_API_URL);
2005
2160
  if (url.protocol !== "https:" || (0, dispatcher_js_1.customDispatcher)())
2006
2161
  return;
2007
- if (transportThread !== undefined && transportThread.origin === url.origin) {
2008
- transportThread.poll();
2009
- if (transportThread.state !== "closed" || transportThread.everReady)
2162
+ const t = transportThread;
2163
+ if (t !== undefined && t.origin === url.origin) {
2164
+ t.poll();
2165
+ if (t.everReady)
2166
+ return;
2167
+ if (t.state !== "closed") {
2168
+ const check = () => {
2169
+ t.poll();
2170
+ if (t.everReady || t.state === "closed")
2171
+ return;
2172
+ const wait = t.startedAt + exports.tuning.standbyAfterMs - performance.now();
2173
+ if (wait > 0) {
2174
+ const timer = setTimeout(check, wait);
2175
+ timer.unref?.();
2176
+ return;
2177
+ }
2178
+ standby(url);
2179
+ };
2180
+ check();
2010
2181
  return;
2182
+ }
2011
2183
  }
2012
2184
  warmTls();
2013
2185
  poolFor(url.origin).warm(exports.tuning.loadFirstUseMs);
@@ -31,3 +31,5 @@ export declare function mark(s: TraceStamp | undefined, phase: string): void;
31
31
  export declare function traceEvent(name: string): void;
32
32
  /** A process event that happened at `t` (`performance.now()` terms), recorded now: one another thread reported. */
33
33
  export declare function traceEventAt(name: string, t: number): void;
34
+ /** `fn` runs once at exit, before the trace is written (tracing on only). */
35
+ export declare function onTraceFlush(fn: () => void): void;
package/dist/cjs/trace.js CHANGED
@@ -6,6 +6,7 @@ exports.traceBegin = traceBegin;
6
6
  exports.mark = mark;
7
7
  exports.traceEvent = traceEvent;
8
8
  exports.traceEventAt = traceEventAt;
9
+ exports.onTraceFlush = onTraceFlush;
9
10
  const node_fs_1 = require("node:fs");
10
11
  const core_js_1 = require("./core.js");
11
12
  exports.TRACE_ON = (() => {
@@ -68,7 +69,23 @@ function modulePath() {
68
69
  const m = /\((.*):\d+:\d+\)\s*$/.exec(frame) ?? /at (.*):\d+:\d+\s*$/.exec(frame);
69
70
  return m?.[1] ?? "";
70
71
  }
72
+ const beforeFlush = [];
73
+ function onTraceFlush(fn) {
74
+ if (!exports.TRACE_ON)
75
+ return;
76
+ beforeFlush.push(fn);
77
+ if (!hooked) {
78
+ hooked = true;
79
+ process.once("exit", flush);
80
+ }
81
+ }
71
82
  function flush() {
83
+ for (const fn of beforeFlush.splice(0)) {
84
+ try {
85
+ fn();
86
+ }
87
+ catch { }
88
+ }
72
89
  const head = {
73
90
  trace: "promigence-sdk",
74
91
  version: core_js_1.VERSION,
@@ -36,6 +36,11 @@ function start(init) {
36
36
  }
37
37
  catch { }
38
38
  };
39
+ const ev = (n) => {
40
+ if (init.trace === true)
41
+ post({ t: "ev", n, hr: hrNow() });
42
+ };
43
+ ev("boot");
39
44
  const live = () => sessions.filter((e) => !e.retired && !e.session.closed && !e.session.destroyed);
40
45
  const end = (reason) => {
41
46
  if (closed)
@@ -96,14 +101,14 @@ function start(init) {
96
101
  catch {
97
102
  context = undefined;
98
103
  }
99
- const connect = (address) => new Promise((resolve) => {
104
+ const connect = (address, k) => new Promise((resolve) => {
100
105
  let done = false;
101
106
  const finish = (e) => { if (!done) {
102
107
  done = true;
103
108
  clearTimeout(timer);
104
109
  resolve(e);
105
110
  } };
106
- const timer = setTimeout(() => { try {
111
+ const timer = setTimeout(() => { ev(`timeout${k}`); try {
107
112
  socket.destroy();
108
113
  }
109
114
  catch { } finish(null); }, init.connectTimeoutMs);
@@ -115,8 +120,11 @@ function start(init) {
115
120
  checkServerIdentity: ca_guard_js_1.guardCertChain,
116
121
  ...(context === undefined ? {} : { secureContext: context }),
117
122
  });
118
- socket.once("error", () => finish(null));
123
+ if (init.trace === true)
124
+ socket.once("connect", () => { ev(`tcp${k}`); });
125
+ socket.once("error", (e) => { ev(`fail${k} ${e?.code ?? "error"}`); finish(null); });
119
126
  socket.once("secureConnect", () => {
127
+ ev(`tls${k}`);
120
128
  if (socket.alpnProtocol !== "h2") {
121
129
  socket.destroy();
122
130
  finish(null);
@@ -169,14 +177,66 @@ function start(init) {
169
177
  resolve(v4);
170
178
  return;
171
179
  }
172
- try {
173
- const r = new dns.Resolver({ timeout: init.lookupTimeoutMs, tries: 1 });
174
- if (Array.isArray(init.servers) && init.servers.length > 0)
175
- r.setServers(init.servers);
176
- r.resolve4(name, (err, addrs) => resolve(!err && Array.isArray(addrs) ? addrs : []));
177
- }
178
- catch {
179
- resolve([]);
180
+ const at = init.lookupAtMs.length > 0 ? init.lookupAtMs : [0];
181
+ const resolvers = [];
182
+ const timers = [];
183
+ let done = false;
184
+ let asked = 0;
185
+ let failed = 0;
186
+ const finish = (addrs) => {
187
+ if (done)
188
+ return;
189
+ done = true;
190
+ for (const t of timers)
191
+ clearTimeout(t);
192
+ for (const r of resolvers) {
193
+ try {
194
+ r.cancel();
195
+ }
196
+ catch { }
197
+ }
198
+ resolve(addrs);
199
+ };
200
+ const gaveUp = () => {
201
+ if (++failed < asked)
202
+ return;
203
+ if (asked < at.length)
204
+ ask();
205
+ else
206
+ finish([]);
207
+ };
208
+ const ask = () => {
209
+ if (done || asked >= at.length)
210
+ return;
211
+ const k = asked++;
212
+ try {
213
+ const r = new dns.Resolver({ timeout: init.lookupTimeoutMs, tries: init.lookupTries });
214
+ const list = Array.isArray(init.servers) && init.servers.length > 0 ? init.servers : r.getServers();
215
+ if (list.length > 0)
216
+ r.setServers(list.map((_, i) => list[(i + k) % list.length]));
217
+ resolvers.push(r);
218
+ ev(`lookup${k + 1}`);
219
+ r.resolve4(name, (err, addrs) => {
220
+ if (done)
221
+ return;
222
+ if (!err && Array.isArray(addrs) && addrs.length > 0) {
223
+ ev(`addrs${k + 1} ${addrs.length}`);
224
+ finish(addrs);
225
+ return;
226
+ }
227
+ ev(`nolookup${k + 1} ${err?.code ?? "empty"}`);
228
+ gaveUp();
229
+ });
230
+ }
231
+ catch {
232
+ gaveUp();
233
+ }
234
+ };
235
+ for (const ms of at) {
236
+ if (ms <= 0)
237
+ ask();
238
+ else
239
+ timers.push(setTimeout(ask, ms));
180
240
  }
181
241
  });
182
242
  const limitOf = (e) => {
@@ -198,7 +258,7 @@ function start(init) {
198
258
  }
199
259
  const n = Math.max(1, init.sessions);
200
260
  pendingConnects = n;
201
- const tries = Array.from({ length: n }, (_, i) => connect(addrs[i % addrs.length]).then((e) => {
261
+ const tries = Array.from({ length: n }, (_, i) => connect(addrs[i % addrs.length], i).then((e) => {
202
262
  pendingConnects--;
203
263
  if (closed) {
204
264
  if (e)
package/dist/core.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Promigence SDK — constants, error hierarchy and shared types. */
2
- export declare const VERSION = "0.1.0-beta.13";
2
+ export declare const VERSION = "0.1.0-beta.14";
3
3
  export declare const DOCS_URL = "https://promigence.ai";
4
4
  /** The error reference; each code is an anchor on it (the API's `docs` link has the same form). */
5
5
  export declare const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
package/dist/core.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { hintArg } from "./hint.js";
2
- export const VERSION = "0.1.0-beta.13";
2
+ export const VERSION = "0.1.0-beta.14";
3
3
  export const DOCS_URL = "https://promigence.ai";
4
4
  export const ERRORS_DOCS_URL = "https://www.promigence.ai/docs/errors";
5
5
  export const DEFAULT_API_URL = "https://api.promigence.ai/v1";
package/dist/http2.d.ts CHANGED
@@ -189,6 +189,7 @@ export declare const __http2Internals: {
189
189
  closedReason: string;
190
190
  messages: number;
191
191
  posts: number;
192
+ startedAt: number;
192
193
  } | undefined;
193
194
  /** The transport thread's number of connections and spacing between new requests on one, for an environment (`threadKnobs`). */
194
195
  threadKnobs(env: NodeJS.ProcessEnv): {
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);
package/dist/trace.d.ts CHANGED
@@ -31,3 +31,5 @@ export declare function mark(s: TraceStamp | undefined, phase: string): void;
31
31
  export declare function traceEvent(name: string): void;
32
32
  /** A process event that happened at `t` (`performance.now()` terms), recorded now: one another thread reported. */
33
33
  export declare function traceEventAt(name: string, t: number): void;
34
+ /** `fn` runs once at exit, before the trace is written (tracing on only). */
35
+ export declare function onTraceFlush(fn: () => void): void;
package/dist/trace.js CHANGED
@@ -60,7 +60,23 @@ function modulePath() {
60
60
  const m = /\((.*):\d+:\d+\)\s*$/.exec(frame) ?? /at (.*):\d+:\d+\s*$/.exec(frame);
61
61
  return m?.[1] ?? "";
62
62
  }
63
+ const beforeFlush = [];
64
+ export function onTraceFlush(fn) {
65
+ if (!TRACE_ON)
66
+ return;
67
+ beforeFlush.push(fn);
68
+ if (!hooked) {
69
+ hooked = true;
70
+ process.once("exit", flush);
71
+ }
72
+ }
63
73
  function flush() {
74
+ for (const fn of beforeFlush.splice(0)) {
75
+ try {
76
+ fn();
77
+ }
78
+ catch { }
79
+ }
64
80
  const head = {
65
81
  trace: "promigence-sdk",
66
82
  version: VERSION,
@@ -34,6 +34,11 @@ function start(init) {
34
34
  }
35
35
  catch { }
36
36
  };
37
+ const ev = (n) => {
38
+ if (init.trace === true)
39
+ post({ t: "ev", n, hr: hrNow() });
40
+ };
41
+ ev("boot");
37
42
  const live = () => sessions.filter((e) => !e.retired && !e.session.closed && !e.session.destroyed);
38
43
  const end = (reason) => {
39
44
  if (closed)
@@ -94,14 +99,14 @@ function start(init) {
94
99
  catch {
95
100
  context = undefined;
96
101
  }
97
- const connect = (address) => new Promise((resolve) => {
102
+ const connect = (address, k) => new Promise((resolve) => {
98
103
  let done = false;
99
104
  const finish = (e) => { if (!done) {
100
105
  done = true;
101
106
  clearTimeout(timer);
102
107
  resolve(e);
103
108
  } };
104
- const timer = setTimeout(() => { try {
109
+ const timer = setTimeout(() => { ev(`timeout${k}`); try {
105
110
  socket.destroy();
106
111
  }
107
112
  catch { } finish(null); }, init.connectTimeoutMs);
@@ -113,8 +118,11 @@ function start(init) {
113
118
  checkServerIdentity: guardCertChain,
114
119
  ...(context === undefined ? {} : { secureContext: context }),
115
120
  });
116
- socket.once("error", () => finish(null));
121
+ if (init.trace === true)
122
+ socket.once("connect", () => { ev(`tcp${k}`); });
123
+ socket.once("error", (e) => { ev(`fail${k} ${e?.code ?? "error"}`); finish(null); });
117
124
  socket.once("secureConnect", () => {
125
+ ev(`tls${k}`);
118
126
  if (socket.alpnProtocol !== "h2") {
119
127
  socket.destroy();
120
128
  finish(null);
@@ -167,14 +175,66 @@ function start(init) {
167
175
  resolve(v4);
168
176
  return;
169
177
  }
170
- try {
171
- const r = new dns.Resolver({ timeout: init.lookupTimeoutMs, tries: 1 });
172
- if (Array.isArray(init.servers) && init.servers.length > 0)
173
- r.setServers(init.servers);
174
- r.resolve4(name, (err, addrs) => resolve(!err && Array.isArray(addrs) ? addrs : []));
175
- }
176
- catch {
177
- resolve([]);
178
+ const at = init.lookupAtMs.length > 0 ? init.lookupAtMs : [0];
179
+ const resolvers = [];
180
+ const timers = [];
181
+ let done = false;
182
+ let asked = 0;
183
+ let failed = 0;
184
+ const finish = (addrs) => {
185
+ if (done)
186
+ return;
187
+ done = true;
188
+ for (const t of timers)
189
+ clearTimeout(t);
190
+ for (const r of resolvers) {
191
+ try {
192
+ r.cancel();
193
+ }
194
+ catch { }
195
+ }
196
+ resolve(addrs);
197
+ };
198
+ const gaveUp = () => {
199
+ if (++failed < asked)
200
+ return;
201
+ if (asked < at.length)
202
+ ask();
203
+ else
204
+ finish([]);
205
+ };
206
+ const ask = () => {
207
+ if (done || asked >= at.length)
208
+ return;
209
+ const k = asked++;
210
+ try {
211
+ const r = new dns.Resolver({ timeout: init.lookupTimeoutMs, tries: init.lookupTries });
212
+ const list = Array.isArray(init.servers) && init.servers.length > 0 ? init.servers : r.getServers();
213
+ if (list.length > 0)
214
+ r.setServers(list.map((_, i) => list[(i + k) % list.length]));
215
+ resolvers.push(r);
216
+ ev(`lookup${k + 1}`);
217
+ r.resolve4(name, (err, addrs) => {
218
+ if (done)
219
+ return;
220
+ if (!err && Array.isArray(addrs) && addrs.length > 0) {
221
+ ev(`addrs${k + 1} ${addrs.length}`);
222
+ finish(addrs);
223
+ return;
224
+ }
225
+ ev(`nolookup${k + 1} ${err?.code ?? "empty"}`);
226
+ gaveUp();
227
+ });
228
+ }
229
+ catch {
230
+ gaveUp();
231
+ }
232
+ };
233
+ for (const ms of at) {
234
+ if (ms <= 0)
235
+ ask();
236
+ else
237
+ timers.push(setTimeout(ask, ms));
178
238
  }
179
239
  });
180
240
  const limitOf = (e) => {
@@ -196,7 +256,7 @@ function start(init) {
196
256
  }
197
257
  const n = Math.max(1, init.sessions);
198
258
  pendingConnects = n;
199
- const tries = Array.from({ length: n }, (_, i) => connect(addrs[i % addrs.length]).then((e) => {
259
+ const tries = Array.from({ length: n }, (_, i) => connect(addrs[i % addrs.length], i).then((e) => {
200
260
  pendingConnects--;
201
261
  if (closed) {
202
262
  if (e)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promigence/sdk",
3
- "version": "0.1.0-beta.13",
3
+ "version": "0.1.0-beta.14",
4
4
  "description": "The fastest sandboxes for AI agents and code. The Promigence TypeScript SDK: snapshot an environment once, fork it as often as you need, run commands in every copy. A simple Sandbox class and the full PromigenceClient, with zero runtime dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.promigence.ai",