withruntime 0.11.1 → 0.11.2

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.
Files changed (53) hide show
  1. package/BLAXEL.md +1 -1
  2. package/DAYTONA.md +1 -1
  3. package/E2B.md +57 -29
  4. package/README.md +4 -3
  5. package/README.src.md +4 -3
  6. package/compatibility-lock.json +14 -14
  7. package/dist/blaxel/errors.js +4 -4
  8. package/dist/blaxel/sandbox.d.ts +1 -2
  9. package/dist/blaxel/sandbox.js +1 -2
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.js +63 -15
  12. package/dist/client.d.ts +11 -59
  13. package/dist/client.js +8 -113
  14. package/dist/daytona/daytona.d.ts +1 -2
  15. package/dist/daytona/daytona.js +1 -2
  16. package/dist/daytona/sandbox.d.ts +1 -1
  17. package/dist/daytona/sandbox.js +1 -1
  18. package/dist/e2b/client.d.ts +15 -2
  19. package/dist/e2b/client.js +61 -7
  20. package/dist/e2b/commands.d.ts +3 -0
  21. package/dist/e2b/commands.js +16 -5
  22. package/dist/e2b/errors.d.ts +26 -2
  23. package/dist/e2b/errors.js +38 -5
  24. package/dist/e2b/filesystem.d.ts +1 -1
  25. package/dist/e2b/filesystem.js +82 -16
  26. package/dist/e2b/index.d.ts +2 -2
  27. package/dist/e2b/index.js +2 -2
  28. package/dist/e2b/sandbox.d.ts +23 -14
  29. package/dist/e2b/sandbox.js +195 -178
  30. package/dist/e2b/unsupported.d.ts +19 -0
  31. package/dist/e2b/unsupported.js +21 -0
  32. package/dist/e2b/users.d.ts +9 -0
  33. package/dist/e2b/users.js +22 -0
  34. package/dist/errors.d.ts +4 -4
  35. package/dist/errors.js +12 -10
  36. package/dist/machine.d.ts +180 -0
  37. package/dist/machine.js +504 -0
  38. package/dist/openai-agents/index.d.ts +1 -1
  39. package/dist/openai-agents/index.js +1 -1
  40. package/dist/products/jobs.d.ts +2 -3
  41. package/dist/products/jobs.js +1 -2
  42. package/dist/products/limits.d.ts +4 -10
  43. package/dist/products/network.d.ts +1 -1
  44. package/dist/products/secrets.d.ts +1 -1
  45. package/dist/products/switching.d.ts +2 -3
  46. package/dist/sandbox.d.ts +7 -92
  47. package/dist/sandbox.js +8 -362
  48. package/dist/transport.d.ts +4 -2
  49. package/dist/transport.js +6 -4
  50. package/dist/types.d.ts +53 -8
  51. package/dist/vercel/sandbox.d.ts +1 -2
  52. package/dist/vercel/sandbox.js +1 -2
  53. package/package.json +1 -1
package/dist/sandbox.d.ts CHANGED
@@ -1,27 +1,25 @@
1
- import { Page } from "./page.js";
1
+ import type { Page } from "./page.js";
2
2
  import { type FileEvent, type WatchHandle, type WatchOptions } from "./products/watch.js";
3
3
  import { Transport, type RequestOptions } from "./transport.js";
