withruntime 0.9.0 → 0.11.0
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 +4 -4
- package/DAYTONA.md +52 -40
- package/E2B.md +65 -62
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +34 -22
- package/compatibility-lock.json +16 -16
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +262 -56
- package/dist/client.d.ts +41 -3
- package/dist/client.js +67 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/computer-use.d.ts +162 -0
- package/dist/daytona/computer-use.js +324 -0
- package/dist/daytona/context.d.ts +10 -0
- package/dist/daytona/context.js +24 -0
- package/dist/daytona/daytona.d.ts +20 -0
- package/dist/daytona/daytona.js +134 -20
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +129 -38
- package/dist/daytona/index.d.ts +3 -27
- package/dist/daytona/index.js +2 -7
- package/dist/daytona/process.d.ts +63 -6
- package/dist/daytona/process.js +224 -23
- package/dist/daytona/sandbox.d.ts +20 -6
- package/dist/daytona/sandbox.js +55 -32
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +6 -2
- package/dist/e2b/commands.d.ts +43 -12
- package/dist/e2b/commands.js +217 -64
- package/dist/e2b/errors.d.ts +7 -0
- package/dist/e2b/errors.js +20 -0
- package/dist/e2b/filesystem.d.ts +2 -1
- package/dist/e2b/filesystem.js +43 -6
- package/dist/e2b/index.d.ts +3 -2
- package/dist/e2b/index.js +2 -1
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +83 -0
- package/dist/e2b/sandbox.d.ts +17 -6
- package/dist/e2b/sandbox.js +239 -43
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +139 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/browser.d.ts +60 -0
- package/dist/products/browser.js +37 -0
- package/dist/products/desktop.d.ts +2 -2
- package/dist/products/desktop.js +2 -2
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +59 -8
- package/dist/sandbox.js +479 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/types.d.ts +5 -2
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/index.d.ts +1 -7
- package/dist/vercel/index.js +1 -3
- package/dist/vercel/sandbox-user.d.ts +70 -0
- package/dist/vercel/sandbox-user.js +206 -0
- package/dist/vercel/sandbox.d.ts +55 -20
- package/dist/vercel/sandbox.js +353 -99
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +2 -1
package/dist/vercel/sandbox.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { extensionSeconds, LeaseKeeper, LEASE_MAX_SECONDS } from "../compat/lease.js";
|
|
1
2
|
import { RuntimeError } from "../errors.js";
|
|
2
3
|
import { clientFor, signalOf, } from "./client.js";
|
|
3
4
|
import { Command, CommandFinished, exitCodeOf } from "./command.js";
|
|
4
5
|
import { APIError, guard, NotSupportedError, translate } from "./errors.js";
|
|
5
6
|
import { FileSystem } from "./filesystem.js";
|
|
7
|
+
import { rootOrThrow, SandboxUser, SandboxUserAlreadyExistsError, validateName, } from "./sandbox-user.js";
|
|
6
8
|
import { Snapshot } from "./snapshot.js";
|
|
7
9
|
/* Vercel Sandbox's `Sandbox` over Runtime's. Every call is one or a few calls
|
|
8
10
|
of the withruntime SDK; nothing here speaks HTTP. What Runtime cannot do the
|
|
@@ -14,7 +16,8 @@ export const MEMORY_MIB_PER_VCPU = 2048;
|
|
|
14
16
|
/** Vercel's default timeout, 5 minutes. */
|
|
15
17
|
export const DEFAULT_TIMEOUT_MS = 300_000;
|
|
16
18
|
const MIN_LEASE_SECONDS = 60;
|
|
17
|
-
|
|
19
|
+
/** Extend a long timeout's lease when less than this is left. */
|
|
20
|
+
const LEASE_MARGIN_MS = 600_000;
|
|
18
21
|
/** Commands without a timeout run until the sandbox ends: Runtime's longest. */
|
|
19
22
|
const LONGEST_MS = 86_400_000;
|
|
20
23
|
/** Vercel's working directory; `/workspace` on Runtime. */
|
|
@@ -27,15 +30,20 @@ const LEGACY_RUNTIMES = new Set(["node22", "node24", "node26", "python3.13"]);
|
|
|
27
30
|
/** Vercel regions in the United States, where Runtime runs. */
|
|
28
31
|
const US_REGIONS = new Set(["iad1", "sfo1", "cle1", "pdx1"]);
|
|
29
32
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
-
function
|
|
33
|
+
function checkTimeout(timeoutMs) {
|
|
31
34
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
32
35
|
throw new RangeError(`timeout must be a positive number of milliseconds, not ${timeoutMs}.`);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
return Math.max(timeoutMs, MIN_LEASE_SECONDS * 1000);
|
|
37
|
+
}
|
|
38
|
+
/** The first lease for a timeout: all of it, or the hour a lease may run. */
|
|
39
|
+
function leaseSeconds(timeoutMs) {
|
|
40
|
+
return Math.min(Math.ceil(checkTimeout(timeoutMs) / 1000), LEASE_MAX_SECONDS);
|
|
37
41
|
}
|
|
38
42
|
function refuseCreate(params) {
|
|
43
|
+
if (params.keepLastSnapshots !== undefined)
|
|
44
|
+
throw new NotSupportedError("Snapshot count and eviction policies (keepLastSnapshots)", "Omit it; manage saved snapshots explicitly with Snapshot.list and snapshot.delete.");
|
|
45
|
+
if (params.ports !== undefined)
|
|
46
|
+
validatePorts(params.ports);
|
|
39
47
|
if (params.mounts && Object.keys(params.mounts).length)
|
|
40
48
|
throw new NotSupportedError("Vercel Drives (mounts)", "Use a Runtime volume: withruntime: { create: { volumes: [{ volumeId, path }] } } (runtime.volumes creates one).");
|
|
41
49
|
if (params.networkId !== undefined)
|
|
@@ -45,6 +53,11 @@ function refuseCreate(params) {
|
|
|
45
53
|
if (params.failoverRegions?.length)
|
|
46
54
|
throw new NotSupportedError("Failover regions (failoverRegions)", "Remove it: Runtime runs in one US region.");
|
|
47
55
|
}
|
|
56
|
+
function validatePorts(ports) {
|
|
57
|
+
if (!Array.isArray(ports) ||
|
|
58
|
+
ports.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65_535))
|
|
59
|
+
throw new RangeError("ports must contain only integers from 1 to 65535.");
|
|
60
|
+
}
|
|
48
61
|
/** Vercel's network policy as Runtime's rules. Header injection and
|
|
49
62
|
* forwarding rules have no Runtime counterpart and are refused. */
|
|
50
63
|
export function networkRules(policy) {
|
|
@@ -131,6 +144,12 @@ export class Sandbox {
|
|
|
131
144
|
#persistent;
|
|
132
145
|
#routes = new Map();
|
|
133
146
|
#onResume;
|
|
147
|
+
#keeper;
|
|
148
|
+
/** Epoch ms the current session started, as the timeout counts from it. */
|
|
149
|
+
#sessionStart;
|
|
150
|
+
/** Vercel's timeout, when this object was given one or extended it. */
|
|
151
|
+
#timeoutMs;
|
|
152
|
+
#defaultUser;
|
|
134
153
|
#linked;
|
|
135
154
|
#deleted = false;
|
|
136
155
|
/** Use Sandbox.create, Sandbox.get or Sandbox.getOrCreate. */
|
|
@@ -140,6 +159,17 @@ export class Sandbox {
|
|
|
140
159
|
this.#env = { ...resolved.params.env };
|
|
141
160
|
this.#persistent = resolved.params.persistent ?? resolved.runtime.info.onLeaseEnd !== "stop";
|
|
142
161
|
this.#onResume = resolved.params.onResume;
|
|
162
|
+
const timeout = resolved.params.timeout;
|
|
163
|
+
this.#timeoutMs = timeout === undefined ? undefined : checkTimeout(timeout);
|
|
164
|
+
this.#sessionStart =
|
|
165
|
+
timeout === undefined
|
|
166
|
+
? Date.parse(this.#rt.info.expiresAt) - this.#rt.info.timeoutSeconds * 1000
|
|
167
|
+
: Date.now();
|
|
168
|
+
this.#keeper = new LeaseKeeper({
|
|
169
|
+
sandbox: () => this.#rt,
|
|
170
|
+
until: () => this.#until(),
|
|
171
|
+
marginMs: () => Math.min(LEASE_MARGIN_MS, this.timeout / 2),
|
|
172
|
+
});
|
|
143
173
|
this.fs = new FileSystem({
|
|
144
174
|
name: this.name,
|
|
145
175
|
files: async () => (await this.#live()).files,
|
|
@@ -204,9 +234,14 @@ export class Sandbox {
|
|
|
204
234
|
get status() {
|
|
205
235
|
return statusOf(this.#rt.state);
|
|
206
236
|
}
|
|
207
|
-
/** Milliseconds
|
|
237
|
+
/** Milliseconds from the start of the session: the timeout given at
|
|
238
|
+
* create, with every extension, or the lease of a sandbox found by name. */
|
|
208
239
|
get timeout() {
|
|
209
|
-
return this.#rt.info.timeoutSeconds * 1000;
|
|
240
|
+
return this.#timeoutMs ?? this.#rt.info.timeoutSeconds * 1000;
|
|
241
|
+
}
|
|
242
|
+
/** Epoch ms the session should end: its start plus the timeout. */
|
|
243
|
+
#until() {
|
|
244
|
+
return this.#sessionStart + this.timeout;
|
|
210
245
|
}
|
|
211
246
|
get expiresAt() {
|
|
212
247
|
return new Date(this.#rt.info.expiresAt);
|
|
@@ -227,6 +262,8 @@ export class Sandbox {
|
|
|
227
262
|
refuseCreate(params);
|
|
228
263
|
const client = clientFor(params);
|
|
229
264
|
const persistent = params.persistent ?? true;
|
|
265
|
+
if (params.snapshotExpiration !== undefined && persistent)
|
|
266
|
+
retentionDays(params.snapshotExpiration);
|
|
230
267
|
const vcpus = params.resources?.vcpus ?? DEFAULT_VCPUS;
|
|
231
268
|
const source = await guard(() => resolveImage(client, params));
|
|
232
269
|
const input = {
|
|
@@ -240,11 +277,18 @@ export class Sandbox {
|
|
|
240
277
|
...params.withruntime?.create,
|
|
241
278
|
};
|
|
242
279
|
const runtime = await guard(() => client.sandboxes.create(input, signalOf(params)));
|
|
243
|
-
const sandbox = new Sandbox({
|
|
280
|
+
const sandbox = new Sandbox({
|
|
281
|
+
runtime,
|
|
282
|
+
client,
|
|
283
|
+
params: { ...params, persistent, timeout: params.timeout ?? DEFAULT_TIMEOUT_MS },
|
|
284
|
+
});
|
|
244
285
|
try {
|
|
245
286
|
await sandbox.#setUp(params);
|
|
287
|
+
// Starts renewing at once when the timeout runs past the first lease.
|
|
288
|
+
await sandbox.#keeper.check();
|
|
246
289
|
}
|
|
247
290
|
catch (error) {
|
|
291
|
+
sandbox.#keeper.end();
|
|
248
292
|
await runtime.stop({ wait: false }).catch(() => undefined);
|
|
249
293
|
throw translate(error, sandbox.name);
|
|
250
294
|
}
|
|
@@ -300,15 +344,16 @@ export class Sandbox {
|
|
|
300
344
|
/** A sandbox by name (or Runtime id), woken if it is paused unless
|
|
301
345
|
* `resume: false`. */
|
|
302
346
|
static async get(params) {
|
|
347
|
+
params.signal?.throwIfAborted();
|
|
303
348
|
const client = clientFor(params);
|
|
304
|
-
const runtime = await guard(() => find(client, params.name), params.name);
|
|
349
|
+
const runtime = await guard(() => find(client, params.name, params), params.name);
|
|
305
350
|
const sandbox = new Sandbox({
|
|
306
351
|
runtime,
|
|
307
352
|
client,
|
|
308
353
|
params: params.onResume ? { onResume: params.onResume } : {},
|
|
309
354
|
});
|
|
310
355
|
if (params.resume !== false)
|
|
311
|
-
await sandbox.#live();
|
|
356
|
+
await sandbox.#live(params);
|
|
312
357
|
return sandbox;
|
|
313
358
|
}
|
|
314
359
|
/** The named sandbox, or a new one with that name when there is none. */
|
|
@@ -329,9 +374,22 @@ export class Sandbox {
|
|
|
329
374
|
/** A new sandbox copied from a running one, memory and all (a Runtime
|
|
330
375
|
* fork). `sourceSandbox` is its name. */
|
|
331
376
|
static async fork(params) {
|
|
332
|
-
const unsupported = [
|
|
377
|
+
const unsupported = [
|
|
378
|
+
"resources",
|
|
379
|
+
"ports",
|
|
380
|
+
"timeout",
|
|
381
|
+
"networkPolicy",
|
|
382
|
+
"image",
|
|
383
|
+
"tags",
|
|
384
|
+
"env",
|
|
385
|
+
"persistent",
|
|
386
|
+
"snapshotExpiration",
|
|
387
|
+
"keepLastSnapshots",
|
|
388
|
+
"onResume",
|
|
389
|
+
"runtime",
|
|
390
|
+
].find((field) => params[field] !== undefined);
|
|
333
391
|
if (unsupported)
|
|
334
|
-
throw new NotSupportedError(`Overriding ${unsupported} on a fork`, "
|
|
392
|
+
throw new NotSupportedError(`Overriding ${unsupported} on a fork`, "Omit the override to use Runtime's existing fork behavior.");
|
|
335
393
|
refuseCreate(params);
|
|
336
394
|
const source = await Sandbox.get({ ...params, name: params.sourceSandbox });
|
|
337
395
|
const copy = await guard(() => source.#rt.fork({ ...(params.name ? { name: params.name } : {}), ...signalOf(params) }), params.sourceSandbox);
|
|
@@ -342,22 +400,27 @@ export class Sandbox {
|
|
|
342
400
|
});
|
|
343
401
|
}
|
|
344
402
|
/** Running and stopped (paused) sandboxes, oldest first, with Vercel's
|
|
345
|
-
* paginator: `for await (const s of await Sandbox.list())`.
|
|
403
|
+
* paginator: `for await (const s of await Sandbox.list())`. A name prefix,
|
|
404
|
+
* creation-time bounds (`since`, `until`, epoch ms), `sortBy` and
|
|
405
|
+
* `sortOrder`, and a `cursor` from an earlier page are applied here, over
|
|
406
|
+
* every sandbox the key sees. */
|
|
346
407
|
static async list(params = {}) {
|
|
347
|
-
|
|
348
|
-
if (params[field] !== undefined)
|
|
349
|
-
throw new NotSupportedError(`Listing sandboxes by ${field}`, "List them all (oldest first) and filter the result yourself.");
|
|
408
|
+
params.signal?.throwIfAborted();
|
|
350
409
|
const client = clientFor(params);
|
|
410
|
+
const local = ["namePrefix", "sortBy", "sortOrder", "cursor", "since", "until"].some((field) => params[field] !== undefined);
|
|
411
|
+
if (local)
|
|
412
|
+
return listHere(client, params);
|
|
351
413
|
const page = await guard(() => client.sandboxes.list({
|
|
352
414
|
...(params.tags ? { labels: params.tags } : {}),
|
|
353
415
|
...(params.limit ? { limit: Math.min(params.limit, 100) } : {}),
|
|
354
|
-
}));
|
|
355
|
-
return paginate(page);
|
|
416
|
+
}, signalOf(params)));
|
|
417
|
+
return paginate(page, params.signal);
|
|
356
418
|
}
|
|
357
419
|
// ---- commands ---------------------------------------------------------
|
|
358
420
|
/** Wakes the sandbox if it was paused (a stopped persistent sandbox, in
|
|
359
421
|
* Vercel's words), as Vercel resumes one on the next call. */
|
|
360
|
-
async #live() {
|
|
422
|
+
async #live(opts = {}) {
|
|
423
|
+
opts.signal?.throwIfAborted();
|
|
361
424
|
if (this.#deleted)
|
|
362
425
|
throw new APIError(new Response(null, { status: 410 }), {
|
|
363
426
|
message: `Sandbox ${this.name} was deleted.`,
|
|
@@ -365,35 +428,48 @@ export class Sandbox {
|
|
|
365
428
|
});
|
|
366
429
|
const state = this.#rt.state;
|
|
367
430
|
if (state === "paused" || state === "pausing")
|
|
368
|
-
await this.#wake();
|
|
431
|
+
await this.#wake(opts);
|
|
432
|
+
await guard(() => this.#keeper.check(), this.name);
|
|
369
433
|
return this.#rt;
|
|
370
434
|
}
|
|
371
|
-
|
|
372
|
-
|
|
435
|
+
/** A woken sandbox starts a new session, whose timeout counts from now,
|
|
436
|
+
* as Vercel's resumed sessions do. */
|
|
437
|
+
async #wake(opts = {}) {
|
|
438
|
+
await guard(() => this.#rt.wake(signalOf(opts)), this.name);
|
|
439
|
+
if (this.#timeoutMs !== undefined)
|
|
440
|
+
this.#sessionStart = Date.now();
|
|
373
441
|
await this.#onResume?.(this);
|
|
374
442
|
}
|
|
375
443
|
/** Runs `work` on the live sandbox; if the lease paused it meanwhile, wakes
|
|
376
444
|
* it and runs `work` once more. */
|
|
377
|
-
async #withResume(work) {
|
|
378
|
-
|
|
445
|
+
async #withResume(work, opts = {}) {
|
|
446
|
+
opts.signal?.throwIfAborted();
|
|
447
|
+
const runtime = await this.#live(opts);
|
|
379
448
|
try {
|
|
380
449
|
return await work(runtime);
|
|
381
450
|
}
|
|
382
451
|
catch (error) {
|
|
383
452
|
if (!(error instanceof RuntimeError && error.code === "sandbox_paused"))
|
|
384
453
|
throw translate(error, this.name);
|
|
385
|
-
await guard(() => runtime.refresh(), this.name);
|
|
386
|
-
await this.#wake();
|
|
454
|
+
await guard(() => runtime.refresh(signalOf(opts)), this.name);
|
|
455
|
+
await this.#wake(opts);
|
|
387
456
|
return guard(() => work(this.#rt), this.name);
|
|
388
457
|
}
|
|
389
458
|
}
|
|
390
|
-
async #linkHome(text) {
|
|
459
|
+
async #linkHome(text, opts = {}) {
|
|
460
|
+
opts.signal?.throwIfAborted();
|
|
391
461
|
if (this.#linked || !text.includes(HOME))
|
|
392
462
|
return this.#linked;
|
|
393
463
|
this.#linked = (async () => {
|
|
394
|
-
await this.#rt.exec(HOME_LINK).catch(() =>
|
|
464
|
+
await this.#rt.exec(HOME_LINK, signalOf(opts)).catch(() => opts.signal?.throwIfAborted());
|
|
395
465
|
})();
|
|
396
|
-
|
|
466
|
+
try {
|
|
467
|
+
await this.#linked;
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
this.#linked = undefined;
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
397
473
|
}
|
|
398
474
|
async runCommand(commandOrParams, args, opts = {}) {
|
|
399
475
|
const params = typeof commandOrParams === "string"
|
|
@@ -406,18 +482,19 @@ export class Sandbox {
|
|
|
406
482
|
];
|
|
407
483
|
const cwd = params.cwd === undefined ? RUNTIME_HOME : toRuntimePath(params.cwd);
|
|
408
484
|
const env = { ...this.#env, ...params.env };
|
|
409
|
-
await this.#live();
|
|
410
|
-
await this.#linkHome(`${argv.join(" ")}\n${params.cwd ?? ""}\n${Object.values(env).join("\n")}
|
|
485
|
+
await this.#live(params);
|
|
486
|
+
await this.#linkHome(`${argv.join(" ")}\n${params.cwd ?? ""}\n${Object.values(env).join("\n")}`, params);
|
|
411
487
|
const timeoutMs = params.timeoutMs ?? LONGEST_MS;
|
|
412
488
|
const common = {
|
|
413
489
|
cwd,
|
|
414
490
|
...(Object.keys(env).length ? { env } : {}),
|
|
415
491
|
timeoutMs,
|
|
492
|
+
...signalOf(params),
|
|
416
493
|
};
|
|
417
494
|
const startedAt = Date.now();
|
|
418
495
|
const vercelCwd = params.cwd === undefined ? HOME : toVercelPath(params.cwd);
|
|
419
496
|
if (params.detached) {
|
|
420
|
-
const process = await this.#withResume((runtime) => runtime.spawn(argv, common));
|
|
497
|
+
const process = await this.#withResume((runtime) => runtime.spawn(argv, common), params);
|
|
421
498
|
const command = new Command({
|
|
422
499
|
id: process.id,
|
|
423
500
|
cwd: vercelCwd,
|
|
@@ -438,8 +515,7 @@ export class Sandbox {
|
|
|
438
515
|
...common,
|
|
439
516
|
onStdout: record("stdout", params.stdout),
|
|
440
517
|
onStderr: record("stderr", params.stderr),
|
|
441
|
-
|
|
442
|
-
}));
|
|
518
|
+
}), params);
|
|
443
519
|
return new CommandFinished({
|
|
444
520
|
id: result.processId ?? `cmd_${startedAt}`,
|
|
445
521
|
cwd: vercelCwd,
|
|
@@ -449,8 +525,8 @@ export class Sandbox {
|
|
|
449
525
|
}, exitCodeOf(result.exitCode, result.timedOut), result.durationMs ?? Date.now() - startedAt);
|
|
450
526
|
}
|
|
451
527
|
/** A command started earlier, by its cmdId. */
|
|
452
|
-
async getCommand(cmdId,
|
|
453
|
-
const process = await this.#withResume((runtime) => runtime.processes.get(cmdId));
|
|
528
|
+
async getCommand(cmdId, opts = {}) {
|
|
529
|
+
const process = await this.#withResume((runtime) => runtime.processes.get(cmdId, signalOf(opts)), opts);
|
|
454
530
|
return new Command({
|
|
455
531
|
id: process.id,
|
|
456
532
|
cwd: process.info.cwd,
|
|
@@ -460,9 +536,9 @@ export class Sandbox {
|
|
|
460
536
|
});
|
|
461
537
|
}
|
|
462
538
|
// ---- files ------------------------------------------------------------
|
|
463
|
-
async mkDir(path,
|
|
539
|
+
async mkDir(path, opts = {}) {
|
|
464
540
|
const target = toRuntimePath(path);
|
|
465
|
-
await this.#withResume((runtime) => runtime.files.mkdir(target, { parents: true }));
|
|
541
|
+
await this.#withResume((runtime) => runtime.files.mkdir(target, { parents: true, ...signalOf(opts) }), opts);
|
|
466
542
|
}
|
|
467
543
|
/** The file's bytes as a Node stream, or null when there is no file. */
|
|
468
544
|
async readFile(file, opts = {}) {
|
|
@@ -476,7 +552,7 @@ export class Sandbox {
|
|
|
476
552
|
async readFileToBuffer(file, opts = {}) {
|
|
477
553
|
const target = toRuntimePath(file.path, file.cwd);
|
|
478
554
|
try {
|
|
479
|
-
return Buffer.from(await this.#withResume((runtime) => runtime.files.read(target, signalOf(opts))));
|
|
555
|
+
return Buffer.from(await this.#withResume((runtime) => runtime.files.read(target, signalOf(opts)), opts));
|
|
480
556
|
}
|
|
481
557
|
catch (error) {
|
|
482
558
|
if (error instanceof APIError && error.response.status === 404)
|
|
@@ -489,39 +565,42 @@ export class Sandbox {
|
|
|
489
565
|
const buffer = await this.readFileToBuffer(src, opts);
|
|
490
566
|
if (buffer === null)
|
|
491
567
|
return null;
|
|
568
|
+
opts.signal?.throwIfAborted();
|
|
492
569
|
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
493
570
|
const paths = await import("node:path");
|
|
494
571
|
const target = paths.resolve(dst.cwd ?? process.cwd(), dst.path);
|
|
572
|
+
opts.signal?.throwIfAborted();
|
|
495
573
|
if (opts.mkdirRecursive)
|
|
496
574
|
await mkdir(paths.dirname(target), { recursive: true });
|
|
497
|
-
await writeFile(target, buffer);
|
|
575
|
+
await writeFile(target, buffer, signalOf(opts));
|
|
498
576
|
return target;
|
|
499
577
|
}
|
|
500
578
|
/** Writes files, making their directories. Relative paths land in the
|
|
501
579
|
* working directory; `mode` sets permissions. */
|
|
502
|
-
async writeFiles(files,
|
|
580
|
+
async writeFiles(files, opts = {}) {
|
|
503
581
|
const modes = [];
|
|
504
582
|
await this.#withResume(async (runtime) => {
|
|
505
583
|
for (const file of files) {
|
|
506
584
|
const target = toRuntimePath(file.path);
|
|
507
|
-
await runtime.files.write(target, file.content);
|
|
585
|
+
await runtime.files.write(target, file.content, signalOf(opts));
|
|
508
586
|
if (file.mode !== undefined)
|
|
509
587
|
modes.push(file.mode.toString(8), target);
|
|
510
588
|
}
|
|
511
589
|
if (!modes.length)
|
|
512
590
|
return;
|
|
513
591
|
const script = 'while [ "$#" -gt 0 ]; do chmod "$1" "$2" || exit 1; shift 2; done';
|
|
514
|
-
const result = await runtime.exec(["sh", "-c", script, "sh", ...modes]);
|
|
592
|
+
const result = await runtime.exec(["sh", "-c", script, "sh", ...modes], signalOf(opts));
|
|
515
593
|
if (result.exitCode !== 0)
|
|
516
594
|
throw new APIError(new Response(null, { status: 400 }), {
|
|
517
595
|
message: `Setting file modes failed: ${result.stderr.trim()}`,
|
|
518
596
|
sandboxName: this.name,
|
|
519
597
|
});
|
|
520
|
-
});
|
|
598
|
+
}, opts);
|
|
521
599
|
}
|
|
522
600
|
// ---- ports ------------------------------------------------------------
|
|
523
|
-
async #share(port) {
|
|
524
|
-
|
|
601
|
+
async #share(port, opts = {}) {
|
|
602
|
+
opts.signal?.throwIfAborted();
|
|
603
|
+
const preview = await guard(() => this.#rt.previews.create(port, { visibility: "public" }, signalOf(opts)), this.name);
|
|
525
604
|
this.#routes.set(port, preview.url.replace(/\/$/, ""));
|
|
526
605
|
}
|
|
527
606
|
/** The public address of a port listed in `ports`, such as
|
|
@@ -545,6 +624,8 @@ export class Sandbox {
|
|
|
545
624
|
else
|
|
546
625
|
await guard(() => this.#rt.stop(signalOf(opts)), this.name);
|
|
547
626
|
}
|
|
627
|
+
if (!this.#persistent)
|
|
628
|
+
this.#keeper.end();
|
|
548
629
|
return { name: this.name, status: this.status };
|
|
549
630
|
}
|
|
550
631
|
/** Ends the sandbox for good. */
|
|
@@ -555,87 +636,191 @@ export class Sandbox {
|
|
|
555
636
|
if (this.#rt.state !== "stopped")
|
|
556
637
|
await guard(() => this.#rt.stop({ wait: false, ...signalOf(opts) }), this.name);
|
|
557
638
|
this.#deleted = true;
|
|
639
|
+
this.#keeper.end();
|
|
558
640
|
}
|
|
559
641
|
async [Symbol.asyncDispose]() {
|
|
560
642
|
if (this.#deleted)
|
|
561
643
|
return;
|
|
562
644
|
await this.stop().catch(() => undefined);
|
|
563
645
|
}
|
|
564
|
-
/** Moves the end `duration` milliseconds later
|
|
565
|
-
*
|
|
646
|
+
/** Moves the end `duration` milliseconds later. The lease moves at once
|
|
647
|
+
* as far as it may (an hour ahead of now); past that it is extended while
|
|
648
|
+
* this object lives. */
|
|
566
649
|
async extendTimeout(duration, opts = {}) {
|
|
567
650
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
568
651
|
throw new RangeError(`duration must be a positive number of milliseconds, not ${duration}.`);
|
|
569
|
-
await this.#withResume((runtime) =>
|
|
652
|
+
await this.#withResume(async (runtime) => {
|
|
653
|
+
this.#timeoutMs = this.timeout + duration;
|
|
654
|
+
await this.#leaseToTimeout(runtime, opts);
|
|
655
|
+
}, opts);
|
|
656
|
+
}
|
|
657
|
+
/** Extends the lease now toward the timeout, an hour ahead at most. */
|
|
658
|
+
async #leaseToTimeout(runtime, opts) {
|
|
659
|
+
const seconds = extensionSeconds(Date.parse(runtime.info.expiresAt), this.#until(), Date.now(), Infinity);
|
|
660
|
+
if (seconds >= 1)
|
|
661
|
+
await runtime.extend(seconds, signalOf(opts));
|
|
570
662
|
}
|
|
571
663
|
/** Replaces the network rules; returns the policy given. */
|
|
572
|
-
async updateNetworkPolicy(networkPolicy,
|
|
664
|
+
async updateNetworkPolicy(networkPolicy, opts = {}) {
|
|
665
|
+
opts.signal?.throwIfAborted();
|
|
573
666
|
const rules = networkRules(networkPolicy);
|
|
574
|
-
await this.#withResume((runtime) => runtime.network.set(rules));
|
|
667
|
+
await this.#withResume((runtime) => runtime.network.set(rules, signalOf(opts)), opts);
|
|
575
668
|
return networkPolicy;
|
|
576
669
|
}
|
|
577
670
|
/** Changes what Runtime can change on a sandbox: `timeout`, `ports`,
|
|
578
671
|
* `networkPolicy` and `snapshotExpiration`. Anything else is refused before
|
|
579
672
|
* anything changes. */
|
|
580
|
-
async update(params,
|
|
673
|
+
async update(params, opts = {}) {
|
|
674
|
+
opts.signal?.throwIfAborted();
|
|
581
675
|
const allowed = new Set(["timeout", "ports", "networkPolicy", "snapshotExpiration"]);
|
|
582
676
|
const other = Object.keys(params).find((key) => params[key] !== undefined && !allowed.has(key));
|
|
583
677
|
if (other)
|
|
584
678
|
throw new NotSupportedError(`Changing ${other} on an existing sandbox`, "Create a new sandbox with it (Runtime cannot change a sandbox's machine, persistence, tags or region after creation).");
|
|
679
|
+
const rules = params.networkPolicy === undefined ? undefined : networkRules(params.networkPolicy);
|
|
680
|
+
const retention = params.snapshotExpiration === undefined
|
|
681
|
+
? undefined
|
|
682
|
+
: retentionDays(params.snapshotExpiration);
|
|
683
|
+
if (params.ports !== undefined)
|
|
684
|
+
validatePorts(params.ports);
|
|
685
|
+
const wanted = params.ports === undefined ? undefined : new Set(params.ports);
|
|
585
686
|
if (params.timeout !== undefined) {
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
687
|
+
const timeout = checkTimeout(params.timeout);
|
|
688
|
+
// A shorter timeout is kept when the lease has not reached it yet: the
|
|
689
|
+
// lease then stops being extended there. Runtime cannot end a lease
|
|
690
|
+
// earlier than it was given.
|
|
691
|
+
if (this.#sessionStart + timeout < Date.parse(this.#rt.info.expiresAt) - 1000)
|
|
692
|
+
throw new NotSupportedError("A timeout that ends earlier than the current lease", `The lease runs to ${this.#rt.info.expiresAt}; call stop() when the work is done, or pass a timeout that ends after it.`);
|
|
693
|
+
const longer = timeout > this.timeout;
|
|
694
|
+
this.#timeoutMs = timeout;
|
|
695
|
+
if (longer)
|
|
696
|
+
await this.#withResume((runtime) => this.#leaseToTimeout(runtime, opts), opts);
|
|
591
697
|
}
|
|
592
|
-
if (
|
|
593
|
-
await this.
|
|
594
|
-
if (
|
|
595
|
-
await guard(() => this.#rt.setRetention(
|
|
596
|
-
if (
|
|
597
|
-
const wanted = new Set(params.ports);
|
|
698
|
+
if (rules !== undefined)
|
|
699
|
+
await this.#withResume((runtime) => runtime.network.set(rules, signalOf(opts)), opts);
|
|
700
|
+
if (retention !== undefined)
|
|
701
|
+
await guard(() => this.#rt.setRetention(retention, signalOf(opts)), this.name);
|
|
702
|
+
if (wanted !== undefined) {
|
|
598
703
|
for (const port of [...this.#routes.keys()])
|
|
599
704
|
if (!wanted.has(port)) {
|
|
600
|
-
await guard(() => this.#rt.previews.delete(port), this.name);
|
|
705
|
+
await guard(() => this.#rt.previews.delete(port, signalOf(opts)), this.name);
|
|
601
706
|
this.#routes.delete(port);
|
|
602
707
|
}
|
|
603
708
|
for (const port of wanted)
|
|
604
709
|
if (!this.#routes.has(port))
|
|
605
|
-
await this.#share(port);
|
|
710
|
+
await this.#share(port, opts);
|
|
606
711
|
}
|
|
607
712
|
}
|
|
608
|
-
/** Keeps the sandbox's
|
|
609
|
-
*
|
|
713
|
+
/** Keeps the sandbox's filesystem as a disk-only snapshot, then stops
|
|
714
|
+
* the source. Restoring it starts fresh processes, as Vercel does. Start
|
|
610
715
|
* from it with `Sandbox.create({ source: { type: "snapshot", snapshotId } })`. */
|
|
611
716
|
async snapshot(opts = {}) {
|
|
717
|
+
opts.signal?.throwIfAborted();
|
|
718
|
+
const retention = opts.expiration === undefined ? undefined : retentionDays(opts.expiration);
|
|
612
719
|
const taken = await this.#withResume((runtime) => runtime.snapshot({
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
720
|
+
mode: "disk",
|
|
721
|
+
...(retention !== undefined ? { retentionDays: retention } : {}),
|
|
722
|
+
...signalOf(opts),
|
|
723
|
+
}), opts);
|
|
724
|
+
try {
|
|
725
|
+
await guard(() => this.#rt.stop(signalOf(opts)), this.name);
|
|
726
|
+
if (this.#rt.state !== "stopped")
|
|
727
|
+
await guard(() => this.#rt.waitFor("stopped", signalOf(opts)), this.name);
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
const recovery = { snapshotId: taken.id, sourceSandboxId: this.#rt.id };
|
|
731
|
+
if (error instanceof APIError) {
|
|
732
|
+
const json = error.json;
|
|
733
|
+
error.json = {
|
|
734
|
+
...json,
|
|
735
|
+
error: {
|
|
736
|
+
...json?.error,
|
|
737
|
+
details: { ...(json?.error?.details ?? {}), ...recovery },
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
else if (error instanceof Error && Object.isExtensible(error)) {
|
|
742
|
+
Object.assign(error, recovery);
|
|
743
|
+
}
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
if (this.#rt.state !== "stopped") {
|
|
747
|
+
const code = "snapshot_source_stop_timeout";
|
|
748
|
+
const message = `Snapshot ${taken.id} was saved, but sandbox ${this.name} did not finish stopping.`;
|
|
749
|
+
const error = new APIError(new Response(null, { status: 409 }), {
|
|
750
|
+
message,
|
|
751
|
+
sandboxName: this.name,
|
|
752
|
+
json: {
|
|
753
|
+
error: { code, message, details: { snapshotId: taken.id, sourceSandboxId: this.#rt.id } },
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
error.code = code;
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
616
759
|
return new Snapshot(taken, this.#client);
|
|
617
760
|
}
|
|
618
761
|
// ---- what Runtime does differently -----------------------------------
|
|
762
|
+
/** Refused: Vercel hands back a token scoped to this one shell, and
|
|
763
|
+
* Runtime's terminal opens with the account key, which must not leave the
|
|
764
|
+
* server. */
|
|
619
765
|
openInteractive() {
|
|
620
766
|
return Promise.reject(new NotSupportedError("Vercel's interactive shell (openInteractive)", "Use `await sandbox.withruntime.terminal({ cols, rows, onData })`, or `npx withruntime sandbox shell <id>`."));
|
|
621
767
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
768
|
+
/** The user commands run as, `runtime`, and its group. */
|
|
769
|
+
getDefaultUser(_opts = {}) {
|
|
770
|
+
this.#defaultUser ??= Promise.resolve({ username: "runtime", group: "runtime" });
|
|
771
|
+
return this.#defaultUser;
|
|
772
|
+
}
|
|
773
|
+
/** Makes a Linux user with its own home, as Vercel does: `useradd -m`,
|
|
774
|
+
* then the home given to the user and the sandbox's group, mode 770. */
|
|
775
|
+
async createUser(username, opts = {}) {
|
|
776
|
+
validateName(username, "username");
|
|
777
|
+
const { group } = await this.getDefaultUser(opts);
|
|
778
|
+
const signal = opts.signal;
|
|
779
|
+
const useradd = await this.runCommand({
|
|
780
|
+
cmd: "useradd",
|
|
781
|
+
args: ["-m", "-s", "/bin/bash", username],
|
|
782
|
+
sudo: true,
|
|
783
|
+
...(signal ? { signal } : {}),
|
|
784
|
+
});
|
|
785
|
+
if (useradd.exitCode !== 0) {
|
|
786
|
+
if (useradd.exitCode === 9)
|
|
787
|
+
throw new SandboxUserAlreadyExistsError(username);
|
|
788
|
+
throw new Error(`Failed to create user "${username}": ${await useradd.stderr()}`);
|
|
789
|
+
}
|
|
790
|
+
const home = `/home/${username}`;
|
|
791
|
+
await rootOrThrow(this, "chown", [`${username}:${group}`, home], signal, () => `Failed to set ownership on ${home}`);
|
|
792
|
+
await rootOrThrow(this, "chmod", ["770", home], signal, () => `Failed to set permissions on ${home}`);
|
|
793
|
+
return new SandboxUser({ sandbox: this, username });
|
|
794
|
+
}
|
|
795
|
+
/** A handle for an existing user, such as `root`. */
|
|
796
|
+
asUser(username) {
|
|
797
|
+
validateName(username, "username");
|
|
798
|
+
return new SandboxUser({ sandbox: this, username });
|
|
799
|
+
}
|
|
800
|
+
/** Makes a group and its shared directory, `/shared/<group>`, setgid 2770. */
|
|
801
|
+
async createGroup(groupname, opts = {}) {
|
|
802
|
+
validateName(groupname, "group name");
|
|
803
|
+
const { username } = await this.getDefaultUser(opts);
|
|
804
|
+
const sharedDir = `/shared/${groupname}`;
|
|
805
|
+
const steps = [
|
|
806
|
+
["groupadd", [groupname], `Failed to create group "${groupname}"`],
|
|
807
|
+
["mkdir", ["-p", sharedDir], `Failed to create shared directory ${sharedDir}`],
|
|
808
|
+
["chown", [`${username}:${groupname}`, sharedDir], `Failed to set ownership on ${sharedDir}`],
|
|
809
|
+
["chmod", ["2770", sharedDir], `Failed to set permissions on ${sharedDir}`],
|
|
810
|
+
];
|
|
811
|
+
for (const [cmd, args, what] of steps)
|
|
812
|
+
await rootOrThrow(this, cmd, args, opts.signal, () => what);
|
|
813
|
+
return { groupname, sharedDir };
|
|
633
814
|
}
|
|
634
|
-
addUserToGroup() {
|
|
635
|
-
|
|
815
|
+
async addUserToGroup(username, groupname, opts = {}) {
|
|
816
|
+
validateName(username, "username");
|
|
817
|
+
validateName(groupname, "group name");
|
|
818
|
+
await rootOrThrow(this, "usermod", ["-aG", groupname, username], opts.signal, () => `Failed to add "${username}" to group "${groupname}"`);
|
|
636
819
|
}
|
|
637
|
-
removeUserFromGroup() {
|
|
638
|
-
|
|
820
|
+
async removeUserFromGroup(username, groupname, opts = {}) {
|
|
821
|
+
validateName(username, "username");
|
|
822
|
+
validateName(groupname, "group name");
|
|
823
|
+
await rootOrThrow(this, "gpasswd", ["-d", username, groupname], opts.signal, () => `Failed to remove "${username}" from group "${groupname}"`);
|
|
639
824
|
}
|
|
640
825
|
currentSession() {
|
|
641
826
|
throw new NotSupportedError("Vercel sessions (currentSession)", "A Runtime sandbox is its own session: call the methods on the sandbox.");
|
|
@@ -647,15 +832,11 @@ export class Sandbox {
|
|
|
647
832
|
return Promise.reject(new NotSupportedError("Listing a sandbox's snapshots from the sandbox", "Use Snapshot.list({ name }) or runtime.snapshots.list({ sandboxId })."));
|
|
648
833
|
}
|
|
649
834
|
}
|
|
650
|
-
function usersError() {
|
|
651
|
-
return new NotSupportedError("Extra Linux users and groups (createUser, asUser, groups)", "Commands run as the sandbox owner with passwordless sudo; run `sudo useradd ...` and `sudo -u <user> ...` with runCommand.");
|
|
652
|
-
}
|
|
653
|
-
function usersRefused() {
|
|
654
|
-
return Promise.reject(usersError());
|
|
655
|
-
}
|
|
656
835
|
/** Milliseconds as whole days for Runtime's retention: 1 to 365, and 0 (no
|
|
657
836
|
* expiration) as 365. */
|
|
658
837
|
export function retentionDays(ms) {
|
|
838
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
839
|
+
throw new RangeError(`Snapshot expiration must be a nonnegative finite number of milliseconds, not ${ms}.`);
|
|
659
840
|
if (ms === 0)
|
|
660
841
|
return 365;
|
|
661
842
|
return Math.min(365, Math.max(1, Math.ceil(ms / 86_400_000)));
|
|
@@ -671,11 +852,13 @@ async function pipe(command, params) {
|
|
|
671
852
|
}
|
|
672
853
|
/** The newest live sandbox with this name, or the sandbox with this Runtime
|
|
673
854
|
* id. A 404 when there is none. */
|
|
674
|
-
async function find(client, name) {
|
|
855
|
+
async function find(client, name, opts = {}) {
|
|
856
|
+
opts.signal?.throwIfAborted();
|
|
675
857
|
if (UUID.test(name))
|
|
676
|
-
return client.sandboxes.get(name);
|
|
677
|
-
const page = await client.sandboxes.list({ name });
|
|
858
|
+
return client.sandboxes.get(name, signalOf(opts));
|
|
859
|
+
const page = await client.sandboxes.list({ name }, signalOf(opts));
|
|
678
860
|
const all = await page.toArray(1000);
|
|
861
|
+
opts.signal?.throwIfAborted();
|
|
679
862
|
const found = all.filter((one) => one.state !== "stopped").at(-1);
|
|
680
863
|
if (!found)
|
|
681
864
|
throw new RuntimeError({
|
|
@@ -703,24 +886,95 @@ function itemOf(runtime) {
|
|
|
703
886
|
tags: { ...info.labels },
|
|
704
887
|
};
|
|
705
888
|
}
|
|
889
|
+
const CURSOR = /^rt\.([0-9]+)$/;
|
|
890
|
+
/** Sandbox.list with a name prefix, time bounds, sorting or a cursor:
|
|
891
|
+
* applied here over every sandbox the key sees, since Runtime's list filters
|
|
892
|
+
* only by labels and name. The cursor is this adapter's own, an offset into
|
|
893
|
+
* the same query. */
|
|
894
|
+
async function listHere(client, params) {
|
|
895
|
+
let offset = 0;
|
|
896
|
+
if (params.cursor !== undefined) {
|
|
897
|
+
const match = CURSOR.exec(params.cursor);
|
|
898
|
+
if (!match)
|
|
899
|
+
throw new RangeError(`The cursor "${params.cursor}" was not made by Sandbox.list here; pass pagination.next from an earlier page of the same query.`);
|
|
900
|
+
offset = Number(match[1]);
|
|
901
|
+
}
|
|
902
|
+
if (params.sortBy !== undefined &&
|
|
903
|
+
!["createdAt", "name", "statusUpdatedAt"].includes(params.sortBy))
|
|
904
|
+
throw new RangeError("sortBy must be createdAt, name or statusUpdatedAt.");
|
|
905
|
+
if (params.sortOrder !== undefined && !["asc", "desc"].includes(params.sortOrder))
|
|
906
|
+
throw new RangeError("sortOrder must be asc or desc.");
|
|
907
|
+
const page = await guard(() => client.sandboxes.list(params.tags ? { labels: params.tags } : {}, signalOf(params)));
|
|
908
|
+
const all = (await guard(() => page.toArray(10_000))).map(itemOf);
|
|
909
|
+
params.signal?.throwIfAborted();
|
|
910
|
+
const time = (value) => value === undefined ? undefined : value instanceof Date ? value.getTime() : value;
|
|
911
|
+
const since = time(params.since);
|
|
912
|
+
const until = time(params.until);
|
|
913
|
+
const items = all.filter((one) => (params.namePrefix === undefined || one.name.startsWith(params.namePrefix)) &&
|
|
914
|
+
(since === undefined || one.createdAt >= since) &&
|
|
915
|
+
(until === undefined || one.createdAt <= until));
|
|
916
|
+
const by = params.sortBy ?? "createdAt";
|
|
917
|
+
const key = (one) => by === "name" ? one.name : by === "statusUpdatedAt" ? one.updatedAt : one.createdAt;
|
|
918
|
+
const direction = params.sortOrder === "desc" ? -1 : 1;
|
|
919
|
+
items.sort((a, b) => {
|
|
920
|
+
const [x, y] = [key(a), key(b)];
|
|
921
|
+
return (x < y ? -1 : x > y ? 1 : 0) * direction;
|
|
922
|
+
});
|
|
923
|
+
const size = Math.max(1, Math.min(params.limit ?? 50, 100));
|
|
924
|
+
const pageAt = (from) => {
|
|
925
|
+
const sandboxes = items.slice(from, from + size);
|
|
926
|
+
return {
|
|
927
|
+
sandboxes,
|
|
928
|
+
pagination: {
|
|
929
|
+
count: sandboxes.length,
|
|
930
|
+
next: from + size < items.length ? `rt.${from + size}` : null,
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
};
|
|
934
|
+
return Object.assign(pageAt(offset), {
|
|
935
|
+
async *pages() {
|
|
936
|
+
for (let from = offset; from === offset || from < items.length; from += size) {
|
|
937
|
+
params.signal?.throwIfAborted();
|
|
938
|
+
yield pageAt(from);
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
async *[Symbol.asyncIterator]() {
|
|
942
|
+
for (const item of items.slice(offset)) {
|
|
943
|
+
params.signal?.throwIfAborted();
|
|
944
|
+
yield item;
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
async toArray() {
|
|
948
|
+
params.signal?.throwIfAborted();
|
|
949
|
+
return items.slice(offset);
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
}
|
|
706
953
|
/** Vercel's paginator over a Runtime page: the first page's fields, async
|
|
707
954
|
* iteration over every item, `pages()` and `toArray()`. */
|
|
708
|
-
function paginate(first) {
|
|
955
|
+
function paginate(first, signal) {
|
|
709
956
|
const toPage = (page) => ({
|
|
710
957
|
sandboxes: page.data.map(itemOf),
|
|
711
958
|
pagination: { count: page.data.length, next: page.nextCursor },
|
|
712
959
|
});
|
|
713
960
|
return Object.assign(toPage(first), {
|
|
714
961
|
async *pages() {
|
|
715
|
-
for await (const page of first.pages())
|
|
962
|
+
for await (const page of first.pages()) {
|
|
963
|
+
signal?.throwIfAborted();
|
|
716
964
|
yield toPage(page);
|
|
965
|
+
}
|
|
717
966
|
},
|
|
718
967
|
async *[Symbol.asyncIterator]() {
|
|
719
|
-
for await (const item of first)
|
|
968
|
+
for await (const item of first) {
|
|
969
|
+
signal?.throwIfAborted();
|
|
720
970
|
yield itemOf(item);
|
|
971
|
+
}
|
|
721
972
|
},
|
|
722
973
|
async toArray() {
|
|
723
|
-
|
|
974
|
+
signal?.throwIfAborted();
|
|
975
|
+
const items = await first.toArray();
|
|
976
|
+
signal?.throwIfAborted();
|
|
977
|
+
return items.map(itemOf);
|
|
724
978
|
},
|
|
725
979
|
});
|
|
726
980
|
}
|