@vercel/sandbox 1.8.0 → 2.0.0-beta.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/dist/sandbox.js CHANGED
@@ -1,74 +1,153 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Sandbox = void 0;
4
- const promises_1 = require("stream/promises");
5
- const fs_1 = require("fs");
6
- const promises_2 = require("fs/promises");
7
- const path_1 = require("path");
8
4
  const api_client_1 = require("./api-client");
9
- const command_1 = require("./command");
5
+ const api_error_1 = require("./api-client/api-error");
10
6
  const get_credentials_1 = require("./utils/get-credentials");
11
7
  const types_1 = require("./utils/types");
12
- const snapshot_1 = require("./snapshot");
13
- const consume_readable_1 = require("./utils/consume-readable");
14
- const convert_sandbox_1 = require("./utils/convert-sandbox");
8
+ const session_1 = require("./session");
9
+ const network_policy_1 = require("./utils/network-policy");
10
+ const promises_1 = require("node:timers/promises");
11
+ function isSandboxStoppedError(err) {
12
+ return err instanceof api_error_1.APIError && err.response.status === 410;
13
+ }
14
+ function isSandboxStoppingError(err) {
15
+ return (err instanceof api_error_1.APIError &&
16
+ err.response.status === 422 &&
17
+ err.json?.error?.code === "sandbox_stopping");
18
+ }
15
19
  /**
16
- * A Sandbox is an isolated Linux MicroVM to run commands in.
17
- *
20
+ * A Sandbox is a persistent, isolated Linux MicroVMs to run commands in.
18
21
  * Use {@link Sandbox.create} or {@link Sandbox.get} to construct.
19
22
  * @hideconstructor
20
23
  */
