@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.
@@ -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 };
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)
@@ -334,7 +394,7 @@ function start(init) {
334
394
  return;
335
395
  }
336
396
  entry.active++;
337
- post({ t: "ack", id: req.id });
397
+ Atomics.store(init.acks, req.mark, req.id);
338
398
  const sent = init.trace !== true ? 0 : granted > 0 ? granted : hrNow();
339
399
  let head = 0;
340
400
  let status = 0;
@@ -443,11 +503,20 @@ function start(init) {
443
503
  else
444
504
  armIdle(init.idleMs);
445
505
  };
506
+ let wedged = false;
446
507
  port.on("message", (m) => {
447
- if (closed)
508
+ if (closed || wedged)
509
+ return;
510
+ if (m?.t === "wedge-for-test") {
511
+ wedged = true;
448
512
  return;
513
+ }
449
514
  if (m?.t === "req")
450
515
  void run(m);
516
+ else if (m?.t === "reqs") {
517
+ for (const r of m.list)
518
+ void run(r);
519
+ }
451
520
  else if (m?.t === "drain")
452
521
  drain("asked");
453
522
  else if (m?.t === "cancel" && typeof m.id === "number") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promigence/sdk",
3
- "version": "0.1.0-beta.12",
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",