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

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/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.12";
2
+ export declare const VERSION = "0.1.0-beta.13";
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.12";
2
+ export const VERSION = "0.1.0-beta.13";
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";
@@ -48,6 +48,8 @@ interface Timeouts {
48
48
  export declare function longRequestDispatcher(timeoutMs: number, base?: object | undefined): object | undefined;
49
49
  /** Test and diagnostics view. Not part of the public API. */
50
50
  export declare const __dispatcherInternals: {
51
+ /** How many times `longRequestDispatcher` has been asked (each request on the `fetch` path asks once). */
52
+ lookups(): number;
51
53
  /** The timeouts a dispatcher of ours sets, or `undefined` for any other value. */
52
54
  timeoutsOf(d: unknown): Timeouts | undefined;
53
55
  /** The process's global dispatcher composed with exactly these timeouts (never cached). */
@@ -47,7 +47,9 @@ function composeWith(base, ms) {
47
47
  timeouts.set(d, { headersTimeout: ms, bodyTimeout: ms });
48
48
  return d;
49
49
  }
50
+ let lookups = 0;
50
51
  export function longRequestDispatcher(timeoutMs, base = firstGlobalDispatcher()) {
52
+ lookups++;
51
53
  try {
52
54
  const needed = timeoutMs + LONG_REQUEST_SLACK_MS;
53
55
  if (!(needed > FETCH_DEFAULT_TIMEOUT_MS))
@@ -73,6 +75,9 @@ export function longRequestDispatcher(timeoutMs, base = firstGlobalDispatcher())
73
75
  }
74
76
  }
75
77
  export const __dispatcherInternals = {
78
+ lookups() {
79
+ return lookups;
80
+ },
76
81
  timeoutsOf(d) {
77
82
  return d !== null && typeof d === "object" ? timeouts.get(d) : undefined;
78
83
  },
package/dist/http2.d.ts CHANGED
@@ -69,6 +69,8 @@ export interface Http2Init {
69
69
  signal?: AbortSignal;
70
70
  /** Read by `fetch` on fallback only. */
71
71
  dispatcher?: object;
72
+ /** Worked out on fallback only, when no `dispatcher` was given: the one `fetch` should carry for `timeoutMs`. */
73
+ dispatcherFor?: (timeoutMs: number) => object | undefined;
72
74
  /** Phase stamps when `PROMIGENCE_SDK_TRACE=1`. */
73
75
  trace?: TraceStamp;
74
76
  }
@@ -142,9 +144,13 @@ export declare class Http2Transport {
142
144
  */
143
145
  fetch(url: string, init: Http2Init, replayable: boolean): Promise<Response | H2Response>;
144
146
  }
147
+ /** The one `Http2Transport` for every client whose fallback is `fallback` (it keeps nothing of a client's). */
148
+ export declare function sharedTransport(fallback: FetchLike): Http2Transport;
145
149
  export declare function warmTransport(): void;
146
150
  /** Test and diagnostics view of the process-wide pools. Not part of the public API. */
147
151
  export declare const __http2Internals: {
152
+ /** How many request URLs have been read (each new URL once; the same URL again is not read again). */
153
+ targetReads(): number;
148
154
  pools(): Array<{
149
155
  origin: string;
150
156
  sessions: number;
@@ -181,6 +187,8 @@ export declare const __http2Internals: {
181
187
  answered: number;
182
188
  everReady: boolean;
183
189
  closedReason: string;
190
+ messages: number;
191
+ posts: number;
184
192
  } | undefined;
185
193
  /** The transport thread's number of connections and spacing between new requests on one, for an environment (`threadKnobs`). */
186
194
  threadKnobs(env: NodeJS.ProcessEnv): {
@@ -188,6 +196,8 @@ export declare const __http2Internals: {
188
196
  gapUs: number;
189
197
  pace: string;
190
198
  };
199
+ /** Tests: make the transport thread stop answering (it reads nothing more), as a stuck one would. */
200
+ wedgeThreadForTest(): void;
191
201
  /** Tests: make the transport thread fail as a crash would. */
192
202
  crashThreadForTest(): void;
193
203
  /** The early lookup, for tests: start one with a platform, DNS servers and hosts file of the test's own. */
package/dist/http2.js CHANGED
@@ -845,18 +845,18 @@ const earlies = new Map();
845
845
  const MAX_EARLIES = 4;
846
846
  const EARLY_MAX_AGE_MS = 60_000;
847
847
  const EARLY_TIMEOUT_MS = 2_000;
848
- const LOOKUP_AT_LOAD = (() => {
848
+ const DNS_AT_LOAD = (() => {
849
849
  try {
850
- return nodeDns()?.lookup;
850
+ return nodeDns();
851
851
  }
852
852
  catch {
853
853
  return undefined;
854
854
  }
855
855
  })();
856
+ const LOOKUP_AT_LOAD = DNS_AT_LOAD?.lookup;
856
857
  function lookupReplaced() {
857
858
  try {
858
- const d = nodeDns();
859
- return d !== undefined && d.lookup !== LOOKUP_AT_LOAD;
859
+ return DNS_AT_LOAD !== undefined && DNS_AT_LOAD.lookup !== LOOKUP_AT_LOAD;
860
860
  }
861
861
  catch {
862
862
  return true;
@@ -1290,12 +1290,13 @@ function targetOf(url, known, remember) {
1290
1290
  }
1291
1291
  function fetchInit(init, deadline) {
1292
1292
  const timeout = AbortSignal.timeout(Math.max(0, Math.ceil(deadline - performance.now())));
1293
+ const dispatcher = init.dispatcher ?? init.dispatcherFor?.(init.timeoutMs);
1293
1294
  return {
1294
1295
  method: init.method,
1295
1296
  headers: init.headers,
1296
1297
  signal: init.signal === undefined ? timeout : AbortSignal.any([timeout, init.signal]),
1297
1298
  ...(init.body === undefined ? {} : { body: init.body }),
1298
- ...(init.dispatcher === undefined ? {} : { dispatcher: init.dispatcher }),
1299
+ ...(dispatcher === undefined ? {} : { dispatcher }),
1299
1300
  };
1300
1301
  }
1301
1302
  function fetchFailed(cause) {
@@ -1312,11 +1313,14 @@ class HandBack {
1312
1313
  const UNSENT = new HandBack(0, 0);
1313
1314
  const VIA_FETCH = Symbol("via-fetch");
1314
1315
  const THREAD_GRACE_MS = 1_000;
1316
+ const THREAD_SWEEP_MS = 250;
1317
+ const THREAD_MARKS = 4096;
1315
1318
  const THREAD_PHASES = ["w.got", "w.sent", "w.head", "w.end"];
1316
1319
  class TransportThread {
1317
1320
  origin;
1318
1321
  worker;
1319
1322
  port;
1323
+ acks;
1320
1324
  state = "starting";
1321
1325
  sessions = 0;
1322
1326
  limit = DEFAULT_STREAM_LIMIT;
@@ -1326,13 +1330,26 @@ class TransportThread {
1326
1330
  inflight = new Map();
1327
1331
  nextId = 1;
1328
1332
  listening = false;
1333
+ refd = false;
1334
+ sweep;
1335
+ freeMarks = [];
1336
+ messages = 0;
1337
+ outbox = [];
1338
+ turnOpen = false;
1339
+ posts = 0;
1329
1340
  answered = 0;
1330
1341
  pace = THREAD_DEFAULTS.pace;
1331
1342
  hrOffset = Number(process.hrtime.bigint()) / 1e6 - performance.now();
1332
- constructor(origin, worker, port) {
1343
+ constructor(origin, worker, port, acks) {
1333
1344
  this.origin = origin;
1334
1345
  this.worker = worker;
1335
1346
  this.port = port;
1347
+ this.acks = acks;
1348
+ for (let i = acks.length - 1; i >= 0; i--)
1349
+ this.freeMarks.push(i);
1350
+ }
1351
+ acked(p) {
1352
+ return Atomics.load(this.acks, p.mark) === p.id;
1336
1353
  }
1337
1354
  poll() {
1338
1355
  for (;;) {
@@ -1349,12 +1366,13 @@ class TransportThread {
1349
1366
  }
1350
1367
  }
1351
1368
  ready() {
1352
- if (this.state !== "closed")
1369
+ if (!this.listening && this.state !== "closed")
1353
1370
  this.poll();
1354
- return this.state === "ready" && this.sessions > 0 && this.inflight.size < this.sessions * this.limit;
1371
+ return this.state === "ready" && this.sessions > 0 && this.inflight.size < this.sessions * this.limit && this.freeMarks.length > 0;
1355
1372
  }
1356
1373
  onMessage(m) {
1357
1374
  const msg = m;
1375
+ this.messages++;
1358
1376
  switch (msg?.t) {
1359
1377
  case "ready":
1360
1378
  if (this.state === "starting" || this.state === "ready") {
@@ -1391,12 +1409,6 @@ class TransportThread {
1391
1409
  case "closed":
1392
1410
  this.close(msg.reason ?? "closed");
1393
1411
  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
1412
  case "res":
1401
1413
  case "err":
1402
1414
  this.settle(m);
@@ -1480,10 +1492,12 @@ class TransportThread {
1480
1492
  if (this.closedReason === "")
1481
1493
  this.closedReason = reason;
1482
1494
  traceEvent("thread:closed");
1495
+ this.outbox = [];
1483
1496
  for (const [id, p] of [...this.inflight]) {
1497
+ const sent = this.acked(p);
1484
1498
  this.inflight.delete(id);
1485
1499
  this.release(p);
1486
- if (!p.acked)
1500
+ if (!sent)
1487
1501
  p.resolve(UNSENT);
1488
1502
  else if (p.replayable)
1489
1503
  p.resolve(new HandBack(0, 1));
@@ -1505,18 +1519,49 @@ class TransportThread {
1505
1519
  this.poll();
1506
1520
  this.port.on("message", (m) => { this.onMessage(m); });
1507
1521
  }
1522
+ if (this.refd)
1523
+ return;
1524
+ this.refd = true;
1508
1525
  safely(() => { this.port.ref(); });
1509
1526
  safely(() => { this.worker.ref(); });
1527
+ this.sweep = setInterval(() => { this.sweepBackstops(); }, THREAD_SWEEP_MS);
1528
+ safely(() => { this.sweep?.unref?.(); });
1510
1529
  }
1511
1530
  unref() {
1531
+ if (this.sweep !== undefined) {
1532
+ clearInterval(this.sweep);
1533
+ this.sweep = undefined;
1534
+ }
1535
+ if (!this.refd)
1536
+ return;
1537
+ this.refd = false;
1512
1538
  safely(() => { this.port.unref(); });
1513
1539
  safely(() => { this.worker.unref(); });
1514
1540
  }
1541
+ sweepBackstops() {
1542
+ this.poll();
1543
+ const now = performance.now();
1544
+ let late = false;
1545
+ for (const [id, p] of [...this.inflight]) {
1546
+ if (p.backstopAt > now)
1547
+ continue;
1548
+ late = true;
1549
+ this.inflight.delete(id);
1550
+ this.release(p);
1551
+ try {
1552
+ this.port.postMessage({ t: "cancel", id });
1553
+ }
1554
+ catch { }
1555
+ p.reject(timeoutReason());
1556
+ }
1557
+ if (late)
1558
+ this.close("no answer from the transport thread");
1559
+ }
1515
1560
  release(p) {
1516
1561
  if (p.signal !== undefined && p.onAbort !== undefined)
1517
1562
  p.signal.removeEventListener("abort", p.onAbort);
1518
- if (p.backstop !== undefined)
1519
- clearTimeout(p.backstop);
1563
+ Atomics.store(this.acks, p.mark, 0);
1564
+ this.freeMarks.push(p.mark);
1520
1565
  if (this.inflight.size === 0)
1521
1566
  this.unref();
1522
1567
  }
@@ -1541,8 +1586,11 @@ class TransportThread {
1541
1586
  headers["content-length"] = Buffer.byteLength(init.body, "utf8");
1542
1587
  const id = this.nextId++;
1543
1588
  const tr = init.trace;
1589
+ const place = this.freeMarks.pop();
1590
+ if (place === undefined)
1591
+ return Promise.resolve(UNSENT);
1544
1592
  return new Promise((resolve, reject) => {
1545
- const p = { resolve, reject, method: init.method, replayable, acked: false, trace: tr };
1593
+ const p = { id, resolve, reject, method: init.method, replayable, mark: place, backstopAt: performance.now() + remaining + THREAD_GRACE_MS, trace: tr };
1546
1594
  const signal = init.signal;
1547
1595
  if (signal !== undefined) {
1548
1596
  p.signal = signal;
@@ -1551,47 +1599,59 @@ class TransportThread {
1551
1599
  return;
1552
1600
  this.inflight.delete(id);
1553
1601
  this.release(p);
1554
- try {
1555
- this.port.postMessage({ t: "cancel", id });
1602
+ const queued = this.outbox.findIndex((b) => b.id === id);
1603
+ if (queued >= 0)
1604
+ this.outbox.splice(queued, 1);
1605
+ else {
1606
+ try {
1607
+ this.port.postMessage({ t: "cancel", id });
1608
+ }
1609
+ catch { }
1556
1610
  }
1557
- catch { }
1558
1611
  reject(signal.reason);
1559
1612
  };
1560
1613
  signal.addEventListener("abort", p.onAbort, { once: true });
1561
1614
  }
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
1615
  this.inflight.set(id, p);
1577
1616
  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 });
1617
+ const paced = this.pace === "all" || (this.pace === "creates" && init.method === "POST" && CREATE_PATH.test(target.path));
1618
+ this.outbox.push({ id, msg: { t: "req", id, mark: place, headers, sensitive: ["authorization"], body: init.body, timeoutMs: remaining, replayable, paced }, p, resolve, trace: tr });
1619
+ if (!this.turnOpen) {
1620
+ this.turnOpen = true;
1621
+ queueMicrotask(() => { this.turnOpen = false; this.handOver(); });
1622
+ this.handOver();
1581
1623
  }
1582
- catch {
1583
- this.inflight.delete(id);
1584
- this.release(p);
1585
- resolve(UNSENT);
1586
- return;
1624
+ else if (this.outbox.length >= OUTBOX_GROUP)
1625
+ this.handOver();
1626
+ });
1627
+ }
1628
+ handOver() {
1629
+ if (this.outbox.length === 0)
1630
+ return;
1631
+ const batch = this.outbox;
1632
+ this.outbox = [];
1633
+ try {
1634
+ this.port.postMessage(batch.length === 1 ? batch[0].msg : { t: "reqs", list: batch.map((b) => b.msg) });
1635
+ this.posts++;
1636
+ }
1637
+ catch {
1638
+ for (const b of batch) {
1639
+ if (!this.inflight.has(b.id))
1640
+ continue;
1641
+ this.inflight.delete(b.id);
1642
+ this.release(b.p);
1643
+ b.resolve(UNSENT);
1587
1644
  }
1588
- if (tr !== undefined) {
1589
- mark(tr, "sub");
1590
- tr.via = "thread";
1645
+ return;
1646
+ }
1647
+ for (const b of batch)
1648
+ if (b.trace !== undefined) {
1649
+ mark(b.trace, "sub");
1650
+ b.trace.via = "thread";
1591
1651
  }
1592
- });
1593
1652
  }
1594
1653
  }
1654
+ const OUTBOX_GROUP = 8;
1595
1655
  let transportThread;
1596
1656
  function threadFor(origin) {
1597
1657
  const t = transportThread;
@@ -1636,9 +1696,10 @@ function startTransportThread(origin, onFail) {
1636
1696
  const knobs = threadKnobs();
1637
1697
  const file = typeof seam.transportFile === "string" ? seam.transportFile : here.replace(/http2\.js$/, "transport-worker.js");
1638
1698
  const { port1, port2 } = new MessageChannel();
1699
+ const acks = new Int32Array(new SharedArrayBuffer(4 * THREAD_MARKS));
1639
1700
  const worker = new Worker(file.startsWith("file:") ? new URL(file) : file, {
1640
1701
  workerData: {
1641
- promigenceTransport: true, port: port2, origin: origin.origin, sessions: knobs.sessions, gapUs: knobs.gapUs,
1702
+ promigenceTransport: true, port: port2, acks, origin: origin.origin, sessions: knobs.sessions, gapUs: knobs.gapUs,
1642
1703
  firstUseMs: tuning.loadFirstUseMs, idleMs: IDLE_CLOSE_MS, connectTimeoutMs: CONNECT_TIMEOUT_MS,
1643
1704
  lookupTimeoutMs: EARLY_TIMEOUT_MS, trace: TRACE_ON,
1644
1705
  servers: tuning.earlyServers ?? (Array.isArray(seam.servers) ? seam.servers : undefined),
@@ -1651,7 +1712,7 @@ function startTransportThread(origin, onFail) {
1651
1712
  });
1652
1713
  worker.unref();
1653
1714
  port1.unref();
1654
- const t = new TransportThread(origin.origin, worker, port1);
1715
+ const t = new TransportThread(origin.origin, worker, port1, acks);
1655
1716
  t.pace = knobs.pace;
1656
1717
  t.onFail = onFail;
1657
1718
  worker.on("error", () => { t.poll(); t.close("error"); });
@@ -1673,6 +1734,18 @@ function knownPrefix(url) {
1673
1734
  }
1674
1735
  return undefined;
1675
1736
  }
1737
+ let lastTargetUrl;
1738
+ let lastTargetRead;
1739
+ let targetReads = 0;
1740
+ function lastTarget(url) {
1741
+ if (url === lastTargetUrl)
1742
+ return lastTargetRead;
1743
+ targetReads++;
1744
+ const target = targetOf(url, knownPrefix(url), remember);
1745
+ lastTargetUrl = url;
1746
+ lastTargetRead = target;
1747
+ return target;
1748
+ }
1676
1749
  function remember(p) {
1677
1750
  for (let i = 0; i < prefixes.length; i++)
1678
1751
  if (prefixes[i].text === p.text)
@@ -1708,7 +1781,7 @@ export class Http2Transport {
1708
1781
  const deadline = performance.now() + init.timeoutMs;
1709
1782
  let target;
1710
1783
  try {
1711
- target = targetOf(url, knownPrefix(url), remember);
1784
+ target = lastTarget(url);
1712
1785
  }
1713
1786
  catch {
1714
1787
  return this.viaFetch(url, init, deadline);
@@ -1768,6 +1841,15 @@ export class Http2Transport {
1768
1841
  }
1769
1842
  }
1770
1843
  }
1844
+ let sharedFallback;
1845
+ let shared;
1846
+ export function sharedTransport(fallback) {
1847
+ if (shared === undefined || sharedFallback !== fallback) {
1848
+ shared = new Http2Transport(fallback);
1849
+ sharedFallback = fallback;
1850
+ }
1851
+ return shared;
1852
+ }
1771
1853
  export function warmTransport() {
1772
1854
  const res = toResponse(200, { "content-type": "application/json" }, Buffer.from("{}"), "POST");
1773
1855
  res.header("server-timing");
@@ -1780,6 +1862,9 @@ export function warmTransport() {
1780
1862
  exchange({ end() { } }, undefined, { origin: none, host: none, path: none }, undefined, 0).catch(() => { });
1781
1863
  }
1782
1864
  export const __http2Internals = {
1865
+ targetReads() {
1866
+ return targetReads;
1867
+ },
1783
1868
  pools() {
1784
1869
  return [...pools.values()].map((p) => ({
1785
1870
  origin: p.origin.origin,
@@ -1820,11 +1905,17 @@ export const __http2Internals = {
1820
1905
  if (t === undefined)
1821
1906
  return undefined;
1822
1907
  t.poll();
1823
- return { state: t.state, sessions: t.sessions, limit: t.limit, answered: t.answered, everReady: t.everReady, closedReason: t.closedReason };
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 };
1824
1909
  },
1825
1910
  threadKnobs(env) {
1826
1911
  return threadKnobs(env);
1827
1912
  },
1913
+ wedgeThreadForTest() {
1914
+ try {
1915
+ transportThread?.port.postMessage({ t: "wedge-for-test" });
1916
+ }
1917
+ catch { }
1918
+ },
1828
1919
  crashThreadForTest() {
1829
1920
  try {
1830
1921
  transportThread?.port.postMessage({ t: "crash-for-test" });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Is `method path` a create that takes an `Idempotency-Key`? `path` is relative to `/v1` and may
3
+ * carry a query string. The list is the API reference's (generated from the operations that
4
+ * document the header), so a create the API keys is never sent unkeyed by this client.
5
+ */
6
+ export declare function isKeyedCreate(method: string, path: string): boolean;
package/dist/keyed.js ADDED
@@ -0,0 +1,55 @@
1
+ import { KEYED_CREATES } from "./keyed.gen.js";
2
+ const KEYED_PATTERNS = KEYED_CREATES.map(([m, p]) => [m, p.split("/").filter(Boolean)]);
3
+ const KEYED_METHODS = new Set(KEYED_CREATES.map(([m]) => m));
4
+ let keyedLastMethod = "";
5
+ let keyedLastPath = "";
6
+ let keyedLastAnswer = false;
7
+ let keyedReads = 0;
8
+ export function isKeyedCreate(method, path) {
9
+ if (!KEYED_METHODS.has(method))
10
+ return false;
11
+ if (path === keyedLastPath && method === keyedLastMethod)
12
+ return keyedLastAnswer;
13
+ const answer = keyedCreateOf(method, path);
14
+ keyedLastMethod = method;
15
+ keyedLastPath = path;
16
+ keyedLastAnswer = answer;
17
+ return answer;
18
+ }
19
+ function keyedCreateOf(method, path) {
20
+ keyedReads++;
21
+ let end = path.indexOf("?");
22
+ if (end < 0)
23
+ end = path.length;
24
+ for (let p = 0; p < KEYED_PATTERNS.length; p++) {
25
+ const entry = KEYED_PATTERNS[p];
26
+ if (entry[0] === method && segmentsMatch(path, end, entry[1]))
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ function segmentsMatch(path, end, pat) {
32
+ let i = 0;
33
+ let k = 0;
34
+ for (;;) {
35
+ while (i < end && path.charCodeAt(i) === 0x2f)
36
+ i++;
37
+ if (i >= end)
38
+ return k === pat.length;
39
+ if (k >= pat.length)
40
+ return false;
41
+ let j = i;
42
+ while (j < end && path.charCodeAt(j) !== 0x2f)
43
+ j++;
44
+ const s = pat[k];
45
+ if (s.charCodeAt(0) !== 0x7b && (j - i !== s.length || !path.startsWith(s, i)))
46
+ return false;
47
+ k++;
48
+ i = j;
49
+ }
50
+ }
51
+ export const __keyedInternals = {
52
+ reads() {
53
+ return keyedReads;
54
+ },
55
+ };
package/dist/proxy.js CHANGED
@@ -57,12 +57,23 @@ export function noProxy(host, port, env = process.env) {
57
57
  }
58
58
  return false;
59
59
  }
60
+ let proxyLastInputs;
61
+ let proxyLast;
60
62
  export function proxyFor(url, env = process.env) {
61
63
  const https = envOf(env, "HTTPS_PROXY");
62
64
  const http = envOf(env, "HTTP_PROXY");
63
65
  const all = envOf(env, "ALL_PROXY");
64
66
  if (https === undefined && http === undefined && all === undefined)
65
67
  return undefined;
68
+ const inputs = `${url}\n${https ?? "\0"}\n${http ?? "\0"}\n${all ?? "\0"}\n${envOf(env, "NO_PROXY") ?? "\0"}`;
69
+ if (inputs === proxyLastInputs)
70
+ return proxyLast;
71
+ const found = proxyForOf(url, env, https, http, all);
72
+ proxyLastInputs = inputs;
73
+ proxyLast = found;
74
+ return found;
75
+ }
76
+ function proxyForOf(url, env, https, http, all) {
66
77
  let u;
67
78
  try {
68
79
  u = new URL(url);
package/dist/sandbox.d.ts CHANGED
@@ -396,7 +396,9 @@ export declare class Sandbox {
396
396
  }>;
397
397
  /**
398
398
  * `forkHandles`, plus what `create` needs to end a run it was interrupted starting: the key the
399
- * create is sent under, a signal for it and its waits, and the start options as sent.
399
+ * create is sent under, a signal for it and its waits, and the start options as sent (`onStart`
400
+ * is handed the very options sent, the signal included); `client` and `count` stand in for the
401
+ * options' own.
400
402
  */
401
403
  private static forkHandlesWith;
402
404
  /** Attach to a running sandbox (`GET /sandboxes/:id`). */
package/dist/sandbox.js CHANGED
@@ -168,16 +168,19 @@ function onSigint() {
168
168
  return;
169
169
  void Promise.allSettled(cleanups.map((f) => f())).finally(() => { process.kill(process.pid, "SIGINT"); });
170
170
  }
171
+ function createAborted() {
172
+ return new TimeoutError("Sandbox.create was aborted by the caller's signal; anything it had started was ended", "Nothing to fix: the caller aborted it.");
173
+ }
171
174
  function holdSigint(cleanup) {
172
175
  if (typeof process === "undefined" || typeof process.listenerCount !== "function")
173
- return () => { };
174
- if (process.listenerCount("SIGINT") - (sigintHeld ? 1 : 0) > 0)
175
- return () => { };
176
- creatingNow.add(cleanup);
176
+ return undefined;
177
177
  if (!sigintHeld) {
178
+ if (process.listenerCount("SIGINT") > 0)
179
+ return undefined;
178
180
  process.once("SIGINT", onSigint);
179
181
  sigintHeld = true;
180
182
  }
183
+ creatingNow.add(cleanup);
181
184
  return () => {
182
185
  creatingNow.delete(cleanup);
183
186
  if (creatingNow.size === 0 && sigintHeld) {
@@ -455,7 +458,7 @@ export class Sandbox {
455
458
  ...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }),
456
459
  ...(opts.apiUrl === undefined ? {} : { apiUrl: opts.apiUrl }),
457
460
  });
458
- const aborted = () => new TimeoutError("Sandbox.create was aborted by the caller's signal; anything it had started was ended", "Nothing to fix: the caller aborted it.");
461
+ const aborted = createAborted;
459
462
  if (opts.signal?.aborted)
460
463
  throw aborted();
461
464
  if (opts.image !== undefined) {
@@ -476,24 +479,31 @@ export class Sandbox {
476
479
  else if (snapshotIdOrAlias === undefined || snapshotIdOrAlias === "base") {
477
480
  snapshotIdOrAlias = DEFAULT_SNAPSHOT;
478
481
  }
479
- const stop = new AbortController();
480
482
  const key = newIdempotencyKey();
481
483
  let sent;
482
484
  let runId;
483
485
  let ending;
484
486
  const end = () => (ending ??= endInterruptedCreate(client, runId, sent));
485
- const release = holdSigint(() => { stop.abort(); return end(); });
486
- const onAbort = () => { stop.abort(); };
487
- opts.signal?.addEventListener("abort", onAbort, { once: true });
487
+ let stop;
488
+ const release = holdSigint(() => { stop?.abort(); return end(); });
489
+ const callerSignal = opts.signal;
490
+ const onAbort = callerSignal === undefined ? undefined : () => { stop?.abort(); };
491
+ if (release !== undefined || callerSignal !== undefined)
492
+ stop = new AbortController();
493
+ if (onAbort !== undefined)
494
+ callerSignal.addEventListener("abort", onAbort, { once: true });
488
495
  try {
489
496
  let handles;
490
497
  try {
491
- ({ runId, handles } = await Sandbox.forkHandlesWith(snapshotIdOrAlias, { ...opts, count: 1, client }, {
492
- idempotencyKey: key, signal: stop.signal, onStart: (s) => { sent = s; },
493
- }));
498
+ const extra = {
499
+ idempotencyKey: key, onStart: (s) => { sent = s; }, client, count: 1,
500
+ };
501
+ if (stop !== undefined)
502
+ extra.signal = stop.signal;
503
+ ({ runId, handles } = await Sandbox.forkHandlesWith(snapshotIdOrAlias, opts, extra));
494
504
  }
495
505
  catch (e) {
496
- if (stop.signal.aborted) {
506
+ if (stop?.signal.aborted) {
497
507
  await end();
498
508
  throw aborted();
499
509
  }
@@ -506,7 +516,7 @@ export class Sandbox {
506
516
  return await first;
507
517
  }
508
518
  catch (e) {
509
- if (stop.signal.aborted) {
519
+ if (stop?.signal.aborted) {
510
520
  await end();
511
521
  throw aborted();
512
522
  }
@@ -518,8 +528,9 @@ export class Sandbox {
518
528
  }
519
529
  }
520
530
  finally {
521
- release();
522
- opts.signal?.removeEventListener("abort", onAbort);
531
+ release?.();
532
+ if (onAbort !== undefined)
533
+ callerSignal.removeEventListener("abort", onAbort);
523
534
  }
524
535
  }
525
536
  static async fork(snapshotIdOrAlias, opts = {}) {
@@ -530,11 +541,11 @@ export class Sandbox {
530
541
  return Sandbox.forkHandlesWith(snapshotIdOrAlias, opts, {});
531
542
  }
532
543
  static async forkHandlesWith(snapshotIdOrAlias, opts, extra) {
533
- const client = opts.client ?? new PromigenceClient({
544
+ const client = extra.client ?? opts.client ?? new PromigenceClient({
534
545
  ...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }),
535
546
  ...(opts.apiUrl === undefined ? {} : { apiUrl: opts.apiUrl }),
536
547
  });
537
- const count = opts.count ?? 1;
548
+ const count = extra.count ?? opts.count ?? 1;
538
549
  const spendCap = spendCapFromEnv(opts.spendCap);
539
550
  const start = { snapshot: snapshotIdOrAlias, count };
540
551
  if (spendCap !== undefined)
@@ -555,8 +566,10 @@ export class Sandbox {
555
566
  start.optimizeFor = opts.optimizeFor;
556
567
  if (extra.idempotencyKey !== undefined)
557
568
  start.idempotencyKey = extra.idempotencyKey;
558
- extra.onStart?.({ ...start });
559
- const run = await client.runs.start(extra.signal === undefined ? start : { ...start, signal: extra.signal });
569
+ if (extra.signal !== undefined)
570
+ start.signal = extra.signal;
571
+ extra.onStart?.(start);
572
+ const run = await client.runs.start(start);
560
573
  const envs = opts.envs;
561
574
  const mk = (ref) => {
562
575
  const init = { sandboxId: ref.sandbox_id, runId: run.run_id, snapshot: snapshotIdOrAlias, client };