21
24
  class Sandbox {
22
25
  /**
23
- * Unique ID of this sandbox.
26
+ * The name of this sandbox.
24
27
  */
25
- get sandboxId() {
26
- return this.sandbox.id;
28
+ get name() {
29
+ return this.namedSandbox.name;
27
30
  }
28
- get interactivePort() {
29
- return this.sandbox.interactivePort ?? undefined;
31
+ /**
32
+ * Routes from ports to subdomains.
33
+ * @hidden
34
+ */
35
+ get routes() {
36
+ return this.session.routes;
30
37
  }
31
38
  /**
32
- * The status of the sandbox.
39
+ * Whether this sandbox snapshots on shutdown.
33
40
  */
34
- get status() {
35
- return this.sandbox.status;
41
+ get snapshotOnShutdown() {
42
+ return this.namedSandbox.snapshotOnShutdown;
43
+ }
44
+ /**
45
+ * The region this sandbox runs in.
46
+ */
47
+ get region() {
48
+ return this.namedSandbox.region;
49
+ }
50
+ /**
51
+ * Number of virtual CPUs allocated.
52
+ */
53
+ get vcpus() {
54
+ return this.namedSandbox.vcpus;
55
+ }
56
+ /**
57
+ * Memory allocated in MB.
58
+ */
59
+ get memory() {
60
+ return this.namedSandbox.memory;
61
+ }
62
+ /** Runtime identifier (e.g. "node24", "python3.13"). */
63
+ get runtime() {
64
+ return this.namedSandbox.runtime;
36
65
  }
37
66
  /**
38
- * The creation date of the sandbox.
67
+ * Cumulative egress bytes across all sessions.
68
+ */
69
+ get totalEgressBytes() {
70
+ return this.namedSandbox.totalEgressBytes;
71
+ }
72
+ /**
73
+ * Cumulative ingress bytes across all sessions.
74
+ */
75
+ get totalIngressBytes() {
76
+ return this.namedSandbox.totalIngressBytes;
77
+ }
78
+ /**
79
+ * Cumulative active CPU duration in milliseconds across all sessions.
80
+ */
81
+ get totalActiveCpuDurationMs() {
82
+ return this.namedSandbox.totalActiveCpuDurationMs;
83
+ }
84
+ /**
85
+ * Cumulative wall-clock duration in milliseconds across all sessions.
86
+ */
87
+ get totalDurationMs() {
88
+ return this.namedSandbox.totalDurationMs;
89
+ }
90
+ /**
91
+ * When this sandbox was last updated.
92
+ */
93
+ get updatedAt() {
94
+ return new Date(this.namedSandbox.updatedAt);
95
+ }
96
+ /**
97
+ * When this sandbox was created.
39
98
  */
40
99
  get createdAt() {
41
- return new Date(this.sandbox.createdAt);
100
+ return new Date(this.namedSandbox.createdAt);
101
+ }
102
+ /**
103
+ * Interactive port.
104
+ */
105
+ get interactivePort() {
106
+ return this.session.interactivePort;
42
107
  }
43
108
  /**
44
- * The timeout of the sandbox in milliseconds.
109
+ * The status of the current session.
110
+ */
111
+ get status() {
112
+ return this.session.status;
113
+ }
114
+ /**
115
+ * The default timeout of this sandbox in milliseconds.
45
116
  */
46
117
  get timeout() {
47
- return this.sandbox.timeout;
118
+ return this.namedSandbox.timeout;
48
119
  }
49
120
  /**
50
- * The network policy of the sandbox.
121
+ * The default network policy of this sandbox.
51
122
  */
52
123
  get networkPolicy() {
53
- return this.sandbox.networkPolicy;
124
+ return this.namedSandbox.networkPolicy
125
+ ? (0, network_policy_1.fromAPINetworkPolicy)(this.namedSandbox.networkPolicy)
126
+ : undefined;
54
127
  }
55
128
  /**
56
- * If the sandbox was created from a snapshot, the ID of that snapshot.
129
+ * If the session was created from a snapshot, the ID of that snapshot.
57
130
  */
58
131
  get sourceSnapshotId() {
59
- return this.sandbox.sourceSnapshotId;
132
+ return this.session.sourceSnapshotId;
60
133
  }
61
134
  /**
62
- * The amount of CPU used by the sandbox. Only reported once the VM is stopped.
135
+ * The current snapshot ID of this sandbox, if any.
136
+ */
137
+ get currentSnapshotId() {
138
+ return this.namedSandbox.currentSnapshotId;
139
+ }
140
+ /**
141
+ * The amount of CPU used by the session. Only reported once the VM is stopped.
63
142
  */
64
143
  get activeCpuUsageMs() {
65
- return this.sandbox.activeCpuDurationMs;
144
+ return this.session.activeCpuUsageMs;
66
145
  }
67
146
  /**
68
- * The amount of network data used by the sandbox. Only reported once the VM is stopped.
147
+ * The amount of network data used by the session. Only reported once the VM is stopped.
69
148
  */
70
149
  get networkTransfer() {
71
- return this.sandbox.networkTransfer;
150
+ return this.session.networkTransfer;
72
151
  }
73
152
  /**
74
153
  * Allow to get a list of sandboxes for a team narrowed to the given params.
@@ -82,7 +161,7 @@ class Sandbox {
82
161
  token: credentials.token,
83
162
  fetch: params?.fetch,
84
163
  });
85
- return client.listSandboxes({
164
+ return client.listNamedSandboxes({
86
165
  ...credentials,
87
166
  ...params,
88
167
  });
@@ -107,7 +186,7 @@ class Sandbox {
107
186
  fetch: params?.fetch,
108
187
  });
109
188
  const privateParams = (0, types_1.getPrivateParams)(params);
110
- const sandbox = await client.createSandbox({
189
+ const response = await client.createSandbox({
111
190
  source: params?.source,
112
191
  projectId: credentials.projectId,
113
192
  ports: params?.ports ?? [],
@@ -117,16 +196,20 @@ class Sandbox {
117
196
  networkPolicy: params?.networkPolicy,
118
197
  env: params?.env,
119
198
  signal: params?.signal,
199
+ name: params?.name,
200
+ snapshotOnShutdown: params?.snapshotOnShutdown,
120
201
  ...privateParams,
121
202
  });
122
203
  return new DisposableSandbox({
123
204
  client,
124
- sandbox: sandbox.json.sandbox,
125
- routes: sandbox.json.routes,
205
+ session: response.json.sandbox,
206
+ namedSandbox: response.json.namedSandbox,
207
+ routes: response.json.routes,
208
+ projectId: credentials.projectId,
126
209
  });
127
210
  }
128
211
  /**
129
- * Retrieve an existing sandbox.
212
+ * Retrieve an existing sandbox and resume its session.
130
213
  *
131
214
  * @param params - Get parameters and optional credentials.
132
215
  * @returns A promise resolving to the {@link Sandbox}.
@@ -138,47 +221,93 @@ class Sandbox {
138
221
  token: credentials.token,
139
222
  fetch: params.fetch,
140
223
  });
141
- const privateParams = (0, types_1.getPrivateParams)(params);
142
- const sandbox = await client.getSandbox({
143
- sandboxId: params.sandboxId,
224
+ const response = await client.getNamedSandbox({
225
+ name: params.name,
226
+ projectId: credentials.projectId,
227
+ resume: params.resume,
144
228
  signal: params.signal,
145
- ...privateParams,
146
229
  });
147
230
  return new Sandbox({
148
231
  client,
149
- sandbox: sandbox.json.sandbox,
150
- routes: sandbox.json.routes,
232
+ session: response.json.sandbox,
233
+ namedSandbox: response.json.namedSandbox,
234
+ routes: response.json.routes,
235
+ projectId: credentials.projectId,
151
236
  });
152
237
  }
153
- constructor({ client, routes, sandbox, }) {
238
+ constructor({ client, routes, session, namedSandbox, projectId, }) {
154
239
  this.client = client;
155
- this.routes = routes;
156
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(sandbox);
240
+ this.session = new session_1.Session({ client, routes, session });
241
+ this.namedSandbox = namedSandbox;
242
+ this.projectId = projectId;
157
243
  }
158
244
  /**
159
- * Get a previously run command by its ID.
245
+ * Get the current session (the running VM) for this sandbox.
160
246
  *
161
- * @param cmdId - ID of the command to retrieve
162
- * @param opts - Optional parameters.
163
- * @param opts.signal - An AbortSignal to cancel the operation.
164
- * @returns A {@link Command} instance representing the command
247
+ * @returns The {@link Session} instance.
165
248
  */
166
- async getCommand(cmdId, opts) {
167
- const command = await this.client.getCommand({
168
- sandboxId: this.sandbox.id,
169
- cmdId,
170
- signal: opts?.signal,
249
+ currentSession() {
250
+ return this.session;
251
+ }
252
+ /**
253
+ * Resume this sandbox by creating a new session via `getNamedSandbox`.
254
+ */
255
+ async resume(signal) {
256
+ const response = await this.client.getNamedSandbox({
257
+ name: this.namedSandbox.name,
258
+ projectId: this.projectId,
259
+ resume: true,
260
+ signal,
171
261
  });
172
- return new command_1.Command({
262
+ this.session = new session_1.Session({
173
263
  client: this.client,
174
- sandboxId: this.sandbox.id,
175
- cmd: command.json.command,
264
+ routes: response.json.routes,
265
+ session: response.json.sandbox,
176
266
  });
177
267
  }
268
+ /**
269
+ * Poll until the current session reaches a terminal state, then resume.
270
+ */
271
+ async waitForStopAndResume(signal) {
272
+ const pollingInterval = 500;
273
+ let status = this.session.status;
274
+ while (status === "stopping" || status === "snapshotting") {
275
+ await (0, promises_1.setTimeout)(pollingInterval, undefined, { signal });
276
+ const poll = await this.client.getSandbox({
277
+ sandboxId: this.session.sessionId,
278
+ signal,
279
+ });
280
+ this.session = new session_1.Session({
281
+ client: this.client,
282
+ routes: poll.json.routes,
283
+ session: poll.json.sandbox,
284
+ });
285
+ status = poll.json.sandbox.status;
286
+ }
287
+ await this.resume(signal);
288
+ }
289
+ /**
290
+ * Execute `fn`, and if the session is stopped/stopping, resume and retry.
291
+ */
292
+ async withResume(fn, signal) {
293
+ try {
294
+ return await fn();
295
+ }
296
+ catch (err) {
297
+ if (isSandboxStoppedError(err)) {
298
+ await this.resume(signal);
299
+ return fn();
300
+ }
301
+ if (isSandboxStoppingError(err)) {
302
+ await this.waitForStopAndResume(signal);
303
+ return fn();
304
+ }
305
+ throw err;
306
+ }
307
+ }
178
308
  async runCommand(commandOrParams, args, opts) {
179
- return typeof commandOrParams === "string"
180
- ? this._runCommand({ cmd: commandOrParams, args, signal: opts?.signal })
181
- : this._runCommand(commandOrParams);
309
+ const signal = typeof commandOrParams === "string" ? opts?.signal : commandOrParams.signal;
310
+ return this.withResume(() => this.session.runCommand(commandOrParams, args, opts), signal);
182
311
  }
183
312
  /**
184
313
  * Internal helper to start a command in the sandbox.
@@ -187,71 +316,8 @@ class Sandbox {
187
316
  * @returns A {@link Command} or {@link CommandFinished}, depending on `detached`.
188
317
  * @internal
189
318
  */
190
- async _runCommand(params) {
191
- const wait = params.detached ? false : true;
192
- const getLogs = (command) => {
193
- if (params.stdout || params.stderr) {
194
- (async () => {
195
- try {
196
- for await (const log of command.logs({ signal: params.signal })) {
197
- if (log.stream === "stdout") {
198
- params.stdout?.write(log.data);
199
- }
200
- else if (log.stream === "stderr") {
201
- params.stderr?.write(log.data);
202
- }
203
- }
204
- }
205
- catch (err) {
206
- if (params.signal?.aborted) {
207
- return;
208
- }
209
- throw err;
210
- }
211
- })();
212
- }
213
- };
214
- if (wait) {
215
- const commandStream = await this.client.runCommand({
216
- sandboxId: this.sandbox.id,
217
- command: params.cmd,
218
- args: params.args ?? [],
219
- cwd: params.cwd,
220
- env: params.env ?? {},
221
- sudo: params.sudo ?? false,
222
- wait: true,
223
- signal: params.signal,
224
- });
225
- const command = new command_1.Command({
226
- client: this.client,
227
- sandboxId: this.sandbox.id,
228
- cmd: commandStream.command,
229
- });
230
- getLogs(command);
231
- const finished = await commandStream.finished;
232
- return new command_1.CommandFinished({
233
- client: this.client,
234
- sandboxId: this.sandbox.id,
235
- cmd: finished,
236
- exitCode: finished.exitCode ?? 0,
237
- });
238
- }
239
- const commandResponse = await this.client.runCommand({
240
- sandboxId: this.sandbox.id,
241
- command: params.cmd,
242
- args: params.args ?? [],
243
- cwd: params.cwd,
244
- env: params.env ?? {},
245
- sudo: params.sudo ?? false,
246
- signal: params.signal,
247
- });
248
- const command = new command_1.Command({
249
- client: this.client,
250
- sandboxId: this.sandbox.id,
251
- cmd: commandResponse.json.command,
252
- });
253
- getLogs(command);
254
- return command;
319
+ async getCommand(cmdId, opts) {
320
+ return this.withResume(() => this.session.getCommand(cmdId, opts), opts?.signal);
255
321
  }
256
322
  /**
257
323
  * Create a directory in the filesystem of this sandbox.
@@ -261,11 +327,7 @@ class Sandbox {
261
327
  * @param opts.signal - An AbortSignal to cancel the operation.
262
328
  */
263
329
  async mkDir(path, opts) {
264
- await this.client.mkDir({
265
- sandboxId: this.sandbox.id,
266
- path: path,
267
- signal: opts?.signal,
268
- });
330
+ return this.withResume(() => this.session.mkDir(path, opts), opts?.signal);
269
331
  }
270
332
  /**
271
333
  * Read a file from the filesystem of this sandbox as a stream.
@@ -276,12 +338,7 @@ class Sandbox {
276
338
  * @returns A promise that resolves to a ReadableStream containing the file contents, or null if file not found
277
339
  */
278
340
  async readFile(file, opts) {
279
- return this.client.readFile({
280
- sandboxId: this.sandbox.id,
281
- path: file.path,
282
- cwd: file.cwd,
283
- signal: opts?.signal,
284
- });
341
+ return this.withResume(() => this.session.readFile(file, opts), opts?.signal);
285
342
  }
286
343
  /**
287
344
  * Read a file from the filesystem of this sandbox as a Buffer.
@@ -292,16 +349,7 @@ class Sandbox {
292
349
  * @returns A promise that resolves to the file contents as a Buffer, or null if file not found
293
350
  */
294
351
  async readFileToBuffer(file, opts) {
295
- const stream = await this.client.readFile({
296
- sandboxId: this.sandbox.id,
297
- path: file.path,
298
- cwd: file.cwd,
299
- signal: opts?.signal,
300
- });
301
- if (stream === null) {
302
- return null;
303
- }
304
- return (0, consume_readable_1.consumeReadable)(stream);
352
+ return this.withResume(() => this.session.readFileToBuffer(file, opts), opts?.signal);
305
353
  }
306
354
  /**
307
355
  * Download a file from the sandbox to the local filesystem.
@@ -314,34 +362,7 @@ class Sandbox {
314
362
  * @returns The absolute path to the written file, or null if the source file was not found
315
363
  */
316
364
  async downloadFile(src, dst, opts) {
317
- if (!src?.path) {
318
- throw new Error("downloadFile: source path is required");
319
- }
320
- if (!dst?.path) {
321
- throw new Error("downloadFile: destination path is required");
322
- }
323
- const stream = await this.client.readFile({
324
- sandboxId: this.sandbox.id,
325
- path: src.path,
326
- cwd: src.cwd,
327
- signal: opts?.signal,
328
- });
329
- if (stream === null) {
330
- return null;
331
- }
332
- try {
333
- const dstPath = (0, path_1.resolve)(dst.cwd ?? "", dst.path);
334
- if (opts?.mkdirRecursive) {
335
- await (0, promises_2.mkdir)((0, path_1.dirname)(dstPath), { recursive: true });
336
- }
337
- await (0, promises_1.pipeline)(stream, (0, fs_1.createWriteStream)(dstPath), {
338
- signal: opts?.signal,
339
- });
340
- return dstPath;
341
- }
342
- finally {
343
- stream.destroy();
344
- }
365
+ return this.withResume(() => this.session.downloadFile(src, dst, opts), opts?.signal);
345
366
  }
346
367
  /**
347
368
  * Write files to the filesystem of this sandbox.
@@ -354,13 +375,7 @@ class Sandbox {
354
375
  * @returns A promise that resolves when the files are written
355
376
  */
356
377
  async writeFiles(files, opts) {
357
- return this.client.writeFiles({
358
- sandboxId: this.sandbox.id,
359
- cwd: this.sandbox.cwd,
360
- extractDir: "/",
361
- files: files,
362
- signal: opts?.signal,
363
- });
378
+ return this.withResume(() => this.session.writeFiles(files, opts), opts?.signal);
364
379
  }
365
380
  /**
366
381
  * Get the public domain of a port of this sandbox.
@@ -370,13 +385,7 @@ class Sandbox {
370
385
  * @throws If the port has no associated route
371
386
  */
372
387
  domain(p) {
373
- const route = this.routes.find(({ port }) => port == p);
374
- if (route) {
375
- return `https://${route.subdomain}.vercel.run`;
376
- }
377
- else {
378
- throw new Error(`No route for port ${p}`);
379
- }
388
+ return this.session.domain(p);
380
389
  }
381
390
  /**
382
391
  * Stop the sandbox.
@@ -387,13 +396,7 @@ class Sandbox {
387
396
  * @returns The sandbox metadata at the time the stop was acknowledged, or after fully stopped if `blocking` is true.
388
397
  */
389
398
  async stop(opts) {
390
- const response = await this.client.stopSandbox({
391
- sandboxId: this.sandbox.id,
392
- signal: opts?.signal,
393
- blocking: opts?.blocking,
394
- });
395
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
396
- return this.sandbox;
399
+ return this.session.stop(opts);
397
400
  }
398
401
  /**
399
402
  * Update the network policy for this sandbox.
@@ -427,14 +430,7 @@ class Sandbox {
427
430
  * await sandbox.updateNetworkPolicy("deny-all");
428
431
  */
429
432
  async updateNetworkPolicy(networkPolicy, opts) {
430
- const response = await this.client.updateNetworkPolicy({
431
- sandboxId: this.sandbox.id,
432
- networkPolicy: networkPolicy,
433
- signal: opts?.signal,
434
- });
435
- // Update the internal sandbox metadata with the new timeout value
436
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
437
- return this.sandbox.networkPolicy;
433
+ return this.withResume(() => this.session.updateNetworkPolicy(networkPolicy, opts), opts?.signal);
438
434
  }
439
435
  /**
440
436
  * Extend the timeout of the sandbox by the specified duration.
@@ -453,13 +449,7 @@ class Sandbox {
453
449
  * await sandbox.extendTimeout(ms('5m'));
454
450
  */
455
451
  async extendTimeout(duration, opts) {
456
- const response = await this.client.extendTimeout({
457
- sandboxId: this.sandbox.id,
458
- duration,
459
- signal: opts?.signal,
460
- });
461
- // Update the internal sandbox metadata with the new timeout value
462
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
452
+ return this.withResume(() => this.session.extendTimeout(duration, opts), opts?.signal);
463
453
  }
464
454
  /**
465
455
  * Create a snapshot from this currently running sandbox. New sandboxes can
@@ -473,15 +463,70 @@ class Sandbox {
473
463
  * @returns A promise that resolves to the Snapshot instance
474
464
  */
475
465
  async snapshot(opts) {
476
- const response = await this.client.createSnapshot({
477
- sandboxId: this.sandbox.id,
478
- expiration: opts?.expiration,
466
+ return this.withResume(() => this.session.snapshot(opts), opts?.signal);
467
+ }
468
+ /**
469
+ * Update the named sandbox configuration. Only provided fields are modified.
470
+ *
471
+ * @param params - Fields to update.
472
+ * @param opts - Optional abort signal.
473
+ */
474
+ async update(params, opts) {
475
+ const response = await this.client.updateNamedSandbox({
476
+ name: this.namedSandbox.name,
477
+ projectId: this.projectId,
478
+ resources: params.resources,
479
+ runtime: params.runtime,
480
+ timeout: params.timeout,
481
+ networkPolicy: params.networkPolicy,
479
482
  signal: opts?.signal,
480
483
  });
481
- this.sandbox = (0, convert_sandbox_1.convertSandbox)(response.json.sandbox);
482
- return new snapshot_1.Snapshot({
483
- client: this.client,
484
- snapshot: response.json.snapshot,
484
+ this.namedSandbox = response.json.namedSandbox;
485
+ }
486
+ /**
487
+ * Delete this named sandbox.
488
+ *
489
+ * After deletion the instance becomes inert — all further API calls will
490
+ * throw immediately.
491
+ */
492
+ async delete(opts) {
493
+ await this.client.deleteNamedSandbox({
494
+ name: this.namedSandbox.name,
495
+ projectId: this.projectId,
496
+ preserveSnapshots: opts?.preserveSnapshots,
497
+ signal: opts?.signal,
498
+ });
499
+ }
500
+ /**
501
+ * List sessions (VMs) that have been created for this named sandbox.
502
+ *
503
+ * @param params - Optional pagination parameters.
504
+ * @returns The list of sessions and pagination metadata.
505
+ */
506
+ async listSessions(params) {
507
+ return this.client.listSandboxes({
508
+ projectId: this.projectId,
509
+ name: this.namedSandbox.name,
510
+ limit: params?.limit,
511
+ since: params?.since,
512
+ until: params?.until,
513
+ signal: params?.signal,
514
+ });
515
+ }
516
+ /**
517
+ * List snapshots that belong to this named sandbox.
518
+ *
519
+ * @param params - Optional pagination parameters.
520
+ * @returns The list of snapshots and pagination metadata.
521
+ */
522
+ async listSnapshots(params) {
523
+ return this.client.listSnapshots({
524
+ projectId: this.projectId,
525
+ name: this.namedSandbox.name,
526
+ limit: params?.limit,
527
+ since: params?.since,
528
+ until: params?.until,
529
+ signal: params?.signal,
485
530
  });
486
531
  }
487
532
  }