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.
Files changed (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { RuntimeError } from "./errors.js";
2
2
  import type { Page } from "./page.js";
3
3
  import { type ClientExtensions } from "./products/index.js";
4
+ import { usageExport as makeUsageExport } from "./products/observability.js";
4
5
  import { Sandbox } from "./sandbox.js";
5
6
  import { Snapshots } from "./snapshots.js";
6
7
  import { Transport, type RequestOptions } from "./transport.js";
@@ -11,7 +12,7 @@ export type RuntimeOptions = {
11
12
  /** Default: RUNTIME_API_URL, then https://api.withruntime.com. */
12
13
  baseUrl?: string;
13
14
  fetch?: typeof fetch;
14
- /** Per call, retries included. Default 5 minutes. */
15
+ /** Per call, queue and retries included. Default 5 minutes; 0 disables it. */
15
16
  timeoutMs?: number;
16
17
  /** Retries of transport failures, 429, 502, 503 and 504. Default 4. */
17
18
  maxRetries?: number;
@@ -30,11 +31,16 @@ export interface Runtime extends ClientExtensions {
30
31
  }
31
32
  export declare class Runtime {
32
33
  readonly transport: Transport;
34
+ /** The sandbox product; `sandboxes` remains the same client for compatibility. */
35
+ readonly sandbox: Sandboxes;
33
36
  readonly sandboxes: Sandboxes;
37
+ readonly snapshot: Snapshots;
34
38
  readonly snapshots: Snapshots;
35
39
  readonly feedback: FeedbackApi;
36
40
  readonly support: SupportApi;
37
41
  readonly account: AccountApi;
42
+ /** Date-range settlements, paged with exact charges and the server's CSV. */
43
+ readonly usageExport: ReturnType<typeof makeUsageExport>;
38
44
  constructor(options?: RuntimeOptions);
39
45
  /** Who this key is: organization, agent and credential. */
40
46
  me(options?: RequestOptions): Promise<{
@@ -50,6 +56,25 @@ export declare class Runtime {
50
56
  /** Balance, holds, trial time and per-resource charges, in integer microdollars. */
51
57
  /** Credit, holds, trial time and per-resource charges. */
52
58
  usage(options?: RequestOptions): Promise<Usage>;
59
+ /** This account's authenticated API answers, in hourly windows. Counts are
60
+ * exact decimal strings, and an empty window has a null error percentage.
61
+ * A key needs usage permission. */
62
+ usageRequests(range?: "24h" | "7d" | "30d" | "90d", options?: RequestOptions): Promise<{
63
+ range: "24h" | "7d" | "30d" | "90d";
64
+ since: string;
65
+ until: string;
66
+ calls: string;
67
+ clientErrors: string;
68
+ serverErrors: string;
69
+ errorPercent: number | null;
70
+ operations: Array<{
71
+ operation: string;
72
+ calls: string;
73
+ clientErrors: string;
74
+ serverErrors: string;
75
+ errorPercent: number | null;
76
+ }>;
77
+ }>;
53
78
  }
54
79
  /** How `sandboxes.create` waits when every trial slot, the account's quota or
55
80
  * the region is full. */
@@ -83,7 +108,8 @@ export declare class Sandboxes {
83
108
  /** Deletes a sandbox for good, by id, without reading it first: see
84
109
  * `sandbox.delete()`. Deleting it again answers the same. */
85
110
  delete(id: string, options?: RequestOptions): Promise<DeletedSandbox>;
86
- /** Live sandboxes, oldest first. Await for a page, or `for await` over all. */
111
+ /** Live sandboxes, oldest first. Await for a page, or `for await` over all.
112
+ * Request options apply to the initial request and every subsequent page. */
87
113
  list(filter?: {
88
114
  state?: SandboxInfo["state"][];
89
115
  includeStopped?: boolean;
@@ -91,7 +117,19 @@ export declare class Sandboxes {
91
117
  labels?: Record<string, string>;
92
118
  name?: string;
93
119
  limit?: number;
94
- }): Promise<Page<Sandbox>>;
120
+ }, options?: RequestOptions): Promise<Page<Sandbox>>;
121
+ /** Stops every live sandbox whose labels all match, eight at a time, and
122
+ * says which stopped and which failed; one failure does not stop the rest.
123
+ * Needs at least one label, so stopping everything is never one call. */
124
+ stopAll(filter: {
125
+ labels: Record<string, string>;
126
+ }, options?: RequestOptions): Promise<{
127
+ stopped: string[];
128
+ failed: {
129
+ id: string;
130
+ error: unknown;
131
+ }[];
132
+ }>;
95
133
  }
96
134
  export declare class FeedbackApi {
97
135
  private readonly t;
package/dist/client.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { WAIT_FOR_TIMEOUT_SECONDS } from "./api-defaults.js";
2
2
  import { RuntimeError } from "./errors.js";
3
- import { clientFactories } from "./products/index.js";
3
+ import { clientFactories, clientProductAliases } from "./products/index.js";
4
+ import { usageExport as makeUsageExport } from "./products/observability.js";
4
5
  import { Sandbox, deleteSandbox, sandboxPage } from "./sandbox.js";
5
6
  import { Snapshots } from "./snapshots.js";
6
7
  import { Transport, missingKey } from "./transport.js";
7
- /** The key `runtime login` saved for this machine, found on first use. */
8
- const savedKey = async () => {
8
+ /** The key `runtime login` saved for this machine and this API origin, found
9
+ * on first use. A constructor override must never receive another API's key. */
10
+ const savedKey = async (apiOrigin) => {
9
11
  if (typeof process === "undefined" || !process.versions?.node)
10
12
  throw missingKey();
11
13
  const { resolveCredential } = await import("./credentials.js");
12
- return resolveCredential(process.env);
14
+ return resolveCredential({ ...process.env, RUNTIME_API_URL: apiOrigin });
13
15
  };
14
16
  const env = (name) => typeof process === "undefined"
15
17
  ? undefined
@@ -17,32 +19,42 @@ const env = (name) => typeof process === "undefined"
17
19
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
18
20
  export class Runtime {
19
21
  transport;
22
+ /** The sandbox product; `sandboxes` remains the same client for compatibility. */
23
+ sandbox;
20
24
  sandboxes;
25
+ snapshot;
21
26
  snapshots;
22
27
  feedback;
23
28
  support;
24
29
  account;
30
+ /** Date-range settlements, paged with exact charges and the server's CSV. */
31
+ usageExport;
25
32
  constructor(options = {}) {
26
33
  this.transport = new Transport({
27
34
  // A key given, then RUNTIME_API_KEY, then (in Node and Bun) the key
28
35
  // `runtime login` saved for this machine, read on the first call.
29
- apiKey: options.apiKey ?? env("RUNTIME_API_KEY") ?? savedKey,
36
+ apiKey: options.apiKey ?? env("RUNTIME_API_KEY") ?? (() => savedKey(this.transport.baseUrl)),
30
37
  baseUrl: options.baseUrl ?? env("RUNTIME_API_URL"),
31
38
  ...(options.fetch ? { fetch: options.fetch } : {}),
32
- ...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
39
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
33
40
  ...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
34
- ...(options.maxConnections ? { maxConnections: options.maxConnections } : {}),
41
+ ...(options.maxConnections !== undefined ? { maxConnections: options.maxConnections } : {}),
35
42
  ...(options.waitForCapacityMs !== undefined
36
43
  ? { waitForCapacityMs: options.waitForCapacityMs }
37
44
  : {}),
38
45
  });
39
46
  this.sandboxes = new Sandboxes(this.transport);
47
+ this.sandbox = this.sandboxes;
40
48
  this.snapshots = new Snapshots(this.transport);
49
+ this.snapshot = this.snapshots;
41
50
  this.feedback = new FeedbackApi(this.transport);
42
51
  this.support = new SupportApi(this.transport);
43
52
  this.account = new AccountApi(this.transport);
53
+ this.usageExport = makeUsageExport(this.transport);
44
54
  for (const [name, make] of clientFactories())
45
55
  Object.defineProperty(this, name, { value: make(this.transport), enumerable: true });
56
+ for (const [alias, name] of Object.entries(clientProductAliases))
57
+ Object.defineProperty(this, alias, { value: this[name], enumerable: true });
46
58
  }
47
59
  /** Who this key is: organization, agent and credential. */
48
60
  me(options = {}) {
@@ -61,6 +73,17 @@ export class Runtime {
61
73
  ...options,
62
74
  });
63
75
  }
76
+ /** This account's authenticated API answers, in hourly windows. Counts are
77
+ * exact decimal strings, and an empty window has a null error percentage.
78
+ * A key needs usage permission. */
79
+ usageRequests(range = "24h", options = {}) {
80
+ return this.transport.json({
81
+ method: "GET",
82
+ path: "/v1/usage/requests",
83
+ query: { range },
84
+ ...options,
85
+ });
86
+ }
64
87
  }
65
88
  export class Sandboxes {
66
89
  t;
@@ -84,7 +107,12 @@ export class Sandboxes {
84
107
  });
85
108
  const sandbox = new Sandbox(this.t, info);
86
109
  if (wait !== false && info.state !== "running") {
87
- await sandbox.waitFor("running", { timeoutSeconds: WAIT_FOR_TIMEOUT_SECONDS });
110
+ await sandbox.waitFor("running", {
111
+ timeoutSeconds: WAIT_FOR_TIMEOUT_SECONDS,
112
+ signal: options.signal,
113
+ timeoutMs: options.timeoutMs,
114
+ idempotencyKey: options.idempotencyKey,
115
+ });
88
116
  if (sandbox.state !== "running")
89
117
  throw new RuntimeError({
90
118
  message: `Sandbox ${info.id} is ${sandbox.state}, not running.`,
@@ -114,8 +142,9 @@ export class Sandboxes {
114
142
  delete(id, options = {}) {
115
143
  return deleteSandbox(this.t, id, options);
116
144
  }
117
- /** Live sandboxes, oldest first. Await for a page, or `for await` over all. */
118
- async list(filter = {}) {
145
+ /** Live sandboxes, oldest first. Await for a page, or `for await` over all.
146
+ * Request options apply to the initial request and every subsequent page. */
147
+ async list(filter = {}, options = {}) {
119
148
  const query = {
120
149
  ...(filter.state ? { state: filter.state } : {}),
121
150
  ...(filter.includeStopped ? { includeStopped: true } : {}),
@@ -125,7 +154,34 @@ export class Sandboxes {
125
154
  ...(filter.name ? { name: filter.name } : {}),
126
155
  ...(filter.limit ? { limit: filter.limit } : {}),
127
156
  };
128
- return sandboxPage(this.t, await this.t.json({ method: "GET", path: "/v1/sandboxes", query }), query);
157
+ return sandboxPage(this.t, await this.t.json({ method: "GET", path: "/v1/sandboxes", query, ...options }), query, options);
158
+ }
159
+ /** Stops every live sandbox whose labels all match, eight at a time, and
160
+ * says which stopped and which failed; one failure does not stop the rest.
161
+ * Needs at least one label, so stopping everything is never one call. */
162
+ async stopAll(filter, options = {}) {
163
+ if (!filter?.labels || Object.keys(filter.labels).length === 0)
164
+ throw new RuntimeError({
165
+ message: "stopAll needs at least one label to match.",
166
+ code: "invalid_request",
167
+ status: 0,
168
+ hint: "Stop one sandbox with sandbox.stop(), or label the ones to stop together.",
169
+ });
170
+ const live = await (await this.list({ labels: filter.labels, limit: 100 }, options)).toArray(Infinity);
171
+ // One key cannot name several stops: each stop makes its own.
172
+ const { idempotencyKey: _one, ...each } = options;
173
+ // In list order, oldest first, whichever finishes first.
174
+ const errors = new Array(live.length);
175
+ let next = 0;
176
+ const worker = async () => {
177
+ for (let at = next++; at < live.length; at = next++)
178
+ await live[at].stop(each).catch((error) => (errors[at] = error ?? "failed"));
179
+ };
180
+ await Promise.all(Array.from({ length: Math.min(8, live.length) }, worker));
181
+ return {
182
+ stopped: live.filter((_, at) => errors[at] === undefined).map((sbx) => sbx.id),
183
+ failed: live.flatMap((sbx, at) => errors[at] === undefined ? [] : [{ id: sbx.id, error: errors[at] }]),
184
+ };
129
185
  }
130
186
  }
131
187
  export class FeedbackApi {
@@ -0,0 +1,7 @@
1
+ /** A Durable Object ID includes its namespace; a display name does not. */
2
+ export declare function workerSandboxIdentity(objectId: string, stored: {
3
+ nativeSandboxId?: string;
4
+ sandboxName?: {
5
+ name: string;
6
+ };
7
+ }): string;
@@ -0,0 +1,13 @@
1
+ /** A Durable Object ID includes its namespace; a display name does not. */
2
+ export function workerSandboxIdentity(objectId, stored) {
3
+ if (stored.nativeSandboxId !== undefined) {
4
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(stored.nativeSandboxId))
5
+ throw new Error("The stored Runtime sandbox identity is invalid");
6
+ return stored.nativeSandboxId;
7
+ }
8
+ if (stored.sandboxName)
9
+ throw new Error("This legacy Durable Object has no namespace-safe Runtime identity. Retain its existing sandbox and migrate its identity explicitly before using it.");
10
+ if (!/^[a-f0-9]{64}$/.test(objectId))
11
+ throw new Error("The Durable Object identity is invalid");
12
+ return `cf-do-${objectId}`;
13
+ }
@@ -70,6 +70,7 @@ export interface ProcessOptions {
70
70
  onStart?: (process: Process) => void;
71
71
  onError?: (error: Error) => void;
72
72
  }
73
+ export declare function validateSandboxConfiguration(options: Pick<SandboxOptions, "sleepAfter" | "keepAlive">): void;
73
74
  /** Worker bindings are deliberately not inspected or invoked. Supply Runtime credentials
74
75
  * through options.runtime in Workers; Node also supports RUNTIME_API_KEY/login. */
75
76
  export declare function getSandbox(namespace: unknown, id: string, options?: SandboxOptions): Sandbox;
@@ -77,9 +78,17 @@ export declare class Sandbox {
77
78
  readonly runtime: Runtime;
78
79
  private readonly options;
79
80
  private pending?;
81
+ private existing?;
80
82
  readonly id: string;
81
83
  constructor(runtime: Runtime, id: string, options?: SandboxOptions);
82
84
  configure(options: Pick<SandboxOptions, "sleepAfter" | "keepAlive">): Promise<void>;
85
+ /** Only a successfully initialized native handle can supply a recovery id. */
86
+ existingNativeId(): string | undefined;
87
+ /** Reconcile an uncertain update from an authoritative read, without allocation. */
88
+ reconcileConfiguration(nativeId?: string | undefined): Promise<{
89
+ sleepAfter: number;
90
+ keepAlive: boolean;
91
+ }>;
83
92
  native(): Promise<NativeSandbox>;
84
93
  private session;
85
94
  exec(command: string, options?: ExecOptions): Promise<ExecResult>;
@@ -22,12 +22,24 @@ export function sanitizeSandboxId(id) {
22
22
  }
23
23
  const cleanEnv = (env = {}) => Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
24
24
  function sleepSeconds(value = 600) {
25
+ let seconds;
25
26
  if (typeof value === "number")
26
- return value;
27
- const match = /^(\d+)(s|m|h)$/.exec(value);
28
- if (!match)
29
- throw new TypeError("sleepAfter must be seconds or a duration such as 5m");
30
- return Number(match[1]) * ({ s: 1, m: 60, h: 3600 }[match[2]] ?? 1);
27
+ seconds = value;
28
+ else {
29
+ const match = /^(\d+)(s|m|h)$/.exec(value);
30
+ if (!match)
31
+ throw new TypeError("sleepAfter must be seconds or a duration such as 5m");
32
+ seconds = Number(match[1]) * ({ s: 1, m: 60, h: 3600 }[match[2]] ?? 1);
33
+ }
34
+ if (!Number.isSafeInteger(seconds) || seconds < 0 || seconds > 86_400)
35
+ throw new CompatibilityError("Cloudflare", "sleepAfter outside whole seconds from 0 to 86400");
36
+ return seconds;
37
+ }
38
+ export function validateSandboxConfiguration(options) {
39
+ if (options.sleepAfter !== undefined)
40
+ sleepSeconds(options.sleepAfter);
41
+ if (options.keepAlive !== undefined && typeof options.keepAlive !== "boolean")
42
+ throw new TypeError("keepAlive must be a boolean");
31
43
  }
32
44
  /** Worker bindings are deliberately not inspected or invoked. Supply Runtime credentials
33
45
  * through options.runtime in Workers; Node also supports RUNTIME_API_KEY/login. */
@@ -41,25 +53,50 @@ export class Sandbox {
41
53
  runtime;
42
54
  options;
43
55
  pending;
56
+ existing;
44
57
  id;
45
58
  constructor(runtime, id, options = {}) {
46
59
  this.runtime = runtime;
47
60
  this.options = options;
61
+ validateSandboxConfiguration(options);
48
62
  this.id = id;
49
63
  }
50
64
  async configure(options) {
51
- if (options.sleepAfter !== undefined)
52
- sleepSeconds(options.sleepAfter);
65
+ only("Cloudflare configure", options, ["sleepAfter", "keepAlive"]);
66
+ validateSandboxConfiguration(options);
53
67
  const changed = (options.sleepAfter !== undefined && options.sleepAfter !== this.options.sleepAfter) ||
54
68
  (options.keepAlive !== undefined && options.keepAlive !== this.options.keepAlive);
55
- Object.assign(this.options, options);
69
+ const next = { ...this.options, ...options };
56
70
  if (changed && this.pending) {
57
71
  const s = await this.pending;
58
72
  await s.update({
59
- idlePauseSeconds: this.options.keepAlive ? 0 : sleepSeconds(this.options.sleepAfter),
73
+ idlePauseSeconds: next.keepAlive ? 0 : sleepSeconds(next.sleepAfter),
60
74
  ...(options.keepAlive !== undefined ? { persistent: options.keepAlive } : {}),
61
75
  });
62
76
  }
77
+ Object.assign(this.options, options);
78
+ }
79
+ /** Only a successfully initialized native handle can supply a recovery id. */
80
+ existingNativeId() {
81
+ return this.existing?.id;
82
+ }
83
+ /** Reconcile an uncertain update from an authoritative read, without allocation. */
84
+ async reconcileConfiguration(nativeId = this.existingNativeId()) {
85
+ if (!nativeId)
86
+ throw new Error("The uncertain Runtime configuration has no existing sandbox identity.");
87
+ const sandbox = await this.runtime.sandboxes.get(nativeId);
88
+ const { idlePauseSeconds, persistent } = sandbox.info;
89
+ if (typeof idlePauseSeconds !== "number" ||
90
+ !Number.isSafeInteger(idlePauseSeconds) ||
91
+ idlePauseSeconds < 0 ||
92
+ idlePauseSeconds > 86_400 ||
93
+ typeof persistent !== "boolean")
94
+ throw new Error("Runtime did not return authoritative idle and persistence settings.");
95
+ const configuration = { sleepAfter: idlePauseSeconds, keepAlive: persistent };
96
+ Object.assign(this.options, configuration);
97
+ this.existing = sandbox;
98
+ this.pending = Promise.resolve(sandbox);
99
+ return configuration;
63
100
  }
64
101
  async native() {
65
102
  if (!this.pending) {
@@ -82,6 +119,7 @@ export class Sandbox {
82
119
  throw e;
83
120
  }
84
121
  }
122
+ this.existing = s;
85
123
  return s;
86
124
  })();
87
125
  this.pending.catch(() => {
@@ -14,12 +14,16 @@ type Configuration = {
14
14
  name: string;
15
15
  normalizeId?: boolean;
16
16
  };
17
+ nativeSandboxId?: string;
17
18
  sleepAfter?: string | number;
18
19
  keepAlive?: boolean;
19
20
  };
20
21
  export declare class Sandbox<Env extends RuntimeWorkerEnv = RuntimeWorkerEnv> extends DurableObject<Env> {
21
22
  private configuration;
23
+ private configurationIntent?;
22
24
  private facade?;
25
+ private configurationTail;
26
+ private configurationOperation;
23
27
  private previewTail;
24
28
  private previewOperation;
25
29
  private readonly initialized;
@@ -4,8 +4,47 @@
4
4
  import { DurableObject } from "cloudflare:workers";
5
5
  import { only, CompatibilityError } from "../compat/core.js";
6
6
  import { Runtime } from "../client.js";
7
- import { Sandbox as RuntimeSandbox, sanitizeSandboxId, } from "./index.js";
7
+ import { workerSandboxIdentity } from "./identity.js";
8
+ import { Sandbox as RuntimeSandbox, sanitizeSandboxId, validateSandboxConfiguration, } from "./index.js";
8
9
  export * from "./index.js";
10
+ const CONFIGURATION_KEY = "runtime.configuration";
11
+ const CONFIGURATION_INTENT_KEY = "runtime.configuration.pending";
12
+ /** Preserve the original failure even when durable recovery also fails. */
13
+ function configurationFailure(error, uncertain, cleanup) {
14
+ if (error !== null && (typeof error === "object" || typeof error === "function")) {
15
+ try {
16
+ if (uncertain)
17
+ Object.defineProperty(error, "configurationUncertain", { value: true, configurable: true });
18
+ if (cleanup !== undefined)
19
+ Object.defineProperty(error, "cause" in error && error.cause !== undefined ? "cleanupError" : "cause", {
20
+ value: cleanup,
21
+ configurable: true,
22
+ });
23
+ }
24
+ catch {
25
+ // A frozen exception still keeps its original identity and type.
26
+ }
27
+ }
28
+ return error;
29
+ }
30
+ /** Queue waits share the caller's existing signal and never reset its deadline. */
31
+ async function waitForConfiguration(pending, signal) {
32
+ signal?.throwIfAborted();
33
+ if (!signal)
34
+ return pending;
35
+ let cancel;
36
+ const aborted = new Promise((_resolve, reject) => {
37
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
38
+ cancel = () => reject(signal.reason);
39
+ signal.addEventListener("abort", cancel, { once: true });
40
+ });
41
+ try {
42
+ return await Promise.race([pending, aborted]);
43
+ }
44
+ finally {
45
+ signal.removeEventListener("abort", cancel);
46
+ }
47
+ }
9
48
  /** The official Worker SDK returns plain records with callable RPC properties,
10
49
  * so ids/status are values rather than remote-property promises. */
11
50
  function sessionTarget(session) {
@@ -48,7 +87,14 @@ function processTarget(process) {
48
87
  }
49
88
  export class Sandbox extends DurableObject {
50
89
  configuration = {};
90
+ configurationIntent;
51
91
  facade;
92
+ configurationTail = Promise.resolve();
93
+ configurationOperation(operation) {
94
+ const result = this.configurationTail.catch(() => undefined).then(operation);
95
+ this.configurationTail = result;
96
+ return result;
97
+ }
52
98
  previewTail = Promise.resolve();
53
99
  previewOperation(operation) {
54
100
  const result = this.previewTail.catch(() => undefined).then(operation);
@@ -59,29 +105,133 @@ export class Sandbox extends DurableObject {
59
105
  constructor(ctx, env) {
60
106
  super(ctx, env);
61
107
  this.initialized = ctx.blockConcurrencyWhile(async () => {
62
- this.configuration = (await ctx.storage.get("runtime.configuration")) ?? {};
108
+ this.configuration = (await ctx.storage.get(CONFIGURATION_KEY)) ?? {};
109
+ this.configurationIntent =
110
+ await ctx.storage.get(CONFIGURATION_INTENT_KEY);
63
111
  });
64
112
  }
65
- async sandbox() {
66
- await this.initialized;
113
+ async sandbox(signal) {
114
+ await waitForConfiguration(this.initialized, signal);
115
+ // A staged intent is transient while an update is still running. Wait for
116
+ // the queued update before deciding whether uncertainty was retained.
117
+ let configurationTail;
118
+ do {
119
+ configurationTail = this.configurationTail;
120
+ await waitForConfiguration(configurationTail.catch(() => undefined), signal);
121
+ } while (configurationTail !== this.configurationTail);
122
+ signal?.throwIfAborted();
123
+ if (this.configurationIntent)
124
+ throw configurationFailure(new Error("Runtime's configuration is uncertain. Call configure() to reconcile it before use."), true);
67
125
  if (!this.configuration.sandboxName)
68
126
  throw new Error("Call getSandbox(namespace, id) before using the Durable Object");
69
127
  if (!this.env.RUNTIME_API_KEY?.startsWith("rtcloud_"))
70
128
  throw new Error("Set the RUNTIME_API_KEY Worker secret to a Runtime API key");
71
- this.facade ??= new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), this.configuration.sandboxName.name, this.configuration);
129
+ this.facade ??= new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), workerSandboxIdentity(this.ctx.id.toString(), this.configuration), { ...this.configuration });
72
130
  return this.facade;
73
131
  }
74
132
  async configure(configuration) {
75
133
  only("Cloudflare Worker configure", configuration, ["sandboxName", "sleepAfter", "keepAlive"]);
76
- await this.initialized;
77
- if (this.configuration.sandboxName &&
78
- configuration.sandboxName &&
79
- this.configuration.sandboxName.name !== configuration.sandboxName.name)
80
- throw new Error("A Durable Object cannot change sandbox identity");
81
- this.configuration = { ...this.configuration, ...configuration };
82
- await this.ctx.storage.put("runtime.configuration", this.configuration);
83
- if (this.facade)
84
- await this.facade.configure(configuration);
134
+ validateSandboxConfiguration(configuration);
135
+ if (configuration.sandboxName !== undefined) {
136
+ only("Cloudflare Worker sandboxName", configuration.sandboxName, ["name", "normalizeId"]);
137
+ sanitizeSandboxId(configuration.sandboxName.name);
138
+ if (configuration.sandboxName.normalizeId !== undefined &&
139
+ typeof configuration.sandboxName.normalizeId !== "boolean")
140
+ throw new TypeError("normalizeId must be a boolean");
141
+ }
142
+ const requested = {
143
+ ...(configuration.sandboxName === undefined
144
+ ? {}
145
+ : { sandboxName: { ...configuration.sandboxName } }),
146
+ ...(configuration.sleepAfter === undefined ? {} : { sleepAfter: configuration.sleepAfter }),
147
+ ...(configuration.keepAlive === undefined ? {} : { keepAlive: configuration.keepAlive }),
148
+ };
149
+ return this.configurationOperation(async () => {
150
+ await this.initialized;
151
+ if (this.configuration.sandboxName &&
152
+ requested.sandboxName &&
153
+ this.configuration.sandboxName.name !== requested.sandboxName.name)
154
+ throw new Error("A Durable Object cannot change sandbox identity");
155
+ if (this.configurationIntent) {
156
+ const facade = this.facade ??
157
+ new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), workerSandboxIdentity(this.ctx.id.toString(), this.configuration), { ...this.configuration });
158
+ try {
159
+ // Recovery uses the saved UUID even after a Durable Object restart.
160
+ const actual = await facade.reconcileConfiguration(this.configurationIntent.sandboxId);
161
+ const reconciled = { ...this.configuration, ...actual };
162
+ await this.ctx.storage.put(CONFIGURATION_KEY, reconciled);
163
+ await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
164
+ this.configuration = reconciled;
165
+ this.facade = facade;
166
+ this.configurationIntent = undefined;
167
+ }
168
+ catch (error) {
169
+ throw configurationFailure(error, true);
170
+ }
171
+ }
172
+ const previous = this.configuration;
173
+ const stored = await this.ctx.storage.get(CONFIGURATION_KEY);
174
+ const nativeSandboxId = workerSandboxIdentity(this.ctx.id.toString(), previous);
175
+ const next = { ...previous, ...requested, nativeSandboxId };
176
+ if (!this.facade) {
177
+ await this.ctx.storage.put(CONFIGURATION_KEY, next);
178
+ this.configuration = next;
179
+ return;
180
+ }
181
+ this.configurationIntent = { sandboxId: this.facade.existingNativeId(), previous, next };
182
+ try {
183
+ await this.ctx.storage.put(CONFIGURATION_INTENT_KEY, this.configurationIntent);
184
+ }
185
+ catch (error) {
186
+ let cleanup;
187
+ try {
188
+ // Native mutation has not started. A failed write may still have
189
+ // persisted the intent, so clear this operation's staging record.
190
+ await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
191
+ this.configurationIntent = undefined;
192
+ }
193
+ catch (failure) {
194
+ cleanup = failure;
195
+ }
196
+ throw configurationFailure(error, this.configurationIntent !== undefined, cleanup);
197
+ }
198
+ let nativeAttempted = false;
199
+ try {
200
+ await this.ctx.storage.put(CONFIGURATION_KEY, next);
201
+ nativeAttempted = true;
202
+ await this.facade.configure({
203
+ ...(requested.sleepAfter === undefined ? {} : { sleepAfter: requested.sleepAfter }),
204
+ ...(requested.keepAlive === undefined ? {} : { keepAlive: requested.keepAlive }),
205
+ });
206
+ }
207
+ catch (error) {
208
+ let cleanup;
209
+ try {
210
+ if (stored === undefined)
211
+ await this.ctx.storage.delete(CONFIGURATION_KEY);
212
+ else
213
+ await this.ctx.storage.put(CONFIGURATION_KEY, stored);
214
+ if (!nativeAttempted) {
215
+ await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
216
+ this.configurationIntent = undefined;
217
+ }
218
+ }
219
+ catch (failure) {
220
+ cleanup = failure;
221
+ }
222
+ // A rejected native request may have applied remotely. Keep its intent
223
+ // until an authoritative read succeeds; rollback is only local state.
224
+ throw configurationFailure(error, this.configurationIntent !== undefined, cleanup);
225
+ }
226
+ this.configuration = next;
227
+ try {
228
+ await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
229
+ this.configurationIntent = undefined;
230
+ }
231
+ catch (error) {
232
+ throw configurationFailure(error, true);
233
+ }
234
+ });
85
235
  }
86
236
  setSandboxName(name, normalizeId = false) {
87
237
  return this.configure({
@@ -95,10 +245,10 @@ export class Sandbox extends DurableObject {
95
245
  return this.configure({ keepAlive });
96
246
  }
97
247
  async exec(command, options) {
98
- return (await this.sandbox()).exec(command, options);
248
+ return (await this.sandbox(options?.signal)).exec(command, options);
99
249
  }
100
250
  async execStream(command, options) {
101
- return (await this.sandbox()).execStream(command, options);
251
+ return (await this.sandbox(options?.signal)).execStream(command, options);
102
252
  }
103
253
  async startProcess(command, options) {
104
254
  return processTarget(await (await this.sandbox()).startProcess(command, options));