withruntime 0.11.0 → 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.
- package/BLAXEL.md +8 -9
- package/DAYTONA.md +8 -9
- package/E2B.md +63 -21
- package/README.md +6 -5
- package/README.src.md +6 -5
- package/VERCEL.md +3 -2
- package/compatibility-lock.json +69 -14
- package/dist/api-defaults.d.ts +1 -1
- package/dist/api-defaults.js +1 -1
- package/dist/blaxel/errors.js +4 -4
- package/dist/blaxel/lookup.d.ts +2 -1
- package/dist/blaxel/lookup.js +2 -1
- package/dist/blaxel/sandbox.d.ts +8 -7
- package/dist/blaxel/sandbox.js +16 -12
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +99 -27
- package/dist/client.d.ts +16 -60
- package/dist/client.js +14 -103
- package/dist/compat/files.js +5 -1
- package/dist/compat/lease.d.ts +1 -0
- package/dist/compat/lease.js +14 -8
- package/dist/credentials.d.ts +9 -0
- package/dist/credentials.js +26 -4
- package/dist/daytona/daytona.d.ts +1 -2
- package/dist/daytona/daytona.js +30 -14
- package/dist/daytona/sandbox.d.ts +9 -4
- package/dist/daytona/sandbox.js +24 -5
- package/dist/e2b/client.d.ts +15 -2
- package/dist/e2b/client.js +61 -7
- package/dist/e2b/commands.d.ts +3 -0
- package/dist/e2b/commands.js +16 -5
- package/dist/e2b/errors.d.ts +26 -2
- package/dist/e2b/errors.js +38 -5
- package/dist/e2b/filesystem.d.ts +1 -1
- package/dist/e2b/filesystem.js +82 -16
- package/dist/e2b/index.d.ts +2 -2
- package/dist/e2b/index.js +2 -2
- package/dist/e2b/sandbox.d.ts +25 -12
- package/dist/e2b/sandbox.js +196 -165
- package/dist/e2b/unsupported.d.ts +19 -0
- package/dist/e2b/unsupported.js +21 -0
- package/dist/e2b/users.d.ts +9 -0
- package/dist/e2b/users.js +22 -0
- package/dist/errors.d.ts +10 -4
- package/dist/errors.js +18 -10
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/machine.d.ts +180 -0
- package/dist/machine.js +504 -0
- package/dist/modal/index.js +6 -2
- package/dist/network-cli.js +7 -7
- package/dist/openai-agents/index.d.ts +1 -1
- package/dist/openai-agents/index.js +1 -1
- package/dist/products/browser.d.ts +2 -1
- package/dist/products/browser.js +9 -19
- package/dist/products/desktop.d.ts +2 -1
- package/dist/products/desktop.js +5 -30
- package/dist/products/jobs.d.ts +6 -5
- package/dist/products/jobs.js +6 -3
- package/dist/products/limits.d.ts +4 -10
- package/dist/products/mcp.d.ts +5 -1
- package/dist/products/mcp.js +29 -8
- package/dist/products/network-products.d.ts +1 -1
- package/dist/products/network-products.js +1 -1
- package/dist/products/network.d.ts +1 -1
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/private-network.d.ts +1 -1
- package/dist/products/secrets.d.ts +3 -3
- package/dist/products/switching.d.ts +2 -3
- package/dist/runloop/index.js +6 -2
- package/dist/sandbox.d.ts +7 -84
- package/dist/sandbox.js +58 -345
- package/dist/tar.d.ts +2 -0
- package/dist/tar.js +74 -16
- package/dist/transport.d.ts +36 -8
- package/dist/transport.js +174 -37
- package/dist/types.d.ts +102 -12
- package/dist/vercel/filesystem.js +3 -1
- package/dist/vercel/sandbox.d.ts +18 -12
- package/dist/vercel/sandbox.js +27 -15
- package/dist/wait.d.ts +12 -0
- package/dist/wait.js +47 -0
- package/package.json +1 -1
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "0.11.
|
|
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
|
|
@@ -7,6 +7,11 @@ export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
|
7
7
|
* because the hop never leaves the machine: it goes from the program to the
|
|
8
8
|
* guest's own proxy and over the sandbox's private channel to its host. */
|
|
9
9
|
export declare const SANDBOX_BASE_URL = "http://runtime.internal";
|
|
10
|
+
/** Every Runtime sandbox has this file; guest-net.py keeps it current. Tests
|
|
11
|
+
* move it, so the machine running them does not decide what they see. */
|
|
12
|
+
export declare const sandboxMarker: {
|
|
13
|
+
path: string;
|
|
14
|
+
};
|
|
10
15
|
/** True in a Runtime sandbox. Node and Bun only; a browser is never one. */
|
|
11
16
|
export declare function inRuntimeSandbox(marker?: string): boolean;
|
|
12
17
|
/** Where calls go when no base URL is given: RUNTIME_API_URL, then Runtime's
|
|
@@ -45,23 +50,40 @@ export type Call = RequestOptions & {
|
|
|
45
50
|
retry?: boolean;
|
|
46
51
|
/** How long to keep retrying, with the same key and input, a refusal that
|
|
47
52
|
* clears when a sandbox stops or a host frees room (WAITS_FOR_ROOM): a
|
|
48
|
-
* full
|
|
53
|
+
* full slots without credit, a full quota, a full region. Then the refusal is thrown as
|
|
49
54
|
* it came. Only a create sets it; 0 or absent fails at once. */
|
|
50
55
|
waitForCapacityMs?: number;
|
|
51
56
|
/** Called each time a call waits for room, with the refusal it is waiting
|
|
52
57
|
* out and how long it will sleep before sending the call again. */
|
|
53
58
|
onCapacityWait?: (refusal: RuntimeError, waitMs: number) => void;
|
|
59
|
+
/** Internal: when the call's own deadline ends (performance.now()), set
|
|
60
|
+
* once the call is prepared; absent when it has none. */
|
|
61
|
+
deadlineAt?: number;
|
|
54
62
|
};
|
|
63
|
+
/** How long one attempt waits for its answer to start. The API starts every
|
|
64
|
+
* answer within 120 s: Caddy answers 503 itself past that, and a longer call
|
|
65
|
+
* gets its 200 at 90 s and then its JSON (runtime-late-answer). An attempt
|
|
66
|
+
* with no answer by then is talking to a server that is gone, such as a
|
|
67
|
+
* controller that died with the call in flight while its address moved to
|
|
68
|
+
* the new leader: a client that only waits sends nothing the new leader could
|
|
69
|
+
* refuse, so it waited out the whole deadline and failed (the leader drill on
|
|
70
|
+
* vin-5, 4 October 2026). It is sent again under the same key. */
|
|
71
|
+
export declare const ANSWER_START_MS = 125000;
|
|
55
72
|
export declare function apiOrigin(value: string): string;
|
|
56
73
|
export declare function encodeQuery(query: Query | undefined): string;
|
|
74
|
+
/** Connections a client holds at once by default: under the 64 one address
|
|
75
|
+
* may hold to the API (api.md, "Limits"), with room for a second client. */
|
|
76
|
+
export declare const DEFAULT_MAX_CONNECTIONS = 48;
|
|
57
77
|
/** One connection pool, one retry policy, one error shape for every product.
|
|
58
78
|
* Node's and Bun's fetch keep connections alive, so a client reused across
|
|
59
79
|
* calls pays TLS once. Over HTTP/1.1 every call in flight holds its own
|
|
60
|
-
* connection,
|
|
61
|
-
*
|
|
62
|
-
* `maxConnections` at once
|
|
63
|
-
*
|
|
64
|
-
*
|
|
80
|
+
* connection, and one address may hold 64 to the API before it has used a
|
|
81
|
+
* valid key: past that the API's edge holds new ones back until one closes.
|
|
82
|
+
* So a client holds at most `maxConnections` at once,
|
|
83
|
+
* answers and streams together, and the rest queue here, first come first
|
|
84
|
+
* served, and wait rather than fail. Streams (a command's output, a download,
|
|
85
|
+
* a watch) take at most `maxConnections` less KEPT_FOR_CALLS of them. A
|
|
86
|
+
* terminal or tunnel's WebSocket is not counted. */
|
|
65
87
|
export declare class Transport {
|
|
66
88
|
#private;
|
|
67
89
|
readonly baseUrl: string;
|
|
@@ -73,10 +95,16 @@ export declare class Transport {
|
|
|
73
95
|
baseUrl?: string;
|
|
74
96
|
fetch?: typeof fetch;
|
|
75
97
|
timeoutMs?: number;
|
|
98
|
+
/** How long one attempt waits for its answer to start before it is sent
|
|
99
|
+
* again under the same key. Default ANSWER_START_MS. */
|
|
100
|
+
answerStartMs?: number;
|
|
76
101
|
maxRetries?: number;
|
|
77
102
|
client?: string;
|
|
78
|
-
/**
|
|
103
|
+
/** Connections held at once, answers and streams together; the rest
|
|
104
|
+
* wait their turn. Default 48: one address may hold 64 to the API. */
|
|
79
105
|
maxConnections?: number;
|
|
106
|
+
/** Called each time a call has to wait for one of those connections. */
|
|
107
|
+
onQueued?: (maxConnections: number) => void;
|
|
80
108
|
/** How long a create waits for room. Default 120 000; 0 fails at once. */
|
|
81
109
|
waitForCapacityMs?: number;
|
|
82
110
|
});
|
package/dist/transport.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ConnectionError, DELIBERATE, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
|
|
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.
|
|
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
|
|
@@ -8,10 +8,11 @@ export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
|
8
8
|
* because the hop never leaves the machine: it goes from the program to the
|
|
9
9
|
* guest's own proxy and over the sandbox's private channel to its host. */
|
|
10
10
|
export const SANDBOX_BASE_URL = "http://runtime.internal";
|
|
11
|
-
/** Every Runtime sandbox has this file; guest-net.py keeps it current.
|
|
12
|
-
|
|
11
|
+
/** Every Runtime sandbox has this file; guest-net.py keeps it current. Tests
|
|
12
|
+
* move it, so the machine running them does not decide what they see. */
|
|
13
|
+
export const sandboxMarker = { path: "/run/runtime/environment.json" };
|
|
13
14
|
/** True in a Runtime sandbox. Node and Bun only; a browser is never one. */
|
|
14
|
-
export function inRuntimeSandbox(marker =
|
|
15
|
+
export function inRuntimeSandbox(marker = sandboxMarker.path) {
|
|
15
16
|
try {
|
|
16
17
|
const host = globalThis.process;
|
|
17
18
|
return host?.getBuiltinModule?.("node:fs")?.existsSync?.(marker) === true;
|
|
@@ -34,6 +35,18 @@ export function reachable(origin, inSandbox = inRuntimeSandbox) {
|
|
|
34
35
|
function processEnv() {
|
|
35
36
|
return (globalThis.process?.env ?? {});
|
|
36
37
|
}
|
|
38
|
+
/** How long an answer that ran nothing (NOTHING_RAN) is retried for a call
|
|
39
|
+
* that has no deadline of its own. */
|
|
40
|
+
const NOTHING_RAN_MS = 300_000;
|
|
41
|
+
/** How long one attempt waits for its answer to start. The API starts every
|
|
42
|
+
* answer within 120 s: Caddy answers 503 itself past that, and a longer call
|
|
43
|
+
* gets its 200 at 90 s and then its JSON (runtime-late-answer). An attempt
|
|
44
|
+
* with no answer by then is talking to a server that is gone, such as a
|
|
45
|
+
* controller that died with the call in flight while its address moved to
|
|
46
|
+
* the new leader: a client that only waits sends nothing the new leader could
|
|
47
|
+
* refuse, so it waited out the whole deadline and failed (the leader drill on
|
|
48
|
+
* vin-5, 4 October 2026). It is sent again under the same key. */
|
|
49
|
+
export const ANSWER_START_MS = 125_000;
|
|
37
50
|
export function apiOrigin(value) {
|
|
38
51
|
const url = new URL(value);
|
|
39
52
|
// runtime.internal is reserved and never resolves outside a sandbox, so a
|
|
@@ -107,7 +120,7 @@ function abortReason(signal) {
|
|
|
107
120
|
}
|
|
108
121
|
/** At most `maximum` holders at once; the rest wait their turn, first come
|
|
109
122
|
* first served. */
|
|
110
|
-
function slots(maximum) {
|
|
123
|
+
function slots(maximum, onWait) {
|
|
111
124
|
let active = 0;
|
|
112
125
|
const waiting = [];
|
|
113
126
|
return {
|
|
@@ -117,6 +130,7 @@ function slots(maximum) {
|
|
|
117
130
|
active++;
|
|
118
131
|
return;
|
|
119
132
|
}
|
|
133
|
+
onWait?.();
|
|
120
134
|
await new Promise((resolve, reject) => {
|
|
121
135
|
const ready = () => {
|
|
122
136
|
signal?.removeEventListener("abort", abort);
|
|
@@ -141,14 +155,23 @@ function slots(maximum) {
|
|
|
141
155
|
},
|
|
142
156
|
};
|
|
143
157
|
}
|
|
158
|
+
/** Connections a client holds at once by default: under the 64 one address
|
|
159
|
+
* may hold to the API (api.md, "Limits"), with room for a second client. */
|
|
160
|
+
export const DEFAULT_MAX_CONNECTIONS = 48;
|
|
161
|
+
/** Connections a client keeps for answers read whole, however many streams
|
|
162
|
+
* are open: a stream's reader that makes a call (an output callback writing a
|
|
163
|
+
* file) never waits behind streams, its own included. */
|
|
164
|
+
const KEPT_FOR_CALLS = 8;
|
|
144
165
|
/** One connection pool, one retry policy, one error shape for every product.
|
|
145
166
|
* Node's and Bun's fetch keep connections alive, so a client reused across
|
|
146
167
|
* calls pays TLS once. Over HTTP/1.1 every call in flight holds its own
|
|
147
|
-
* connection,
|
|
148
|
-
*
|
|
149
|
-
* `maxConnections` at once
|
|
150
|
-
*
|
|
151
|
-
*
|
|
168
|
+
* connection, and one address may hold 64 to the API before it has used a
|
|
169
|
+
* valid key: past that the API's edge holds new ones back until one closes.
|
|
170
|
+
* So a client holds at most `maxConnections` at once,
|
|
171
|
+
* answers and streams together, and the rest queue here, first come first
|
|
172
|
+
* served, and wait rather than fail. Streams (a command's output, a download,
|
|
173
|
+
* a watch) take at most `maxConnections` less KEPT_FOR_CALLS of them. A
|
|
174
|
+
* terminal or tunnel's WebSocket is not counted. */
|
|
152
175
|
export class Transport {
|
|
153
176
|
baseUrl;
|
|
154
177
|
#apiKey;
|
|
@@ -157,9 +180,11 @@ export class Transport {
|
|
|
157
180
|
/** Whether calls go through envFetch, so a failure can name its proxy. */
|
|
158
181
|
#routed;
|
|
159
182
|
#timeoutMs;
|
|
183
|
+
#answerStartMs;
|
|
160
184
|
#maxRetries;
|
|
161
185
|
#client;
|
|
162
186
|
#slots;
|
|
187
|
+
#streams;
|
|
163
188
|
/** What `sandboxes.create` waits for room by default. */
|
|
164
189
|
waitForCapacityMs;
|
|
165
190
|
constructor(options) {
|
|
@@ -175,9 +200,13 @@ export class Transport {
|
|
|
175
200
|
this.#fetch = options.fetch ?? envFetch;
|
|
176
201
|
this.#routed = options.fetch === undefined;
|
|
177
202
|
this.#timeoutMs = options.timeoutMs ?? 300_000;
|
|
203
|
+
this.#answerStartMs = options.answerStartMs ?? ANSWER_START_MS;
|
|
178
204
|
this.#maxRetries = options.maxRetries ?? 4;
|
|
179
205
|
this.#client = options.client ?? `sdk-js/${VERSION}`;
|
|
180
|
-
|
|
206
|
+
const most = Math.max(1, options.maxConnections ?? DEFAULT_MAX_CONNECTIONS);
|
|
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);
|
|
181
210
|
this.waitForCapacityMs = Math.max(0, options.waitForCapacityMs ?? 120_000);
|
|
182
211
|
}
|
|
183
212
|
/** The key, found once on first use when it was not given. */
|
|
@@ -225,6 +254,7 @@ export class Transport {
|
|
|
225
254
|
return {
|
|
226
255
|
...call,
|
|
227
256
|
timeoutMs: 0,
|
|
257
|
+
...(timeoutMs === 0 ? {} : { deadlineAt: performance.now() + timeoutMs + roomMs }),
|
|
228
258
|
...(signal ? { signal } : {}),
|
|
229
259
|
...(call.method !== "GET"
|
|
230
260
|
? { idempotencyKey: call.idempotencyKey ?? crypto.randomUUID() }
|
|
@@ -251,7 +281,17 @@ export class Transport {
|
|
|
251
281
|
cause,
|
|
252
282
|
});
|
|
253
283
|
}
|
|
254
|
-
|
|
284
|
+
/** Sleeps between tries. A call holding a connection slot (`gate`) lends
|
|
285
|
+
* it out while it sleeps, since a sleep holds no connection, and takes a
|
|
286
|
+
* slot again before it sends: a thousand creates waiting out a full region
|
|
287
|
+
* or a 429 would otherwise keep every other call of this client, the stop
|
|
288
|
+
* that frees room included, queued behind them. */
|
|
289
|
+
async #rest(ms, signal, gate) {
|
|
290
|
+
gate?.lend();
|
|
291
|
+
await sleep(ms, signal);
|
|
292
|
+
await gate?.take();
|
|
293
|
+
}
|
|
294
|
+
async #send(call, consume, gate) {
|
|
255
295
|
let apiKey;
|
|
256
296
|
try {
|
|
257
297
|
call.signal?.throwIfAborted();
|
|
@@ -271,6 +311,9 @@ export class Transport {
|
|
|
271
311
|
const body = call.bytes ?? (call.body === undefined ? undefined : JSON.stringify(call.body));
|
|
272
312
|
const retry = call.retry ?? true;
|
|
273
313
|
let attempt = 0;
|
|
314
|
+
// An answer that ran nothing is sent again until the call's deadline.
|
|
315
|
+
const idleUntil = call.deadlineAt ?? performance.now() + NOTHING_RAN_MS;
|
|
316
|
+
let idleAttempt = 0;
|
|
274
317
|
for (;;) {
|
|
275
318
|
let response;
|
|
276
319
|
let inCallback = false;
|
|
@@ -282,13 +325,20 @@ export class Transport {
|
|
|
282
325
|
// The API never redirects, so a redirect is refused here exactly as
|
|
283
326
|
// "error" refused it: thrown, retried, then a connection_error.
|
|
284
327
|
signal?.throwIfAborted();
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
328
|
+
const started = new AbortController();
|
|
329
|
+
const unanswered = setTimeout(() => started.abort(new DOMException("No answer started.", "TimeoutError")), this.#answerStartMs);
|
|
330
|
+
try {
|
|
331
|
+
response = await send(url, {
|
|
332
|
+
method: call.method,
|
|
333
|
+
headers: this.#headers(call, key, apiKey),
|
|
334
|
+
redirect: "manual",
|
|
335
|
+
signal: signal ? AbortSignal.any([signal, started.signal]) : started.signal,
|
|
336
|
+
...(body === undefined ? {} : { body: body }),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
clearTimeout(unanswered);
|
|
341
|
+
}
|
|
292
342
|
if (response.type === "opaqueredirect" ||
|
|
293
343
|
(response.status >= 300 && response.status < 400)) {
|
|
294
344
|
await response.body?.cancel().catch(() => { });
|
|
@@ -350,7 +400,23 @@ export class Transport {
|
|
|
350
400
|
inCallback = true;
|
|
351
401
|
call.onCapacityWait?.(error, pause);
|
|
352
402
|
inCallback = false;
|
|
353
|
-
await
|
|
403
|
+
await this.#rest(pause, signal, gate);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Nothing ran: safe to send again for any call, past maxRetries,
|
|
407
|
+
// with growing waits, until the deadline; then this refusal stands.
|
|
408
|
+
// A late answer's failure came after the work began, so only a
|
|
409
|
+
// guest's own refusal (`guest_busy`, before anything is journaled)
|
|
410
|
+
// still says nothing ran.
|
|
411
|
+
if ([429, 503].includes(status) &&
|
|
412
|
+
NOTHING_RAN.has(error.code) &&
|
|
413
|
+
(!late || error.code === "guest_busy")) {
|
|
414
|
+
const header = Number(response.headers.get("retry-after"));
|
|
415
|
+
const base = error.retryAfterMs ?? (Number.isFinite(header) && header > 0 ? header * 1000 : 250);
|
|
416
|
+
const pause = Math.min(30_000, base * 2 ** Math.min(idleAttempt++, 3)) * (0.9 + Math.random() * 0.2);
|
|
417
|
+
if (pause >= idleUntil - performance.now())
|
|
418
|
+
throw error;
|
|
419
|
+
await this.#rest(pause, signal, gate);
|
|
354
420
|
continue;
|
|
355
421
|
}
|
|
356
422
|
const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
|
|
@@ -359,7 +425,7 @@ export class Transport {
|
|
|
359
425
|
const header = Number(response.headers.get("retry-after"));
|
|
360
426
|
const wait = error.retryAfterMs ??
|
|
361
427
|
(Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
|
|
362
|
-
await
|
|
428
|
+
await this.#rest(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal, gate);
|
|
363
429
|
}
|
|
364
430
|
catch (cause) {
|
|
365
431
|
// Deliberate API refusals and unusable proxy settings cannot be
|
|
@@ -373,25 +439,42 @@ export class Transport {
|
|
|
373
439
|
throw cause;
|
|
374
440
|
if (signal?.aborted || !retry || attempt >= this.#maxRetries)
|
|
375
441
|
throw this.#connectionError(call, cause);
|
|
376
|
-
await
|
|
377
|
-
throw this.#connectionError(call, cause);
|
|
442
|
+
await this.#rest(backoff(attempt), signal, gate).catch((cause) => {
|
|
443
|
+
throw cause instanceof RuntimeError ? cause : this.#connectionError(call, cause);
|
|
378
444
|
});
|
|
379
445
|
}
|
|
380
446
|
attempt++;
|
|
381
447
|
}
|
|
382
448
|
}
|
|
449
|
+
/** A hold on `kinds` of slot, taken in order and given back together. */
|
|
450
|
+
#gate(call, kinds) {
|
|
451
|
+
let held = 0;
|
|
452
|
+
const lend = () => {
|
|
453
|
+
while (held > 0)
|
|
454
|
+
kinds[--held].give();
|
|
455
|
+
};
|
|
456
|
+
return {
|
|
457
|
+
take: async () => {
|
|
458
|
+
try {
|
|
459
|
+
for (; held < kinds.length; held++)
|
|
460
|
+
await kinds[held].take(call.signal);
|
|
461
|
+
}
|
|
462
|
+
catch (cause) {
|
|
463
|
+
lend();
|
|
464
|
+
throw this.#connectionError(call, cause);
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
lend,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
383
470
|
async #queued(call, consume) {
|
|
471
|
+
const gate = this.#gate(call, [this.#slots]);
|
|
472
|
+
await gate.take();
|
|
384
473
|
try {
|
|
385
|
-
await this.#
|
|
386
|
-
}
|
|
387
|
-
catch (cause) {
|
|
388
|
-
throw this.#connectionError(call, cause);
|
|
389
|
-
}
|
|
390
|
-
try {
|
|
391
|
-
return await this.#send(call, consume);
|
|
474
|
+
return await this.#send(call, consume, gate);
|
|
392
475
|
}
|
|
393
476
|
finally {
|
|
394
|
-
|
|
477
|
+
gate.lend();
|
|
395
478
|
}
|
|
396
479
|
}
|
|
397
480
|
async json(call) {
|
|
@@ -426,11 +509,12 @@ export class Transport {
|
|
|
426
509
|
* errors with `download_incomplete` if it ends short of the length the API
|
|
427
510
|
* promised; it is never retried part way. */
|
|
428
511
|
async fileStream(call) {
|
|
429
|
-
const response = await this
|
|
512
|
+
const { response, release } = await this.#opened(this.#prepare(call));
|
|
430
513
|
const expected = promisedLength(response.headers);
|
|
431
514
|
const body = response.body ?? new ReadableStream({ start: (c) => c.close() });
|
|
432
515
|
let got = 0;
|
|
433
|
-
|
|
516
|
+
const reader = body
|
|
517
|
+
.pipeThrough(new TransformStream({
|
|
434
518
|
transform(chunk, controller) {
|
|
435
519
|
got += chunk.byteLength;
|
|
436
520
|
if (expected !== undefined && got > expected)
|
|
@@ -442,13 +526,65 @@ export class Transport {
|
|
|
442
526
|
if (expected !== undefined && got !== expected)
|
|
443
527
|
controller.error(incompleteDownload(got, expected));
|
|
444
528
|
},
|
|
445
|
-
}))
|
|
529
|
+
}))
|
|
530
|
+
.getReader();
|
|
531
|
+
// The connection is given back when the body ends, fails or is cancelled.
|
|
532
|
+
return new ReadableStream({
|
|
533
|
+
async pull(controller) {
|
|
534
|
+
try {
|
|
535
|
+
const { value, done } = await reader.read();
|
|
536
|
+
if (done) {
|
|
537
|
+
release();
|
|
538
|
+
controller.close();
|
|
539
|
+
}
|
|
540
|
+
else
|
|
541
|
+
controller.enqueue(value);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
release();
|
|
545
|
+
controller.error(error);
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
async cancel(reason) {
|
|
549
|
+
release();
|
|
550
|
+
await reader.cancel(reason);
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
/** Sends a call whose answer is a stream, holding a stream slot and a
|
|
555
|
+
* connection slot from before it is sent until `release`: once its body is
|
|
556
|
+
* read, failed or cancelled, or its deadline or signal ends it, whichever
|
|
557
|
+
* comes first, so a stream nobody finishes still gives its slots back at
|
|
558
|
+
* its deadline. */
|
|
559
|
+
async #opened(call) {
|
|
560
|
+
const gate = this.#gate(call, [this.#streams, this.#slots]);
|
|
561
|
+
await gate.take();
|
|
562
|
+
let released = false;
|
|
563
|
+
const release = () => {
|
|
564
|
+
if (released)
|
|
565
|
+
return;
|
|
566
|
+
released = true;
|
|
567
|
+
call.signal?.removeEventListener("abort", release);
|
|
568
|
+
gate.lend();
|
|
569
|
+
};
|
|
570
|
+
try {
|
|
571
|
+
const response = await this.#send(call, async (response) => response, gate);
|
|
572
|
+
if (call.signal?.aborted)
|
|
573
|
+
release();
|
|
574
|
+
else
|
|
575
|
+
call.signal?.addEventListener("abort", release, { once: true });
|
|
576
|
+
return { response, release };
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
release();
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
446
582
|
}
|
|
447
583
|
/** Newline-delimited JSON events, as they arrive. */
|
|
448
584
|
async *events(call) {
|
|
449
|
-
const response = await this
|
|
585
|
+
const { response, release } = await this.#opened(this.#prepare({ ...call, accept: "application/x-ndjson" }));
|
|
450
586
|
if (!response.body)
|
|
451
|
-
return;
|
|
587
|
+
return release();
|
|
452
588
|
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
453
589
|
let buffer = "";
|
|
454
590
|
try {
|
|
@@ -477,6 +613,7 @@ export class Transport {
|
|
|
477
613
|
// Cleanup must not replace a parse, network or callback error.
|
|
478
614
|
await reader.cancel().catch(() => undefined);
|
|
479
615
|
reader.releaseLock();
|
|
616
|
+
release();
|
|
480
617
|
}
|
|
481
618
|
}
|
|
482
619
|
/** A WebSocket to an API path, authenticated with the same key. */
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type SandboxInfo = {
|
|
|
5
5
|
kind: "sandbox";
|
|
6
6
|
name: string | null;
|
|
7
7
|
labels: Record<string, string>;
|
|
8
|
+
/** @deprecated Read `state`. An older, coarser name for it, still sent for
|
|
9
|
+
* older clients. */
|
|
8
10
|
status: "pending" | "active" | "paused" | "stopping" | "stopped";
|
|
9
11
|
state: "starting" | "running" | "pausing" | "paused" | "resuming" | "stopping" | "stopped";
|
|
10
12
|
region: string;
|
|
@@ -14,7 +16,10 @@ export type SandboxInfo = {
|
|
|
14
16
|
diskMiB: number;
|
|
15
17
|
cpu: "shared" | "reserved";
|
|
16
18
|
cpuFloorMillis: number;
|
|
19
|
+
/** @deprecated Always true for a sandbox created since 5 October 2026:
|
|
20
|
+
* every sandbox can pause. */
|
|
17
21
|
pausable: boolean;
|
|
22
|
+
/** Its time limit in seconds; 0 is none: it runs while it works. */
|
|
18
23
|
timeoutSeconds: number;
|
|
19
24
|
/** Pauses after this many idle seconds; 0 is never. */
|
|
20
25
|
idlePauseSeconds?: number;
|
|
@@ -23,19 +28,34 @@ export type SandboxInfo = {
|
|
|
23
28
|
idlePauseUnusedOnly?: boolean;
|
|
24
29
|
/** A request (exec, files, terminal, a visit to a shared port) wakes it when paused. */
|
|
25
30
|
autoWake?: boolean;
|
|
26
|
-
/**
|
|
31
|
+
/** Runs until stopped while credit lasts, never paused for idleness or a
|
|
32
|
+
* time limit, and its disk is kept after a stop. */
|
|
27
33
|
persistent?: boolean;
|
|
28
34
|
/** The last exec, file, terminal, desktop or preview request, to within a minute. */
|
|
29
35
|
lastActiveAt?: string | null;
|
|
30
|
-
|
|
36
|
+
/** What happens when its time limit, credit or a spending limit ends it. */
|
|
37
|
+
onLeaseEnd: "pause" | "stop" | "delete";
|
|
38
|
+
/** onLeaseEnd's newer name (0381): the same value. */
|
|
39
|
+
onTimeout?: "pause" | "stop" | "delete";
|
|
31
40
|
createdAt: string;
|
|
32
41
|
readyAt: string | null;
|
|
42
|
+
/** Paid up to: a time ahead of now that moves on by itself while it runs.
|
|
43
|
+
* Not when it ends; that is `endsAt`. */
|
|
33
44
|
expiresAt: string;
|
|
45
|
+
/** When it stops or pauses by itself: its time limit, or where its funding
|
|
46
|
+
* ends once credit or a spending limit stops its renewal. Null when it
|
|
47
|
+
* never will, and when it is not running. Absent from older servers. */
|
|
48
|
+
endsAt?: string | null;
|
|
34
49
|
endedAt: string | null;
|
|
50
|
+
/** Why it stopped or paused, such as `requested`, `idle` or `time_limit`
|
|
51
|
+
* (its `timeoutSeconds` ran out). A server before 4 October 2026 sent
|
|
52
|
+
* `lease_expired` for `time_limit`. */
|
|
35
53
|
stopReason: string | null;
|
|
36
54
|
pausedAt: string | null;
|
|
37
55
|
pausedExpiresAt: string | null;
|
|
38
56
|
chargedMicros: number;
|
|
57
|
+
/** @deprecated Read `chargedMicros` and `runtime.usage()`. Still sent for
|
|
58
|
+
* older clients. */
|
|
39
59
|
heldMicros: number;
|
|
40
60
|
simulated?: boolean;
|
|
41
61
|
replayed?: boolean;
|
|
@@ -68,7 +88,7 @@ export type CreateSandbox = {
|
|
|
68
88
|
* `env`. At most 32 variables and 32 KiB. Values are never shown again;
|
|
69
89
|
* answers list only `envNames`. Copies made by fork() keep them. */
|
|
70
90
|
env?: Record<string, string>;
|
|
71
|
-
/** Omit to use the
|
|
91
|
+
/** Omit to use the included usage while it lasts, then prepaid credit. */
|
|
72
92
|
funding?: "trial" | "paid";
|
|
73
93
|
region?: string;
|
|
74
94
|
vcpu?: number;
|
|
@@ -76,25 +96,33 @@ export type CreateSandbox = {
|
|
|
76
96
|
diskMiB?: number;
|
|
77
97
|
cpu?: "shared" | "reserved";
|
|
78
98
|
cpuFloorMillis?: number;
|
|
79
|
-
/**
|
|
99
|
+
/** A time limit, 60 to 86400 seconds (24 hours). Leave it out (or 0) for
|
|
100
|
+
* none: the sandbox runs while it works and pauses when idle, until you
|
|
101
|
+
* stop it or credit runs out. */
|
|
80
102
|
timeoutSeconds?: number;
|
|
103
|
+
/** @deprecated Ignored since 5 October 2026: every sandbox can pause. */
|
|
81
104
|
pausable?: boolean;
|
|
82
|
-
/** What happens when
|
|
83
|
-
|
|
105
|
+
/** What happens when its time limit, credit or a spending limit ends it:
|
|
106
|
+
* "pause" (default), "stop", or "delete" (when the time limit runs out,
|
|
107
|
+
* delete it and its files; credit or a spending limit only stops it). */
|
|
108
|
+
onLeaseEnd?: "pause" | "stop" | "delete";
|
|
84
109
|
/** Pause after this many seconds in which nothing happens in it: no
|
|
85
110
|
* request, no command or terminal running, no open connection, no network
|
|
86
|
-
* traffic and no CPU use. Default 60
|
|
111
|
+
* traffic and no CPU use. Default 60 unless persistent; 0 never;
|
|
87
112
|
* otherwise 10 to 86400. A request wakes it again. */
|
|
88
113
|
idlePauseSeconds?: number;
|
|
89
114
|
/** A request to a paused sandbox wakes it. Default true. */
|
|
90
115
|
autoWake?: boolean;
|
|
91
|
-
/** Keep it running
|
|
92
|
-
*
|
|
116
|
+
/** Keep it running until you stop it, while credit lasts, with no idle
|
|
117
|
+
* pause unless idlePauseSeconds asks for one. Its disk is billed as any
|
|
118
|
+
* sandbox's, and kept after a stop, for restart(), as any sandbox's is.
|
|
119
|
+
* Paid only. */
|
|
93
120
|
persistent?: boolean;
|
|
94
121
|
/** The most it may cost over its whole life, in microdollars. */
|
|
95
122
|
maxTotalCostMicros?: number;
|
|
96
123
|
/** With name: return the sandbox that already has the name, woken if paused. */
|
|
97
124
|
getOrCreate?: boolean;
|
|
125
|
+
/** @deprecated Use `maxTotalCostMicros`. Still accepted for older clients. */
|
|
98
126
|
maxCostMicros?: number;
|
|
99
127
|
/** Network rules from the first start; the same shape as sandbox.network.set.
|
|
100
128
|
* Omit for the public web on ports 80 and 443. */
|
|
@@ -197,6 +225,7 @@ export type OutputEvent = {
|
|
|
197
225
|
error: {
|
|
198
226
|
code: string;
|
|
199
227
|
message: string;
|
|
228
|
+
status?: number;
|
|
200
229
|
requestId?: string;
|
|
201
230
|
};
|
|
202
231
|
};
|
|
@@ -227,6 +256,8 @@ export type Usage = {
|
|
|
227
256
|
unit: "microdollars";
|
|
228
257
|
credited: string;
|
|
229
258
|
spent: string;
|
|
259
|
+
/** What running sandboxes and the hour ahead of stored items are about to
|
|
260
|
+
* use, already taken out of `available`. */
|
|
230
261
|
held: string;
|
|
231
262
|
/** Credit that expired, or grant credit taken back. */
|
|
232
263
|
expired: string;
|
|
@@ -234,12 +265,41 @@ export type Usage = {
|
|
|
234
265
|
available: string;
|
|
235
266
|
/** The part of spent that refunds and disputes took. */
|
|
236
267
|
takenBack: string;
|
|
268
|
+
/** Machine time the account can still run without credit, in
|
|
269
|
+
* milliseconds, or null when it has none. `totalMs` and `usedMs` count free
|
|
270
|
+
* hours given before 5 October 2026 (0 for an account given none);
|
|
271
|
+
* `availableMs` is the larger of those hours left and this month's included
|
|
272
|
+
* usage, as time at 2 vCPU and 4 GiB. The field keeps its older name. */
|
|
237
273
|
trial: {
|
|
238
274
|
totalMs: number;
|
|
239
275
|
usedMs: number;
|
|
276
|
+
/** @deprecated Read `availableMs`. Still sent for older clients. */
|
|
240
277
|
reservedMs: number;
|
|
241
278
|
availableMs: number;
|
|
279
|
+
/** `monthly`: the included usage every month only. `hours`: free hours
|
|
280
|
+
* given before 5 October 2026, kept, beside the included usage. Absent
|
|
281
|
+
* from an API older than 5 October 2026. */
|
|
282
|
+
offer?: "monthly" | "hours";
|
|
283
|
+
/** When the included usage renews: the 1st of next month, 00:00 UTC. */
|
|
284
|
+
renewsAt?: string;
|
|
242
285
|
} | null;
|
|
286
|
+
/** What the account uses free this calendar month (UTC), one row per
|
|
287
|
+
* product, drawn before credit and never charged
|
|
288
|
+
* (https://withruntime.com/docs/included-usage). */
|
|
289
|
+
allowances?: Array<{
|
|
290
|
+
/** machine-memory, machine-cpu, saved-copies or disks. */
|
|
291
|
+
pool: string;
|
|
292
|
+
/** GiB-hour, vCPU-hour or GB-month. */
|
|
293
|
+
unit: string;
|
|
294
|
+
quantity: number;
|
|
295
|
+
used: number;
|
|
296
|
+
/** Held for what is running now; given back unless used. */
|
|
297
|
+
reserved: number;
|
|
298
|
+
left: number;
|
|
299
|
+
/** The month's first day, YYYY-MM-DD. */
|
|
300
|
+
month: string;
|
|
301
|
+
renewsAt: string;
|
|
302
|
+
}>;
|
|
243
303
|
/** Outbound traffic this calendar month, UTC: the first `allowanceBytes` an
|
|
244
304
|
* account sends are free, and the rest is charged at the rate in the pricing
|
|
245
305
|
* guide (https://withruntime.com/docs/pricing#network-products). */
|
|
@@ -253,6 +313,35 @@ export type Usage = {
|
|
|
253
313
|
/** What the balance or a spending limit could not cover; never charged. */
|
|
254
314
|
writtenOffMicros: string;
|
|
255
315
|
} | null;
|
|
316
|
+
/** The account's pilot while it serves its sandboxes, a free run agreed
|
|
317
|
+
* with Runtime: `hours` sandbox-hours of running time, `usedMs` and
|
|
318
|
+
* `leftMs` of it used and left, and `running`, its sandboxes running now:
|
|
319
|
+
* `leftMs / running` is how long the hours last at this rate. Null on every
|
|
320
|
+
* other account. */
|
|
321
|
+
pilot?: {
|
|
322
|
+
id: string;
|
|
323
|
+
sandboxes: number;
|
|
324
|
+
vcpu: number;
|
|
325
|
+
memoryMiB: number;
|
|
326
|
+
diskMiB: number;
|
|
327
|
+
startsAt: string;
|
|
328
|
+
endsAt: string;
|
|
329
|
+
graceEndsAt: string;
|
|
330
|
+
endedAt: string | null;
|
|
331
|
+
hours: number;
|
|
332
|
+
usedMs: number;
|
|
333
|
+
leftMs: number;
|
|
334
|
+
running?: number;
|
|
335
|
+
} | null;
|
|
336
|
+
/** Everything this key acts for, a row a kind, past the hundred newest
|
|
337
|
+
* `resources` lists: how many, and what was charged and is held, in
|
|
338
|
+
* integer microdollars as strings. Absent from an API before 6 October 2026. */
|
|
339
|
+
kinds?: Array<{
|
|
340
|
+
kind: string;
|
|
341
|
+
resources: number;
|
|
342
|
+
chargedMicros: string;
|
|
343
|
+
heldMicros: string;
|
|
344
|
+
}>;
|
|
256
345
|
resources: Array<Record<string, unknown>>;
|
|
257
346
|
[key: string]: unknown;
|
|
258
347
|
};
|
|
@@ -269,7 +358,8 @@ export type SandboxSettings = {
|
|
|
269
358
|
/** Pause after this many seconds with no activity, counted from now; 0
|
|
270
359
|
* never, otherwise 10 to 86400. */
|
|
271
360
|
idlePauseSeconds?: number;
|
|
272
|
-
/** Keep it running while credit lasts and keep its disk
|
|
361
|
+
/** Keep it running until you stop it, while credit lasts, and keep its disk
|
|
362
|
+
* after a stop. Paid only. */
|
|
273
363
|
persistent?: boolean;
|
|
274
364
|
/** Lifetime cap in microdollars; null removes it. */
|
|
275
365
|
maxTotalCostMicros?: number | null;
|
|
@@ -288,11 +378,11 @@ export type DeletedSandbox = {
|
|
|
288
378
|
endedAt: string | null;
|
|
289
379
|
replayed?: boolean;
|
|
290
380
|
};
|
|
291
|
-
/** How `sandbox.keepAlive()`
|
|
381
|
+
/** How `sandbox.keepAlive()` moves on the time limit of a sandbox that has one. */
|
|
292
382
|
export type KeepAliveOptions = {
|
|
293
383
|
/** How often it checks, in seconds. Default 60. */
|
|
294
384
|
everySeconds?: number;
|
|
295
|
-
/** How much
|
|
385
|
+
/** How much of its time limit it keeps ahead of now, in seconds (60 to 3600). Default 600. */
|
|
296
386
|
marginSeconds?: number;
|
|
297
387
|
/** Called with an error an extension met; the loop carries on. */
|
|
298
388
|
onError?: (error: unknown) => void;
|