4
- import type { CommandResult, DeletedSandbox, ExecOptions, FileEntry, OutputEvent, BinaryOutputEvent, KeepAliveOptions, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
4
+ import type { CommandResult, DeletedSandbox, ExecOptions, FileEntry, OutputEvent, BinaryOutputEvent, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
5
5
  import { type SandboxExtensions } from "./products/index.js";
6
- import type { Snapshot, SnapshotOptions } from "./snapshots.js";
7
6
  import { Tunnel, type PortForward } from "./tunnel.js";
8
7
  import type { StreamUploadSource } from "./file-source.js";
9
- /** DELETE /v1/sandboxes/{id}, for `sandbox.delete()` and
10
- * `runtime.sandboxes.delete(id)`. */
11
- export declare function deleteSandbox(t: Transport, id: string, options?: RequestOptions): Promise<DeletedSandbox>;
8
+ import { Machine, type MachineProduct } from "./machine.js";
9
+ /** Sandboxes, as a product of the machine engine (machine.ts). */
10
+ export declare const SANDBOXES: MachineProduct;
12
11
  /** A running (or stopped) sandbox. Every method is one API call or a short
13
12
  * sequence of them; `await using sbx = await Sandbox.create()` stops it for
14
13
  * you at the end of the block. */
15
14
  export interface Sandbox extends SandboxExtensions {
16
15
  }
17
- export declare class Sandbox implements AsyncDisposable {
16
+ export declare class Sandbox extends Machine<SandboxInfo, SandboxSettings, DeletedSandbox> {
18
17
  #private;
19
18
  readonly files: Files;
20
19
  readonly processes: Processes;
21
20
  /** Short-lived tokens that let a browser reach this sandbox directly. */
22
21
  readonly sessions: SandboxSessions;
23
22
  constructor(transport: Transport, info: SandboxInfo);
24
- get id(): string;
25
23
  /** A sandbox reached with a session token instead of an API key, for code in
26
24
  * a browser: commands and their streams, processes, files and previews of
27
25
  * this one sandbox. Makes no call; `info` holds only the id until
@@ -39,14 +37,6 @@ export declare class Sandbox implements AsyncDisposable {
39
37
  apiUrl?: string;
40
38
  fetch?: typeof fetch;
41
39
  }): Sandbox;
42
- /** What the API last said about this sandbox. `refresh()` asks again. */
43
- get info(): SandboxInfo;
44
- get state(): SandboxInfo["state"];
45
- refresh(options?: RequestOptions): Promise<this>;
46
- /** Waits (server-side, no polling) until the sandbox reaches `state`. */
47
- waitFor(state: "running" | "paused" | "stopped", options?: {
48
- timeoutSeconds?: number;
49
- } & RequestOptions): Promise<this>;
50
40
  /** Runs a command and returns its exit code and output. A string runs under
51
41
  * `bash -c`; an array runs without a shell. With onStdout/onStderr the output
52
42
  * streams as it happens. A timeout is a result (timedOut: true, the output so
@@ -94,76 +84,6 @@ export declare class Sandbox implements AsyncDisposable {
94
84
  localPort?: number;
95
85
  host?: string;
96
86
  }): Promise<PortForward>;
97
- stop(options?: RequestOptions & {
98
- wait?: boolean;
99
- }): Promise<this>;
100
- pause(options?: RequestOptions & {
101
- wait?: boolean;
102
- }): Promise<this>;
103
- /** Deletes it for good: stops it if it runs or is paused, deletes its disk
104
- * and paused memory, revokes its previews and ports, and removes it from
105
- * lists. Its snapshots, usage and audit entries stay. Deleting it again
106
- * answers the same. */
107
- delete(options?: RequestOptions): Promise<DeletedSandbox>;
108
- /** Carries on a paused sandbox; for one with a time limit,
109
- * `timeoutSeconds` is its new one. */
110
- wake(options?: RequestOptions & {
111
- wait?: boolean;
112
- timeoutSeconds?: number;
113
- }): Promise<this>;
114
- /** More time before its time limit ends (at most an hour ahead of now). A
115
- * sandbox with no time limit needs none: the call answers at once and
116
- * changes nothing. */
117
- extend(seconds: number, options?: RequestOptions): Promise<this>;
118
- /** Changes its name, labels, automatic wake, idle pause or persistence;
119
- * fields left out stay as they are. `persistent: true` keeps it running
120
- * while credit lasts (paid only). */
121
- update(settings: SandboxSettings, options?: RequestOptions): Promise<this>;
122
- /** Moves it to another image (id, name, name:tag or name@version), keeping
123
- * its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
124
- * volumes, environment, name and previews. Its processes restart, and
125
- * everything else on its old disk (sudo installs, apt packages, /etc) is
126
- * lost; snapshot it first to keep everything. A running sandbox is paused
127
- * first. A switch that fails is undone (`switch_undone`), the sandbox on
128
- * its old image with nothing lost. Charged as a wake. `keep` is optional:
129
- * "workspace" is the only value, and it is sent either way, so an API from
130
- * before it was optional accepts the call too. */
131
- switchImage(image: string, options?: RequestOptions & {
132
- keep?: "workspace";
133
- }): Promise<this>;
134
- /** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
135
- * whole disk, volumes, environment, name and previews stay, and its
136
- * programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
137
- * persistent, and comes back running at the new size on the same server; a
138
- * paused or stopped one is started. Memory bills on the new size from then.
139
- * Refused, with nothing changed, when the server has no room
140
- * (`no_capacity`), above your quota or the trial's 2 vCPU and 4 GiB, or
141
- * while a snapshot or fork of it is being taken. `restart` is optional:
142
- * true is the only value, and it is sent either way, so an API from before
143
- * it was optional accepts the call too. */
144
- resize(size: {
145
- vcpu?: number;
146
- memoryMiB?: number;
147
- }, options?: RequestOptions & {
148
- restart?: true;
149
- }): Promise<this>;
150
- /** Keeps a sandbox with a time limit running past it, in the background,
151
- * until stop() or the returned function ends it: every `everySeconds` (60)
152
- * it extends the limit so that `marginSeconds` (600) remain, never more
153
- * than the hour ahead the API allows. A sandbox with no time limit
154
- * (`endsAt` null) needs none, and is only watched. Running time is billed
155
- * as it is used. A paused sandbox is left paused (a request wakes it,
156
- * unless autoWake is off); a stopped one ends the loop. It does not keep a
157
- * Node or Bun process alive by itself. */
158
- keepAlive(options?: KeepAliveOptions): () => void;
159
- /** Days a paused sandbox is kept before deletion (1 to 365). */
160
- setRetention(days: number, options?: RequestOptions): Promise<this>;
161
- /** Keeps this sandbox's whole machine (files, memory, running processes) as
162
- * a snapshot to start new sandboxes from. A running sandbox is paused for
163
- * the moment it takes, then woken; a paused one stays paused. Past
164
- * `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
165
- * the error names the snapshot. */
166
- snapshot(options?: SnapshotOptions & RequestOptions): Promise<Snapshot>;
167
87
  /** Copies of this sandbox as it is now (files, memory, running processes),
168
88
  * each its own sandbox, answered once they run. A running sandbox is paused
169
89
  * for the moment its snapshot takes, then woken. One without `count`; an
@@ -182,12 +102,6 @@ export declare class Sandbox implements AsyncDisposable {
182
102
  keepSnapshot?: boolean;
183
103
  funding?: "trial" | "paid";
184
104
  } & RequestOptions): Promise<Sandbox[]>;
185
- /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
186
- restart(options?: RequestOptions & {
187
- wait?: boolean;
188
- }): Promise<this>;
189
- [Symbol.asyncDispose](): Promise<void>;
190
- toJSON(): SandboxInfo;
191
105
  }
192
106
  /** A sandbox session, as the API answers it. `token` is set only in the
193
107
  * answer to `create`. */
@@ -411,6 +325,7 @@ export declare class Files {
411
325
  * the API's folder route. */
412
326
  download(remotePath: string, localPath: string, options?: RequestOptions): Promise<void>;
413
327
  }
328
+ /** One page of `runtime.sandboxes.list()`, and the pages after it. */
414
329
  export declare function sandboxPage(t: Transport, body: {
415
330
  data: SandboxInfo[];
416
331
  nextCursor: string | null;
package/dist/sandbox.js CHANGED
@@ -1,13 +1,13 @@
1
- import { KEEP_ALIVE_MARGIN_SECONDS, STREAMED_EXEC_TIMEOUT_MS, WAIT_FOR_TIMEOUT_SECONDS, } from "./api-defaults.js";
1
+ import { STREAMED_EXEC_TIMEOUT_MS } from "./api-defaults.js";
2
2
  import { CommandError, ConnectionError, DELIBERATE, errorFor, NotFoundError, RateLimitError, RuntimeError, ServiceUnavailableError, } from "./errors.js";
3
- import { Page } from "./page.js";
4
3
  import { sandboxWatches, watchDirectory, } from "./products/watch.js";
5
4
  import { Transport } from "./transport.js";
6
5
  import { sandboxFactories } from "./products/index.js";
7
6
  import { Tunnel } from "./tunnel.js";
8
7
  import { pause } from "./wait.js";
9
- /** How long snapshot() waits, pause and capture together, unless told. */
10
- const SNAPSHOT_DEADLINE_MS = 10 * 60_000;
8
+ import { Machine, machinePage, requestOnly } from "./machine.js";
9
+ /** Sandboxes, as a product of the machine engine (machine.ts). */
10
+ export const SANDBOXES = { plural: "sandboxes", noun: "Sandbox" };
11
11
  const CHUNK = 1_048_576;
12
12
  /** Chunks of a large write in flight at once. Each chunk's reply waits on the
13
13
  * API and the guest, and the link idles while every chunk in flight waits:
@@ -59,15 +59,6 @@ function requestScope(options) {
59
59
  };
60
60
  return { request, check, wait };
61
61
  }
62
- /** DELETE /v1/sandboxes/{id}, for `sandbox.delete()` and
63
- * `runtime.sandboxes.delete(id)`. */
64
- export function deleteSandbox(t, id, options = {}) {
65
- return t.json({
66
- method: "DELETE",
67
- path: `/v1/sandboxes/${enc(id)}`,
68
- ...options,
69
- });
70
- }
71
62
  /* Web APIs only, so the sandbox's commands, processes and files work in a
72
63
  browser with a session token (Sandbox.fromSession): no Buffer, no
73
64
  node:crypto. */
@@ -97,26 +88,21 @@ function commandBody(command, options) {
97
88
  };
98
89
  }
99
90
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
100
- export class Sandbox {
101
- #info;
102
- #keepAlive;
91
+ export class Sandbox extends Machine {
103
92
  #t;
104
93
  files;
105
94
  processes;
106
95
  /** Short-lived tokens that let a browser reach this sandbox directly. */
107
96
  sessions;
108
97
  constructor(transport, info) {
98
+ super(SANDBOXES, transport, info);
109
99
  this.#t = transport;
110
- this.#info = info;
111
100
  this.files = new Files(transport, info.id);
112
101
  this.processes = new Processes(transport, info.id);
113
102
  this.sessions = new SandboxSessions(transport, info.id);
114
103
  for (const [name, make] of sandboxFactories())
115
104
  Object.defineProperty(this, name, { value: make(transport, this), enumerable: false });
116
105
  }
117
- get id() {
118
- return this.#info.id;
119
- }
120
106
  /** A sandbox reached with a session token instead of an API key, for code in
121
107
  * a browser: commands and their streams, processes, files and previews of
122
108
  * this one sandbox. Makes no call; `info` holds only the id until
@@ -141,41 +127,6 @@ export class Sandbox {
141
127
  });
142
128
  return new Sandbox(transport, { id: session.sandboxId });
143
129
  }
144
- /** The API's latest answer. `start`, the report of an image's start
145
- * command, is the create's alone: the API keeps no record of it, so a
146
- * later read of this sandbox keeps it here. */
147
- #keep(info) {
148
- const start = this.#info?.start;
149
- this.#info = info.start === undefined && start !== undefined ? { ...info, start } : info;
150
- }
151
- /** What the API last said about this sandbox. `refresh()` asks again. */
152
- get info() {
153
- return this.#info;
154
- }
155
- get state() {
156
- return this.#info.state;
157
- }
158
- async refresh(options = {}) {
159
- this.#keep(await this.#t.json({
160
- method: "GET",
161
- path: `/v1/sandboxes/${enc(this.id)}`,
162
- ...options,
163
- }));
164
- return this;
165
- }
166
- /** Waits (server-side, no polling) until the sandbox reaches `state`. */
167
- async waitFor(state, options = {}) {
168
- this.#keep(await this.#t.json({
169
- method: "GET",
170
- path: `/v1/sandboxes/${enc(this.id)}`,
171
- query: {
172
- waitFor: state,
173
- timeoutSeconds: options.timeoutSeconds ?? WAIT_FOR_TIMEOUT_SECONDS,
174
- },
175
- ...options,
176
- }));
177
- return this;
178
- }
179
130
  /** Runs a command and returns its exit code and output. A string runs under
180
131
  * `bash -c`; an array runs without a shell. With onStdout/onStderr the output
181
132
  * streams as it happens. A timeout is a result (timedOut: true, the output so
@@ -368,275 +319,6 @@ export class Sandbox {
368
319
  throw error;
369
320
  }
370
321
  }
371
- async stop(options = {}) {
372
- this.#keepAlive?.();
373
- return this.#lifecycle("stop", options);
374
- }
375
- async pause(options = {}) {
376
- return this.#lifecycle("pause", options);
377
- }
378
- /** Deletes it for good: stops it if it runs or is paused, deletes its disk
379
- * and paused memory, revokes its previews and ports, and removes it from
380
- * lists. Its snapshots, usage and audit entries stay. Deleting it again
381
- * answers the same. */
382
- async delete(options = {}) {
383
- this.#keepAlive?.();
384
- return deleteSandbox(this.#t, this.id, options);
385
- }
386
- /** Carries on a paused sandbox; for one with a time limit,
387
- * `timeoutSeconds` is its new one. */
388
- async wake(options = {}) {
389
- const { timeoutSeconds, ...rest } = options;
390
- try {
391
- return await this.#lifecycle("wake", rest, timeoutSeconds === undefined ? {} : { timeoutSeconds });
392
- }
393
- catch (error) {
394
- // Waking one that is already awake is done, not a mistake. With a new
395
- // lease asked for, the refusal stands: it was not given.
396
- if (!(error instanceof RuntimeError) || error.code !== "not_paused" || timeoutSeconds)
397
- throw error;
398
- await this.refresh(requestOnly(rest));
399
- if (this.state !== "running" && this.state !== "starting")
400
- throw error;
401
- return this;
402
- }
403
- }
404
- /** More time before its time limit ends (at most an hour ahead of now). A
405
- * sandbox with no time limit needs none: the call answers at once and
406
- * changes nothing. */
407
- async extend(seconds, options = {}) {
408
- return this.#lifecycle("extend", { ...options, wait: false }, { seconds });
409
- }
410
- /** Changes its name, labels, automatic wake, idle pause or persistence;
411
- * fields left out stay as they are. `persistent: true` keeps it running
412
- * while credit lasts (paid only). */
413
- async update(settings, options = {}) {
414
- return this.#lifecycle("update", { ...options, wait: false }, { ...settings });
415
- }
416
- /** Moves it to another image (id, name, name:tag or name@version), keeping
417
- * its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
418
- * volumes, environment, name and previews. Its processes restart, and
419
- * everything else on its old disk (sudo installs, apt packages, /etc) is
420
- * lost; snapshot it first to keep everything. A running sandbox is paused
421
- * first. A switch that fails is undone (`switch_undone`), the sandbox on
422
- * its old image with nothing lost. Charged as a wake. `keep` is optional:
423
- * "workspace" is the only value, and it is sent either way, so an API from
424
- * before it was optional accepts the call too. */
425
- async switchImage(image, options = {}) {
426
- const { keep = "workspace", ...rest } = options;
427
- this.#keep(await this.#t.json({
428
- method: "POST",
429
- path: `/v1/sandboxes/${enc(this.id)}:switch-image`,
430
- body: { image, keep },
431
- // A pause, two boots and the copy between them.
432
- wait: 120,
433
- ...requestOnly(rest),
434
- }));
435
- return this;
436
- }
437
- /** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
438
- * whole disk, volumes, environment, name and previews stay, and its
439
- * programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
440
- * persistent, and comes back running at the new size on the same server; a
441
- * paused or stopped one is started. Memory bills on the new size from then.
442
- * Refused, with nothing changed, when the server has no room
443
- * (`no_capacity`), above your quota or the trial's 2 vCPU and 4 GiB, or
444
- * while a snapshot or fork of it is being taken. `restart` is optional:
445
- * true is the only value, and it is sent either way, so an API from before
446
- * it was optional accepts the call too. */
447
- async resize(size, options = {}) {
448
- const { restart = true, ...rest } = options;
449
- this.#keep(await this.#t.json({
450
- method: "POST",
451
- path: `/v1/sandboxes/${enc(this.id)}:resize`,
452
- body: { ...size, restart },
453
- // A pause or stop and a cold boot.
454
- wait: 120,
455
- ...requestOnly(rest),
456
- }));
457
- return this;
458
- }
459
- /** Keeps a sandbox with a time limit running past it, in the background,
460
- * until stop() or the returned function ends it: every `everySeconds` (60)
461
- * it extends the limit so that `marginSeconds` (600) remain, never more
462
- * than the hour ahead the API allows. A sandbox with no time limit
463
- * (`endsAt` null) needs none, and is only watched. Running time is billed
464
- * as it is used. A paused sandbox is left paused (a request wakes it,
465
- * unless autoWake is off); a stopped one ends the loop. It does not keep a
466
- * Node or Bun process alive by itself. */
467
- keepAlive(options = {}) {
468
- this.#keepAlive?.();
469
- const every = Math.max(10, options.everySeconds ?? 60) * 1000;
470
- const margin = Math.min(3600, Math.max(60, options.marginSeconds ?? KEEP_ALIVE_MARGIN_SECONDS));
471
- let ended = false;
472
- let timer;
473
- const end = () => {
474
- ended = true;
475
- if (timer)
476
- clearTimeout(timer);
477
- if (this.#keepAlive === end)
478
- this.#keepAlive = undefined;
479
- };
480
- const tick = async () => {
481
- if (ended)
482
- return;
483
- try {
484
- await this.refresh();
485
- if (this.state === "stopped" || this.state === "stopping")
486
- return end();
487
- // No time limit: nothing to extend (an older server sends no endsAt).
488
- if (this.state === "running" && this.info.endsAt !== null) {
489
- const left = (Date.parse(this.info.endsAt ?? this.info.expiresAt) - Date.now()) / 1000;
490
- const need = Math.ceil(margin - left);
491
- if (need >= 1)
492
- await this.extend(Math.min(3600, need));
493
- }
494
- }
495
- catch (error) {
496
- options.onError?.(error);
497
- }
498
- if (ended)
499
- return;
500
- timer = setTimeout(() => void tick(), every);
501
- timer.unref?.();
502
- };
503
- this.#keepAlive = end;
504
- void tick();
505
- return end;
506
- }
507
- /** Days a paused sandbox is kept before deletion (1 to 365). */
508
- async setRetention(days, options = {}) {
509
- return this.#lifecycle("retention", { ...options, wait: false }, { days });
510
- }
511
- /** Keeps this sandbox's whole machine (files, memory, running processes) as
512
- * a snapshot to start new sandboxes from. A running sandbox is paused for
513
- * the moment it takes, then woken; a paused one stays paused. Past
514
- * `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
515
- * the error names the snapshot. */
516
- async snapshot(options = {}) {
517
- const { idempotencyKey, signal, timeoutMs, ...body } = options;
518
- if (body.mode !== undefined && body.mode !== "memory" && body.mode !== "disk")
519
- throw new TypeError("Snapshot mode must be memory or disk.");
520
- signal?.throwIfAborted();
521
- // One minute, whatever `timeoutMs` said, was too short: pausing a busy
522
- // 16 vCPU, 32 GiB sandbox writes its memory out first (4 October 2026).
523
- const until = performance.now() + (timeoutMs && timeoutMs > 0 ? timeoutMs : SNAPSHOT_DEADLINE_MS);
524
- let running = false;
525
- /** The capture in progress, once the server has one. */
526
- let capturing;
527
- let late = false;
528
- const request = () => {
529
- signal?.throwIfAborted();
530
- const left = Math.ceil(until - performance.now());
531
- if (left <= 0 && capturing) {
532
- late = true;
533
- throw new RuntimeError({
534
- code: "snapshot_timeout",
535
- status: 0,
536
- message: `Snapshot ${capturing} was still capturing at the deadline.${running ? " The sandbox stays paused until it ends: waking it now would fail the capture." : ""}`,
537
- hint: `Wait until runtime.snapshots.get("${capturing}") is ready${running ? ", then wake the sandbox" : ""}, or pass a longer timeoutMs.`,
538
- details: { snapshotId: capturing, sourceSandboxId: this.id },
539
- });
540
- }
541
- if (left <= 0)
542
- throw new RuntimeError({
543
- code: "snapshot_timeout",
544
- status: 0,
545
- message: "Snapshot capture ran past its deadline.",
546
- });
547
- return { ...(signal ? { signal } : {}), timeoutMs: left };
548
- };
549
- await this.refresh(request());
550
- // Straight after a fork or a wake the sandbox is still `resuming`, and
551
- // after a pause still `pausing`: wait for where it is going, or a
552
- // snapshot of it is refused as not paused (user lane, 23 September 2026).
553
- if (this.state === "resuming" || this.state === "starting")
554
- await this.waitFor("running", request());
555
- else if (this.state === "pausing")
556
- await this.waitFor("paused", request());
557
- running = this.state === "running";
558
- // Check before entering cleanup: an abort during refresh must not pause or wake.
559
- const pauseOptions = running ? request() : undefined;
560
- let captured;
561
- let primary;
562
- let failed = false;
563
- try {
564
- if (running)
565
- await this.pause(pauseOptions);
566
- let snapshot = await this.#t.json({
567
- method: "POST",
568
- path: `/v1/sandboxes/${enc(this.id)}:snapshot`,
569
- body,
570
- wait: 10,
571
- ...(idempotencyKey ? { idempotencyKey } : {}),
572
- ...request(),
573
- });
574
- captured = snapshot;
575
- // Prefer: wait is bounded on the server. A capture still in progress
576
- // must keep its source paused, or the worker refuses it after we wake.
577
- while (snapshot.state === "capturing") {
578
- capturing = snapshot.id;
579
- request();
580
- await new Promise((resolve) => setTimeout(resolve, 200));
581
- snapshot = await this.#t.json({
582
- method: "GET",
583
- path: `/v1/snapshots/${enc(snapshot.id)}`,
584
- ...request(),
585
- });
586
- }
587
- if (snapshot.state !== "ready")
588
- throw new RuntimeError({
589
- code: "snapshot_failed",
590
- status: 409,
591
- message: snapshot.error ?? `Snapshot capture ended in state ${snapshot.state}.`,
592
- details: { snapshotId: snapshot.id },
593
- });
594
- if (body.mode === "disk" && snapshot.mode !== "disk")
595
- throw new RuntimeError({
596
- code: "snapshot_mode_mismatch",
597
- status: 409,
598
- message: "The server did not confirm a disk-only snapshot.",
599
- details: { snapshotId: snapshot.id },
600
- });
601
- captured = snapshot;
602
- }
603
- catch (error) {
604
- failed = true;
605
- primary = error;
606
- // A request cut off by the deadline mid-capture is late all the same.
607
- if (capturing && performance.now() >= until)
608
- late = true;
609
- }
610
- // A capture still running needs its source paused: the worker fails one
611
- // whose source woke. The timeout says so and leaves the wake to the caller.
612
- if (running && !late) {
613
- try {
614
- await this.wake();
615
- }
616
- catch (wakeError) {
617
- const recovery = {
618
- ...(captured ? { snapshotId: captured.id } : {}),
619
- sourceSandboxId: this.id,
620
- sourceWakeError: {
621
- ...(wakeError instanceof RuntimeError ? { code: wakeError.code } : {}),
622
- message: wakeError instanceof Error ? wakeError.message : String(wakeError),
623
- },
624
- };
625
- const reported = failed ? primary : wakeError;
626
- if (reported instanceof Error && Object.isExtensible(reported)) {
627
- if (reported instanceof RuntimeError)
628
- Object.assign(reported, { details: { ...reported.details, ...recovery } });
629
- else
630
- Object.assign(reported, recovery);
631
- }
632
- if (!failed)
633
- throw wakeError;
634
- }
635
- }
636
- if (failed)
637
- throw primary;
638
- return captured;
639
- }
640
322
  async fork(options = {}) {
641
323
  const { idempotencyKey, signal, timeoutMs, ...body } = options;
642
324
  const reply = await this.#t.json({
@@ -649,29 +331,6 @@ export class Sandbox {
649
331
  const sandboxes = reply.sandboxes.map((info) => new Sandbox(this.#t, info));
650
332
  return options.count === undefined ? sandboxes[0] : sandboxes;
651
333
  }
652
- /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
653
- async restart(options = {}) {
654
- return this.#lifecycle("restart", options);
655
- }
656
- async #lifecycle(verb, options, body = {}) {
657
- this.#keep(await this.#t.json({
658
- method: "POST",
659
- path: `/v1/sandboxes/${enc(this.id)}:${verb}`,
660
- body,
661
- wait: options.wait === false ? 0 : 60,
662
- ...requestOnly(options),
663
- }));
664
- return this;
665
- }
666
- async [Symbol.asyncDispose]() {
667
- this.#keepAlive?.();
668
- if (this.#info.state === "stopped")
669
- return;
670
- await this.stop({ wait: false }).catch(() => undefined);
671
- }
672
- toJSON() {
673
- return this.#info;
674
- }
675
334
  }
676
335
  /** Where a reader of a process's output is, in bytes of stdout and stderr
677
336
  * together. Output it did not receive, named by the server or not, becomes a
@@ -747,15 +406,6 @@ function pick(options) {
747
406
  ...(options.timeoutMs ? { timeoutMs: options.timeoutMs + 60_000 } : {}),
748
407
  };
749
408
  }
750
- /** Any other call's request options, as the caller gave them: `timeoutMs` is
751
- * the call's whole deadline, and 0 turns it off. */
752
- function requestOnly(options) {
753
- return {
754
- ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
755
- ...(options.signal ? { signal: options.signal } : {}),
756
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
757
- };
758
- }
759
409
  /** Sessions: a token your backend makes and hands to your own frontend, which
760
410
  * then runs commands, reads and writes files and reaches previews in this one
761
411
  * sandbox directly, without proxying through your servers. A session cannot
@@ -1752,11 +1402,7 @@ export class Files {
1752
1402
  }
1753
1403
  }
1754
1404
  }
1405
+ /** One page of `runtime.sandboxes.list()`, and the pages after it. */
1755
1406
  export function sandboxPage(t, body, query, options = {}) {
1756
- return new Page(body.data.map((info) => new Sandbox(t, info)), body.nextCursor, async (cursor) => sandboxPage(t, await t.json({
1757
- method: "GET",
1758
- path: "/v1/sandboxes",
1759
- query: Object.assign({}, query, { cursor }),
1760
- ...options,
1761
- }), query, options));
1407
+ return machinePage(SANDBOXES, t, (transport, info) => new Sandbox(transport, info), body, query, options);
1762
1408
  }
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.11.1";
2
+ export declare const VERSION = "0.11.2";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
5
5
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -50,7 +50,7 @@ export type Call = RequestOptions & {
50
50
  retry?: boolean;
51
51
  /** How long to keep retrying, with the same key and input, a refusal that
52
52
  * clears when a sandbox stops or a host frees room (WAITS_FOR_ROOM): a
53
- * full trial, a full quota, a full region. Then the refusal is thrown as
53
+ * full slots without credit, a full quota, a full region. Then the refusal is thrown as
54
54
  * it came. Only a create sets it; 0 or absent fails at once. */
55
55
  waitForCapacityMs?: number;
56
56
  /** Called each time a call waits for room, with the refusal it is waiting
@@ -103,6 +103,8 @@ export declare class Transport {
103
103
  /** Connections held at once, answers and streams together; the rest
104
104
  * wait their turn. Default 48: one address may hold 64 to the API. */
105
105
  maxConnections?: number;
106
+ /** Called each time a call has to wait for one of those connections. */
107
+ onQueued?: (maxConnections: number) => void;
106
108
  /** How long a create waits for room. Default 120 000; 0 fails at once. */
107
109
  waitForCapacityMs?: number;
108
110
  });
package/dist/transport.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
- export const VERSION = "0.11.1";
3
+ export const VERSION = "0.11.2";
4
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
5
5
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
6
6
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -120,7 +120,7 @@ function abortReason(signal) {
120
120
  }
121
121
  /** At most `maximum` holders at once; the rest wait their turn, first come
122
122
  * first served. */
123
- function slots(maximum) {
123
+ function slots(maximum, onWait) {
124
124
  let active = 0;
125
125
  const waiting = [];
126
126
  return {
@@ -130,6 +130,7 @@ function slots(maximum) {
130
130
  active++;
131
131
  return;
132
132
  }
133
+ onWait?.();
133
134
  await new Promise((resolve, reject) => {
134
135
  const ready = () => {
135
136
  signal?.removeEventListener("abort", abort);
@@ -203,8 +204,9 @@ export class Transport {
203
204
  this.#maxRetries = options.maxRetries ?? 4;
204
205
  this.#client = options.client ?? `sdk-js/${VERSION}`;
205
206
  const most = Math.max(1, options.maxConnections ?? DEFAULT_MAX_CONNECTIONS);
206
- this.#slots = slots(most);
207
- this.#streams = slots(Math.max(1, most - KEPT_FOR_CALLS));
207
+ const queued = options.onQueued && (() => options.onQueued(most));
208
+ this.#slots = slots(most, queued);
209
+ this.#streams = slots(Math.max(1, most - KEPT_FOR_CALLS), queued);
208
210
  this.waitForCapacityMs = Math.max(0, options.waitForCapacityMs ?? 120_000);
209
211
  }
210
212
  /** The key, found once on first use when it was not given. */