@daytona/sdk 0.199.0-alpha.1 → 0.200.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/esm/Sandbox.js CHANGED
@@ -4,8 +4,9 @@
4
4
  */
5
5
  import { __esDecorate, __runInitializers } from "tslib";
6
6
  import { SandboxState } from '@daytona/api-client';
7
+ import { TelemetryApi as AnalyticsTelemetryApi, Configuration as AnalyticsConfiguration, } from '@daytona/analytics-api-client';
7
8
  import { Daytona } from './Daytona.js';
8
- import { FileSystemApi, GitApi, ProcessApi, LspApi, InfoApi, ComputerUseApi, InterpreterApi, ServerApi, } from '@daytona/toolbox-api-client';
9
+ import { FileSystemApi, GitApi, ProcessApi, LspApi, InfoApi, SystemApi, ComputerUseApi, InterpreterApi, ServerApi, } from '@daytona/toolbox-api-client';
9
10
  import { FileSystem } from './FileSystem.js';
10
11
  import { Git } from './Git.js';
11
12
  import { Process } from './Process.js';
@@ -15,35 +16,12 @@ import { CODE_TOOLBOX_LANGUAGE_LABEL } from './Daytona.js';
15
16
  import { ComputerUse } from './ComputerUse.js';
16
17
  import { CodeInterpreter } from './CodeInterpreter.js';
17
18
  import { WithInstrumentation } from './utils/otel.decorator.js';
18
- import { VolumeType } from '@daytona/api-client';
19
- /**
20
- * Builds the shell command that bootstraps the hotmount agent and mounts the volume inside a
21
- * Sandbox. It exports the SEAWEED_* environment contract and runs the region's `init.sh`, using
22
- * passwordless sudo when not already root (sudo strips env, so the vars are passed via `env`).
23
- */
24
- function buildHotmountMountCommand(mountToken, mountPath) {
25
- const envVars = {
26
- SEAWEED_TOKEN: mountToken.token,
27
- SEAWEED_GATEWAY_GRPC: mountToken.gatewayGrpc,
28
- SEAWEED_GATEWAY_HTTP: mountToken.gatewayHttp,
29
- SEAWEED_BINARIES_URL: mountToken.binariesUrl,
30
- SEAWEED_MOUNT_DIR: mountPath,
31
- SEAWEED_VERSION: mountToken.version,
19
+ function withEvents(target, _context) {
20
+ return function (...args) {
21
+ ;
22
+ this.ensureSubscribed();
23
+ return target.apply(this, args);
32
24
  };
33
- const envAssignments = Object.entries(envVars)
34
- .filter(([, value]) => value !== undefined && value !== '')
35
- .map(([key, value]) => `${key}='${value}'`)
36
- .join(' ');
37
- // The bootstrap (and init.sh itself) requires curl. Fail loudly if it is missing or the
38
- // download fails, rather than letting a broken `curl … | bash` pipe exit 0 and mount nothing.
39
- const inner = [
40
- 'set -e',
41
- 'if ! command -v curl >/dev/null 2>&1; then echo "hotmount: curl is required to bootstrap the agent but was not found in the sandbox" >&2; exit 1; fi',
42
- 'mkdir -p "$SEAWEED_MOUNT_DIR"',
43
- 'init_script="$(curl -fsSL "$SEAWEED_BINARIES_URL/init.sh")"',
44
- 'printf %s "$init_script" | bash',
45
- ].join('; ');
46
- return (`if [ "$(id -u)" != 0 ]; then SUDO="sudo -n"; else SUDO=""; fi; ` + `$SUDO env ${envAssignments} bash -c '${inner}'`);
47
25
  }
48
26
  /**
49
27
  * Represents a Daytona Sandbox.
@@ -74,8 +52,10 @@ function buildHotmountMountCommand(mountToken, mountPath) {
74
52
  * @property {string} [backupCreatedAt] - When the backup was created (not returned by list results;
75
53
  * call `refreshData()` on each item to populate)
76
54
  * @property {number} [autoStopInterval] - Auto-stop interval in minutes
55
+ * @property {number} [autoPauseInterval] - Auto-pause interval in minutes
77
56
  * @property {number} [autoArchiveInterval] - Auto-archive interval in minutes
78
57
  * @property {number} [autoDeleteInterval] - Auto-delete interval in minutes
58
+ * @property {string} [autoDestroyAt] - When the Sandbox will be automatically destroyed (only set when a TTL is configured)
79
59
  * @property {Array<SandboxVolume>} [volumes] - Volumes attached to the Sandbox (not returned by
80
60
  * list results; call `refreshData()` on each item to populate)
81
61
  * @property {BuildInfo} [buildInfo] - Build information for the Sandbox if it was created from dynamic build
@@ -97,13 +77,14 @@ function buildHotmountMountCommand(mountToken, mountPath) {
97
77
  let Sandbox = (() => {
98
78
  let _instanceExtraInitializers = [];
99
79
  let _getUserHomeDir_decorators;
80
+ let _getMetricsLatest_decorators;
81
+ let _getMetrics_decorators;
100
82
  let _getUserRootDir_decorators;
101
83
  let _getWorkDir_decorators;
102
- let _mountVolume_decorators;
103
- let _pullVolumes_decorators;
104
84
  let _createLspServer_decorators;
105
85
  let _setLabels_decorators;
106
86
  let _start_decorators;
87
+ let _recover_decorators;
107
88
  let _stop_decorators;
108
89
  let __experimental_fork_decorators;
109
90
  let __experimental_createSnapshot_decorators;
@@ -112,13 +93,18 @@ let Sandbox = (() => {
112
93
  let _waitUntilStarted_decorators;
113
94
  let _waitUntilStopped_decorators;
114
95
  let _refreshData_decorators;
96
+ let _refreshActivity_decorators;
115
97
  let _setAutostopInterval_decorators;
98
+ let _setAutoPauseInterval_decorators;
99
+ let _setTtl_decorators;
116
100
  let _setAutoArchiveInterval_decorators;
117
101
  let _setAutoDeleteInterval_decorators;
118
102
  let _updateNetworkSettings_decorators;
119
103
  let _updateSecrets_decorators;
120
104
  let _updateEnv_decorators;
121
105
  let _getPreviewLink_decorators;
106
+ let _getSignedPreviewUrl_decorators;
107
+ let _expireSignedPreviewUrl_decorators;
122
108
  let _archive_decorators;
123
109
  let _resize_decorators;
124
110
  let _waitForResizeComplete_decorators;
@@ -128,43 +114,50 @@ let Sandbox = (() => {
128
114
  return class Sandbox {
129
115
  static {
130
116
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
131
- _getUserHomeDir_decorators = [WithInstrumentation()];
132
- _getUserRootDir_decorators = [WithInstrumentation()];
133
- _getWorkDir_decorators = [WithInstrumentation()];
134
- _mountVolume_decorators = [WithInstrumentation()];
135
- _pullVolumes_decorators = [WithInstrumentation()];
136
- _createLspServer_decorators = [WithInstrumentation()];
137
- _setLabels_decorators = [WithInstrumentation()];
138
- _start_decorators = [WithInstrumentation()];
139
- _stop_decorators = [WithInstrumentation()];
140
- __experimental_fork_decorators = [WithInstrumentation()];
141
- __experimental_createSnapshot_decorators = [WithInstrumentation()];
142
- _pause_decorators = [WithInstrumentation()];
143
- _delete_decorators = [WithInstrumentation()];
144
- _waitUntilStarted_decorators = [WithInstrumentation()];
145
- _waitUntilStopped_decorators = [WithInstrumentation()];
146
- _refreshData_decorators = [WithInstrumentation()];
147
- _setAutostopInterval_decorators = [WithInstrumentation()];
148
- _setAutoArchiveInterval_decorators = [WithInstrumentation()];
149
- _setAutoDeleteInterval_decorators = [WithInstrumentation()];
117
+ _getUserHomeDir_decorators = [WithInstrumentation(), withEvents];
118
+ _getMetricsLatest_decorators = [WithInstrumentation()];
119
+ _getMetrics_decorators = [WithInstrumentation()];
120
+ _getUserRootDir_decorators = [WithInstrumentation(), withEvents];
121
+ _getWorkDir_decorators = [WithInstrumentation(), withEvents];
122
+ _createLspServer_decorators = [WithInstrumentation(), withEvents];
123
+ _setLabels_decorators = [WithInstrumentation(), withEvents];
124
+ _start_decorators = [WithInstrumentation(), withEvents];
125
+ _recover_decorators = [withEvents];
126
+ _stop_decorators = [WithInstrumentation(), withEvents];
127
+ __experimental_fork_decorators = [WithInstrumentation(), withEvents];
128
+ __experimental_createSnapshot_decorators = [WithInstrumentation(), withEvents];
129
+ _pause_decorators = [WithInstrumentation(), withEvents];
130
+ _delete_decorators = [WithInstrumentation(), withEvents];
131
+ _waitUntilStarted_decorators = [WithInstrumentation(), withEvents];
132
+ _waitUntilStopped_decorators = [WithInstrumentation(), withEvents];
133
+ _refreshData_decorators = [WithInstrumentation(), withEvents];
134
+ _refreshActivity_decorators = [withEvents];
135
+ _setAutostopInterval_decorators = [WithInstrumentation(), withEvents];
136
+ _setAutoPauseInterval_decorators = [WithInstrumentation()];
137
+ _setTtl_decorators = [WithInstrumentation()];
138
+ _setAutoArchiveInterval_decorators = [WithInstrumentation(), withEvents];
139
+ _setAutoDeleteInterval_decorators = [WithInstrumentation(), withEvents];
150
140
  _updateNetworkSettings_decorators = [WithInstrumentation()];
151
141
  _updateSecrets_decorators = [WithInstrumentation()];
152
142
  _updateEnv_decorators = [WithInstrumentation()];
153
- _getPreviewLink_decorators = [WithInstrumentation()];
154
- _archive_decorators = [WithInstrumentation()];
155
- _resize_decorators = [WithInstrumentation()];
156
- _waitForResizeComplete_decorators = [WithInstrumentation()];
157
- _createSshAccess_decorators = [WithInstrumentation()];
158
- _revokeSshAccess_decorators = [WithInstrumentation()];
159
- _validateSshAccess_decorators = [WithInstrumentation()];
143
+ _getPreviewLink_decorators = [WithInstrumentation(), withEvents];
144
+ _getSignedPreviewUrl_decorators = [withEvents];
145
+ _expireSignedPreviewUrl_decorators = [withEvents];
146
+ _archive_decorators = [WithInstrumentation(), withEvents];
147
+ _resize_decorators = [WithInstrumentation(), withEvents];
148
+ _waitForResizeComplete_decorators = [WithInstrumentation(), withEvents];
149
+ _createSshAccess_decorators = [WithInstrumentation(), withEvents];
150
+ _revokeSshAccess_decorators = [WithInstrumentation(), withEvents];
151
+ _validateSshAccess_decorators = [WithInstrumentation(), withEvents];
160
152
  __esDecorate(this, null, _getUserHomeDir_decorators, { kind: "method", name: "getUserHomeDir", static: false, private: false, access: { has: obj => "getUserHomeDir" in obj, get: obj => obj.getUserHomeDir }, metadata: _metadata }, null, _instanceExtraInitializers);
153
+ __esDecorate(this, null, _getMetricsLatest_decorators, { kind: "method", name: "getMetricsLatest", static: false, private: false, access: { has: obj => "getMetricsLatest" in obj, get: obj => obj.getMetricsLatest }, metadata: _metadata }, null, _instanceExtraInitializers);
154
+ __esDecorate(this, null, _getMetrics_decorators, { kind: "method", name: "getMetrics", static: false, private: false, access: { has: obj => "getMetrics" in obj, get: obj => obj.getMetrics }, metadata: _metadata }, null, _instanceExtraInitializers);
161
155
  __esDecorate(this, null, _getUserRootDir_decorators, { kind: "method", name: "getUserRootDir", static: false, private: false, access: { has: obj => "getUserRootDir" in obj, get: obj => obj.getUserRootDir }, metadata: _metadata }, null, _instanceExtraInitializers);
162
156
  __esDecorate(this, null, _getWorkDir_decorators, { kind: "method", name: "getWorkDir", static: false, private: false, access: { has: obj => "getWorkDir" in obj, get: obj => obj.getWorkDir }, metadata: _metadata }, null, _instanceExtraInitializers);
163
- __esDecorate(this, null, _mountVolume_decorators, { kind: "method", name: "mountVolume", static: false, private: false, access: { has: obj => "mountVolume" in obj, get: obj => obj.mountVolume }, metadata: _metadata }, null, _instanceExtraInitializers);
164
- __esDecorate(this, null, _pullVolumes_decorators, { kind: "method", name: "pullVolumes", static: false, private: false, access: { has: obj => "pullVolumes" in obj, get: obj => obj.pullVolumes }, metadata: _metadata }, null, _instanceExtraInitializers);
165
157
  __esDecorate(this, null, _createLspServer_decorators, { kind: "method", name: "createLspServer", static: false, private: false, access: { has: obj => "createLspServer" in obj, get: obj => obj.createLspServer }, metadata: _metadata }, null, _instanceExtraInitializers);
166
158
  __esDecorate(this, null, _setLabels_decorators, { kind: "method", name: "setLabels", static: false, private: false, access: { has: obj => "setLabels" in obj, get: obj => obj.setLabels }, metadata: _metadata }, null, _instanceExtraInitializers);
167
159
  __esDecorate(this, null, _start_decorators, { kind: "method", name: "start", static: false, private: false, access: { has: obj => "start" in obj, get: obj => obj.start }, metadata: _metadata }, null, _instanceExtraInitializers);
160
+ __esDecorate(this, null, _recover_decorators, { kind: "method", name: "recover", static: false, private: false, access: { has: obj => "recover" in obj, get: obj => obj.recover }, metadata: _metadata }, null, _instanceExtraInitializers);
168
161
  __esDecorate(this, null, _stop_decorators, { kind: "method", name: "stop", static: false, private: false, access: { has: obj => "stop" in obj, get: obj => obj.stop }, metadata: _metadata }, null, _instanceExtraInitializers);
169
162
  __esDecorate(this, null, __experimental_fork_decorators, { kind: "method", name: "_experimental_fork", static: false, private: false, access: { has: obj => "_experimental_fork" in obj, get: obj => obj._experimental_fork }, metadata: _metadata }, null, _instanceExtraInitializers);
170
163
  __esDecorate(this, null, __experimental_createSnapshot_decorators, { kind: "method", name: "_experimental_createSnapshot", static: false, private: false, access: { has: obj => "_experimental_createSnapshot" in obj, get: obj => obj._experimental_createSnapshot }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -173,13 +166,18 @@ let Sandbox = (() => {
173
166
  __esDecorate(this, null, _waitUntilStarted_decorators, { kind: "method", name: "waitUntilStarted", static: false, private: false, access: { has: obj => "waitUntilStarted" in obj, get: obj => obj.waitUntilStarted }, metadata: _metadata }, null, _instanceExtraInitializers);
174
167
  __esDecorate(this, null, _waitUntilStopped_decorators, { kind: "method", name: "waitUntilStopped", static: false, private: false, access: { has: obj => "waitUntilStopped" in obj, get: obj => obj.waitUntilStopped }, metadata: _metadata }, null, _instanceExtraInitializers);
175
168
  __esDecorate(this, null, _refreshData_decorators, { kind: "method", name: "refreshData", static: false, private: false, access: { has: obj => "refreshData" in obj, get: obj => obj.refreshData }, metadata: _metadata }, null, _instanceExtraInitializers);
169
+ __esDecorate(this, null, _refreshActivity_decorators, { kind: "method", name: "refreshActivity", static: false, private: false, access: { has: obj => "refreshActivity" in obj, get: obj => obj.refreshActivity }, metadata: _metadata }, null, _instanceExtraInitializers);
176
170
  __esDecorate(this, null, _setAutostopInterval_decorators, { kind: "method", name: "setAutostopInterval", static: false, private: false, access: { has: obj => "setAutostopInterval" in obj, get: obj => obj.setAutostopInterval }, metadata: _metadata }, null, _instanceExtraInitializers);
171
+ __esDecorate(this, null, _setAutoPauseInterval_decorators, { kind: "method", name: "setAutoPauseInterval", static: false, private: false, access: { has: obj => "setAutoPauseInterval" in obj, get: obj => obj.setAutoPauseInterval }, metadata: _metadata }, null, _instanceExtraInitializers);
172
+ __esDecorate(this, null, _setTtl_decorators, { kind: "method", name: "setTtl", static: false, private: false, access: { has: obj => "setTtl" in obj, get: obj => obj.setTtl }, metadata: _metadata }, null, _instanceExtraInitializers);
177
173
  __esDecorate(this, null, _setAutoArchiveInterval_decorators, { kind: "method", name: "setAutoArchiveInterval", static: false, private: false, access: { has: obj => "setAutoArchiveInterval" in obj, get: obj => obj.setAutoArchiveInterval }, metadata: _metadata }, null, _instanceExtraInitializers);
178
174
  __esDecorate(this, null, _setAutoDeleteInterval_decorators, { kind: "method", name: "setAutoDeleteInterval", static: false, private: false, access: { has: obj => "setAutoDeleteInterval" in obj, get: obj => obj.setAutoDeleteInterval }, metadata: _metadata }, null, _instanceExtraInitializers);
179
175
  __esDecorate(this, null, _updateNetworkSettings_decorators, { kind: "method", name: "updateNetworkSettings", static: false, private: false, access: { has: obj => "updateNetworkSettings" in obj, get: obj => obj.updateNetworkSettings }, metadata: _metadata }, null, _instanceExtraInitializers);
180
176
  __esDecorate(this, null, _updateSecrets_decorators, { kind: "method", name: "updateSecrets", static: false, private: false, access: { has: obj => "updateSecrets" in obj, get: obj => obj.updateSecrets }, metadata: _metadata }, null, _instanceExtraInitializers);
181
177
  __esDecorate(this, null, _updateEnv_decorators, { kind: "method", name: "updateEnv", static: false, private: false, access: { has: obj => "updateEnv" in obj, get: obj => obj.updateEnv }, metadata: _metadata }, null, _instanceExtraInitializers);
182
178
  __esDecorate(this, null, _getPreviewLink_decorators, { kind: "method", name: "getPreviewLink", static: false, private: false, access: { has: obj => "getPreviewLink" in obj, get: obj => obj.getPreviewLink }, metadata: _metadata }, null, _instanceExtraInitializers);
179
+ __esDecorate(this, null, _getSignedPreviewUrl_decorators, { kind: "method", name: "getSignedPreviewUrl", static: false, private: false, access: { has: obj => "getSignedPreviewUrl" in obj, get: obj => obj.getSignedPreviewUrl }, metadata: _metadata }, null, _instanceExtraInitializers);
180
+ __esDecorate(this, null, _expireSignedPreviewUrl_decorators, { kind: "method", name: "expireSignedPreviewUrl", static: false, private: false, access: { has: obj => "expireSignedPreviewUrl" in obj, get: obj => obj.expireSignedPreviewUrl }, metadata: _metadata }, null, _instanceExtraInitializers);
183
181
  __esDecorate(this, null, _archive_decorators, { kind: "method", name: "archive", static: false, private: false, access: { has: obj => "archive" in obj, get: obj => obj.archive }, metadata: _metadata }, null, _instanceExtraInitializers);
184
182
  __esDecorate(this, null, _resize_decorators, { kind: "method", name: "resize", static: false, private: false, access: { has: obj => "resize" in obj, get: obj => obj.resize }, metadata: _metadata }, null, _instanceExtraInitializers);
185
183
  __esDecorate(this, null, _waitForResizeComplete_decorators, { kind: "method", name: "waitForResizeComplete", static: false, private: false, access: { has: obj => "waitForResizeComplete" in obj, get: obj => obj.waitForResizeComplete }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -191,7 +189,8 @@ let Sandbox = (() => {
191
189
  clientConfig = __runInitializers(this, _instanceExtraInitializers);
192
190
  axiosInstance;
193
191
  sandboxApi;
194
- volumeService;
192
+ getAnalyticsApiUrl;
193
+ subscriptionManager;
195
194
  fs;
196
195
  git;
197
196
  process;
@@ -216,8 +215,10 @@ let Sandbox = (() => {
216
215
  backupState;
217
216
  backupCreatedAt;
218
217
  autoStopInterval;
218
+ autoPauseInterval;
219
219
  autoArchiveInterval;
220
220
  autoDeleteInterval;
221
+ autoDestroyAt;
221
222
  volumes;
222
223
  buildInfo;
223
224
  createdAt;
@@ -230,25 +231,28 @@ let Sandbox = (() => {
230
231
  toolboxProxyUrl;
231
232
  infoApi;
232
233
  serverApi;
234
+ systemApi;
235
+ stateWaiters = [];
236
+ stateWaiterErrorMessageFns = new WeakMap();
237
+ subId;
233
238
  /**
234
- * Creates a new Sandbox instance
239
+ * Creates a new Sandbox instance.
240
+ *
241
+ * Internal: obtain sandboxes via {@link Daytona.create}, {@link Daytona.get}, or
242
+ * {@link Daytona.list} rather than constructing directly.
235
243
  *
236
244
  * @param {SandboxDto} sandboxDto - The API Sandbox instance
245
+ * @param {SandboxApi} sandboxApi - API client for Sandbox operations
246
+ * @param {InfoApi} infoApi - API client for info operations
247
+ * @param {EventSubscriptionManager} subscriptionManager - Event subscription manager for real-time updates
237
248
  */
238
- constructor(sandboxDto, clientConfig, axiosInstance, sandboxApi, volumeService) {
249
+ constructor(sandboxDto, clientConfig, axiosInstance, sandboxApi, getAnalyticsApiUrl, subscriptionManager) {
239
250
  this.clientConfig = clientConfig;
240
251
  this.axiosInstance = axiosInstance;
241
252
  this.sandboxApi = sandboxApi;
242
- this.volumeService = volumeService;
253
+ this.getAnalyticsApiUrl = getAnalyticsApiUrl;
254
+ this.subscriptionManager = subscriptionManager;
243
255
  this.processSandboxDto(sandboxDto);
244
- // Set the toolbox base URL
245
- let baseUrl = this.toolboxProxyUrl;
246
- if (!baseUrl.endsWith('/')) {
247
- baseUrl += '/';
248
- }
249
- this.axiosInstance.defaults.baseURL = baseUrl + this.id;
250
- this.clientConfig.basePath = this.axiosInstance.defaults.baseURL;
251
- // Initialize Services
252
256
  const getPreviewToken = async () => (await this.getPreviewLink(1)).token;
253
257
  this.fs = new FileSystem(this.clientConfig, new FileSystemApi(this.clientConfig, '', this.axiosInstance));
254
258
  this.git = new Git(new GitApi(this.clientConfig, '', this.axiosInstance));
@@ -258,6 +262,8 @@ let Sandbox = (() => {
258
262
  this.computerUse = new ComputerUse(new ComputerUseApi(this.clientConfig, '', this.axiosInstance));
259
263
  this.infoApi = new InfoApi(this.clientConfig, '', this.axiosInstance);
260
264
  this.serverApi = new ServerApi(this.clientConfig, '', this.axiosInstance);
265
+ this.systemApi = new SystemApi(this.clientConfig, '', this.axiosInstance);
266
+ this.subscribeToEvents();
261
267
  }
262
268
  /**
263
269
  * Gets the user's home directory path for the logged in user inside the Sandbox.
@@ -272,6 +278,53 @@ let Sandbox = (() => {
272
278
  const response = await this.infoApi.getUserHomeDir();
273
279
  return response.data.dir;
274
280
  }
281
+ /**
282
+ * Gets the most recent resource usage sample directly from the sandbox daemon.
283
+ *
284
+ * Unlike {@link getMetrics}, which returns aggregated historical samples, this returns the
285
+ * single current reading without going through the telemetry backend.
286
+ *
287
+ * @returns The current resource usage sample for the sandbox.
288
+ *
289
+ * @example
290
+ * const m = await sandbox.getMetricsLatest()
291
+ * console.log(`CPU: ${m.cpuUsedPct}%, mem: ${m.memUsed}/${m.memTotal}`)
292
+ */
293
+ async getMetricsLatest() {
294
+ const response = await this.systemApi.getSystemMetrics();
295
+ return sandboxMetricsFromSystemMetrics(response.data);
296
+ }
297
+ /**
298
+ * Gets historical time-series resource usage metrics for the Sandbox.
299
+ *
300
+ * @param {Date} [start] - Start of the time range. Defaults to the Sandbox creation time.
301
+ * @param {Date} [end] - End of the time range. Defaults to the current time.
302
+ * @returns Time-ordered usage samples over the requested range.
303
+ *
304
+ * @example
305
+ * const samples = await sandbox.getMetrics()
306
+ * for (const s of samples) {
307
+ * console.log(`${s.timestamp.toISOString()} CPU: ${s.cpuUsedPct}% mem: ${s.memUsed}/${s.memTotal}`)
308
+ * }
309
+ */
310
+ async getMetrics(start, end) {
311
+ const to = end ?? new Date();
312
+ const from = start ?? (this.createdAt ? new Date(this.createdAt) : to);
313
+ const analyticsApiUrl = await this.getAnalyticsApiUrl();
314
+ if (analyticsApiUrl) {
315
+ const response = await this.buildAnalyticsTelemetryApi(analyticsApiUrl).organizationOrganizationIdSandboxSandboxIdTelemetryMetricsGet(this.organizationId, this.id, from.toISOString(), to.toISOString(), SANDBOX_METRIC_NAMES.join(','));
316
+ return pivotSandboxMetricPoints(response.data);
317
+ }
318
+ const response = await this.sandboxApi.getSandboxMetrics(this.id, from, to, undefined, SANDBOX_METRIC_NAMES);
319
+ return pivotSandboxMetrics(response.data.series);
320
+ }
321
+ buildAnalyticsTelemetryApi(analyticsApiUrl) {
322
+ const analyticsConfig = new AnalyticsConfiguration({
323
+ basePath: analyticsApiUrl,
324
+ apiKey: this.clientConfig.baseOptions?.headers?.Authorization,
325
+ });
326
+ return new AnalyticsTelemetryApi(analyticsConfig, undefined, Daytona.createAxiosInstance());
327
+ }
275
328
  /**
276
329
  * @deprecated Use `getUserHomeDir` instead. This method will be removed in a future version.
277
330
  */
@@ -292,71 +345,6 @@ let Sandbox = (() => {
292
345
  const response = await this.infoApi.getWorkDir();
293
346
  return response.data.dir;
294
347
  }
295
- /**
296
- * Mounts a hotmount Volume into the running Sandbox on the fly.
297
- *
298
- * Unlike legacy and blockmount volumes (which are attached at Sandbox creation), hotmount
299
- * volumes are mounted at runtime: this method requests a short-lived mount token from the API
300
- * and bootstraps the hotmount agent inside the Sandbox (downloading and running the region's
301
- * `init.sh`), mounting the filesystem at the given path.
302
- *
303
- * The Sandbox must have `/dev/fuse` available, outbound access to the region gateway (ports
304
- * 443 and 18443) and binaries bucket, and passwordless sudo (or run as root).
305
- *
306
- * @param {Volume} volume - The hotmount Volume to mount. Must be of type `VolumeType.HOTMOUNT`.
307
- * @param {string} mountPath - The absolute path inside the Sandbox to mount the Volume at
308
- * @returns {Promise<void>}
309
- * @throws {DaytonaError} If the Volume is not a hotmount volume, or if mounting fails
310
- *
311
- * @example
312
- * const volume = await daytona.volume.get("shared-fs");
313
- * await sandbox.mountVolume(volume, "/mnt/shared");
314
- */
315
- async mountVolume(volume, mountPath) {
316
- if (volume.type !== VolumeType.HOTMOUNT) {
317
- throw new DaytonaValidationError(`Only hotmount volumes can be mounted on the fly. Volume '${volume.name}' is of type '${volume.type}'.`);
318
- }
319
- if (!this.volumeService) {
320
- throw new DaytonaError('Volume service is not available for this Sandbox instance.');
321
- }
322
- const mountToken = await this.volumeService.getMountToken(volume);
323
- const command = buildHotmountMountCommand(mountToken, mountPath);
324
- const response = await this.process.executeCommand(command);
325
- if (response.exitCode !== 0) {
326
- throw new DaytonaError(`Failed to mount hotmount volume '${volume.name}': ${response.result}`);
327
- }
328
- }
329
- /**
330
- * Pulls the latest state of the Sandbox's blockmount Volumes into the running Sandbox.
331
- *
332
- * Blockmount volumes reconcile in the background: each sandbox writes to a private scratch
333
- * that is committed to the shared store periodically, but other sandboxes' commits only
334
- * appear locally on re-materialize. This method makes them appear immediately, without
335
- * stopping the Sandbox: it commits this Sandbox's local changes (so they participate in the
336
- * merge), then applies the volume's latest merged state in place. Files modified locally
337
- * after another sandbox's change keep the local version (last-change-wins by mtime).
338
- *
339
- * @param {Volume} [volume] - The blockmount Volume to pull. Omit to pull every blockmount
340
- * Volume attached to the Sandbox.
341
- * @returns {Promise<VolumePullResult[]>} Per-volume pull results
342
- * @throws {DaytonaError} If the Sandbox has no matching mounted blockmount Volume
343
- *
344
- * @example
345
- * // sandbox B picks up what sandbox A committed, while both keep running
346
- * const results = await sandbox.pullVolumes();
347
- * console.log(results); // [{ volumeId, upToDate, filesWritten, deleted, ... }]
348
- */
349
- async pullVolumes(volume) {
350
- if (volume && volume.type !== VolumeType.BLOCKMOUNT) {
351
- throw new DaytonaValidationError(`Only blockmount volumes can be pulled. Volume '${volume.name}' is of type '${volume.type}'.`);
352
- }
353
- // Raw toolbox call (the daemon's generated client has no pull op — the runner intercepts
354
- // this path), so replicate what the generated clients do: resolve against this sandbox's
355
- // clientConfig.basePath (the shared axios instance's default baseURL is mutated by every
356
- // Sandbox construction and may point at another sandbox) and inject the auth headers.
357
- const response = await this.axiosInstance.post(`${this.clientConfig.basePath}/volumes/pull`, volume ? { volumeId: volume.id } : {}, { headers: this.clientConfig.baseOptions?.headers });
358
- return response.data.results ?? [];
359
- }
360
348
  /**
361
349
  * Creates a new Language Server Protocol (LSP) server instance.
362
350
  *
@@ -494,9 +482,16 @@ let Sandbox = (() => {
494
482
  timeout: timeout * 1000,
495
483
  });
496
484
  const sandboxDto = response.data;
497
- const forkedSandbox = new Sandbox(sandboxDto, structuredClone(this.clientConfig), Daytona.createAxiosInstance(), this.sandboxApi);
485
+ const sandboxWithProxyUrl = sandboxDto.toolboxProxyUrl
486
+ ? sandboxDto
487
+ : {
488
+ ...sandboxDto,
489
+ toolboxProxyUrl: (await this.sandboxApi.getToolboxProxyUrl(sandboxDto.id)).data.url,
490
+ };
491
+ const forkedSandbox = new Sandbox(sandboxWithProxyUrl, structuredClone(this.clientConfig), Daytona.createAxiosInstance(), this.sandboxApi, this.getAnalyticsApiUrl, this.subscriptionManager);
498
492
  const timeElapsed = Date.now() - startTime;
499
- await forkedSandbox.waitUntilStarted(timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout);
493
+ const remainingTimeout = timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout;
494
+ await forkedSandbox.waitUntilStarted(remainingTimeout);
500
495
  return forkedSandbox;
501
496
  }
502
497
  /**
@@ -524,34 +519,19 @@ let Sandbox = (() => {
524
519
  }
525
520
  const startTime = Date.now();
526
521
  const req = { name };
527
- await this.sandboxApi.createSandboxSnapshot(this.id, req, undefined, {
522
+ const response = await this.sandboxApi.createSandboxSnapshot(this.id, req, undefined, {
528
523
  timeout: timeout * 1000,
529
524
  });
530
- await this.refreshData();
525
+ this.processSandboxDto(response.data);
531
526
  const timeElapsed = Date.now() - startTime;
532
527
  const remainingTimeout = timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout;
533
528
  await this.waitForSnapshotComplete(remainingTimeout);
534
529
  }
535
530
  async waitForSnapshotComplete(timeout) {
536
- let checkInterval = 100;
537
- const startTime = Date.now();
538
- while (this.state === SandboxState.SNAPSHOTTING) {
539
- await this.refreshData();
540
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
541
- if (this.state === SandboxState.ERROR || this.state === SandboxState.BUILD_FAILED) {
542
- throw new DaytonaError(`Sandbox ${this.id} snapshot failed with state: ${this.state}, error reason: ${this.errorReason}`);
543
- }
544
- if (this.state !== SandboxState.SNAPSHOTTING) {
545
- return;
546
- }
547
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
548
- throw new DaytonaTimeoutError('Sandbox snapshot did not complete within the timeout period');
549
- }
550
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
551
- if (Date.now() - startTime > 5000) {
552
- checkInterval = Math.min(checkInterval * 1.1, 1000);
553
- }
554
- }
531
+ const errorStates = [SandboxState.ERROR, SandboxState.BUILD_FAILED];
532
+ const excludeStates = new Set([SandboxState.SNAPSHOTTING, ...errorStates]);
533
+ const targetStates = Object.values(SandboxState).filter((s) => !excludeStates.has(s));
534
+ return this.waitForState(targetStates, errorStates, timeout, 'Sandbox snapshot did not complete within the timeout period', (state) => `Sandbox ${this.id} snapshot failed with state: ${state}, error reason: ${this.errorReason}`);
555
535
  }
556
536
  /**
557
537
  * Pauses the Sandbox, freezing all running processes.
@@ -582,31 +562,47 @@ let Sandbox = (() => {
582
562
  await this.refreshData();
583
563
  const timeElapsed = Date.now() - startTime;
584
564
  const remainingTimeout = timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout;
585
- await this.waitForPauseComplete(remainingTimeout);
586
- }
587
- async waitForPauseComplete(timeout) {
588
- let checkInterval = 100;
589
- const startTime = Date.now();
590
- while (this.state === SandboxState.PAUSING) {
591
- await this.refreshData();
592
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
593
- if (this.state === SandboxState.ERROR) {
594
- throw new DaytonaError(`Sandbox ${this.id} pause failed with state: ${this.state}, error reason: ${this.errorReason}`);
595
- }
596
- if (timeout > 0 && (Date.now() - startTime) / 1000 >= timeout) {
597
- throw new DaytonaError(`Sandbox ${this.id} failed to pause within ${timeout} seconds`);
598
- }
599
- await new Promise((resolve) => globalThis.setTimeout(resolve, checkInterval));
600
- checkInterval = Math.min(checkInterval * 1.5, 1000);
601
- }
565
+ // Main's contract: pause completes when the sandbox has *left* PAUSING
566
+ // (paused, stopped, archived, ...), not only on exactly PAUSED.
567
+ const errorStates = [SandboxState.ERROR, SandboxState.BUILD_FAILED];
568
+ const excludeStates = new Set([SandboxState.PAUSING, ...errorStates]);
569
+ const targetStates = Object.values(SandboxState).filter((s) => !excludeStates.has(s));
570
+ await this.waitForState(targetStates, errorStates, remainingTimeout, 'Sandbox failed to become paused within the timeout period', (state) => `Sandbox ${this.id} pause failed with state: ${state}, error reason: ${this.errorReason}`);
602
571
  }
603
572
  /**
604
573
  * Deletes the Sandbox.
574
+ *
575
+ * By default this returns as soon as the deletion request is accepted (matching
576
+ * historical behavior). Pass `wait = true` to block until the Sandbox reaches
577
+ * the 'destroyed' state.
578
+ *
579
+ * @param {number} [timeout] - Timeout in seconds for the request — and, when
580
+ * `wait` is true, for reaching 'destroyed'. 0 means no timeout.
581
+ * Defaults to 60-second timeout.
582
+ * @param {boolean} [wait] - If true, wait until the Sandbox is destroyed. Defaults to false.
605
583
  * @returns {Promise<void>}
606
584
  */
607
- async delete(timeout = 60) {
608
- await this.sandboxApi.deleteSandbox(this.id, undefined, { timeout: timeout * 1000 });
609
- this.refreshDataSafe();
585
+ async delete(timeout = 60, wait = false) {
586
+ if (timeout < 0) {
587
+ throw new DaytonaValidationError('Timeout must be a non-negative number');
588
+ }
589
+ const startTime = Date.now();
590
+ const response = await this.sandboxApi.deleteSandbox(this.id, undefined, { timeout: timeout * 1000 });
591
+ if (response.data) {
592
+ this.processSandboxDto(response.data);
593
+ }
594
+ try {
595
+ if (wait && this.state !== SandboxState.DESTROYED) {
596
+ const timeElapsed = Date.now() - startTime;
597
+ await this.waitForState([SandboxState.DESTROYED], [SandboxState.ERROR, SandboxState.BUILD_FAILED], timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout, 'Sandbox failed to be destroyed within the timeout period', (state) => `Sandbox ${this.id} failed to delete with state: ${state}, error reason: ${this.errorReason}`, true);
598
+ }
599
+ }
600
+ finally {
601
+ if (this.subId) {
602
+ this.subscriptionManager.unsubscribe(this.subId);
603
+ this.subId = undefined;
604
+ }
605
+ }
610
606
  }
611
607
  /**
612
608
  * Waits for the Sandbox to reach the 'started' state.
@@ -623,26 +619,10 @@ let Sandbox = (() => {
623
619
  if (timeout < 0) {
624
620
  throw new DaytonaValidationError('Timeout must be a non-negative number');
625
621
  }
626
- let checkInterval = 100;
627
- const startTime = Date.now();
628
- while (this.state !== 'started') {
629
- await this.refreshData();
630
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
631
- if (this.state === 'started') {
632
- return;
633
- }
634
- if (this.state === 'error') {
635
- const errMsg = `Sandbox ${this.id} failed to start with status: ${this.state}, error reason: ${this.errorReason}`;
636
- throw new DaytonaError(errMsg);
637
- }
638
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
639
- throw new DaytonaTimeoutError('Sandbox failed to become ready within the timeout period');
640
- }
641
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
642
- if (Date.now() - startTime > 5000) {
643
- checkInterval = Math.min(checkInterval * 1.1, 1000);
644
- }
622
+ if (this.state === SandboxState.STARTED) {
623
+ return;
645
624
  }
625
+ return this.waitForState([SandboxState.STARTED], [SandboxState.ERROR, SandboxState.BUILD_FAILED], timeout, 'Sandbox failed to become ready within the timeout period', (state) => `Sandbox ${this.id} failed to start with status: ${state}, error reason: ${this.errorReason}`);
646
626
  }
647
627
  /**
648
628
  * Wait for Sandbox to reach 'stopped' state.
@@ -659,27 +639,11 @@ let Sandbox = (() => {
659
639
  if (timeout < 0) {
660
640
  throw new DaytonaValidationError('Timeout must be a non-negative number');
661
641
  }
662
- let checkInterval = 100;
663
- const startTime = Date.now();
664
642
  // Treat destroyed as stopped to cover ephemeral sandboxes that are automatically deleted after stopping
665
- while (this.state !== 'stopped' && this.state !== 'destroyed') {
666
- this.refreshDataSafe();
667
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
668
- if (this.state === 'stopped' || this.state === 'destroyed') {
669
- return;
670
- }
671
- if (this.state === 'error') {
672
- const errMsg = `Sandbox failed to stop with status: ${this.state}, error reason: ${this.errorReason}`;
673
- throw new DaytonaError(errMsg);
674
- }
675
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
676
- throw new DaytonaTimeoutError('Sandbox failed to become stopped within the timeout period');
677
- }
678
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
679
- if (Date.now() - startTime > 5000) {
680
- checkInterval = Math.min(checkInterval * 1.1, 1000);
681
- }
643
+ if (this.state === SandboxState.STOPPED || this.state === SandboxState.DESTROYED) {
644
+ return;
682
645
  }
646
+ return this.waitForState([SandboxState.STOPPED, SandboxState.DESTROYED], [SandboxState.ERROR, SandboxState.BUILD_FAILED], timeout, 'Sandbox failed to become stopped within the timeout period', (state) => `Sandbox failed to stop with status: ${state}, error reason: ${this.errorReason}`, true);
683
647
  }
684
648
  /**
685
649
  * Refreshes the Sandbox data from the API.
@@ -736,6 +700,60 @@ let Sandbox = (() => {
736
700
  await this.sandboxApi.setAutostopInterval(this.id, interval);
737
701
  this.autoStopInterval = interval;
738
702
  }
703
+ /**
704
+ * Set the auto-pause interval for the Sandbox.
705
+ *
706
+ * The Sandbox will automatically pause after being idle (no new events) for the specified interval.
707
+ * Events include any state changes or interactions with the Sandbox through the sdk.
708
+ * Interactions using Sandbox Previews are not included.
709
+ *
710
+ * Only supported for sandbox classes that support pausing. At most one of the auto-stop
711
+ * and auto-pause intervals may be non-zero, so disable auto-stop first by setting its
712
+ * interval to 0.
713
+ *
714
+ * @param {number} interval - Number of minutes of inactivity before auto-pausing.
715
+ * Set to 0 to disable auto-pause. For pause-supporting sandbox
716
+ * classes, creation defaults to 60 minutes when neither interval is provided.
717
+ * @returns {Promise<void>}
718
+ * @throws {DaytonaError} - `DaytonaError` - If interval is not a non-negative integer
719
+ *
720
+ * @example
721
+ * // Auto-pause after 1 hour
722
+ * await sandbox.setAutoPauseInterval(60);
723
+ * // Or disable auto-pause
724
+ * await sandbox.setAutoPauseInterval(0);
725
+ */
726
+ async setAutoPauseInterval(interval) {
727
+ if (!Number.isInteger(interval) || interval < 0) {
728
+ throw new DaytonaValidationError('autoPauseInterval must be a non-negative integer');
729
+ }
730
+ await this.sandboxApi.setAutoPauseInterval(this.id, interval);
731
+ this.autoPauseInterval = interval;
732
+ }
733
+ /**
734
+ * Set the TTL (maximum time to live) for the Sandbox.
735
+ *
736
+ * The Sandbox will be destroyed once the TTL elapses, counted as wall-clock time regardless of the
737
+ * Sandbox state - even if it is stopped, paused, or archived. Calling this method re-anchors the
738
+ * deadline from the current time. Call `refreshData()` afterwards to read the updated `autoDestroyAt`.
739
+ *
740
+ * @param {number} ttlMinutes - Number of minutes from now after which the Sandbox will be destroyed.
741
+ * Set to 0 to disable the TTL.
742
+ * @returns {Promise<void>}
743
+ * @throws {DaytonaError} - `DaytonaError` - If ttlMinutes is not a non-negative integer
744
+ *
745
+ * @example
746
+ * // Destroy the Sandbox 1 hour from now
747
+ * await sandbox.setTtl(60);
748
+ * // Or disable the TTL
749
+ * await sandbox.setTtl(0);
750
+ */
751
+ async setTtl(ttlMinutes) {
752
+ if (!Number.isInteger(ttlMinutes) || ttlMinutes < 0) {
753
+ throw new DaytonaValidationError('ttlMinutes must be a non-negative integer');
754
+ }
755
+ await this.sandboxApi.setTtl(this.id, ttlMinutes);
756
+ }
739
757
  /**
740
758
  * Set the auto-archive interval for the Sandbox.
741
759
  *
@@ -958,25 +976,13 @@ let Sandbox = (() => {
958
976
  if (timeout < 0) {
959
977
  throw new DaytonaValidationError('Timeout must be a non-negative number');
960
978
  }
961
- let checkInterval = 100;
962
- const startTime = Date.now();
963
- while (this.state === SandboxState.RESIZING) {
964
- await this.refreshData();
965
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
966
- if (this.state === SandboxState.ERROR || this.state === SandboxState.BUILD_FAILED) {
967
- throw new DaytonaError(`Sandbox ${this.id} resize failed with state: ${this.state}, error reason: ${this.errorReason}`);
968
- }
969
- if (this.state !== SandboxState.RESIZING) {
970
- return;
971
- }
972
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
973
- throw new DaytonaTimeoutError('Sandbox resize did not complete within the timeout period');
974
- }
975
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
976
- if (Date.now() - startTime > 5000) {
977
- checkInterval = Math.min(checkInterval * 1.1, 1000);
978
- }
979
+ if (this.state !== SandboxState.RESIZING) {
980
+ return;
979
981
  }
982
+ const errorStates = [SandboxState.ERROR, SandboxState.BUILD_FAILED];
983
+ const excludeStates = new Set([SandboxState.RESIZING, ...errorStates]);
984
+ const targetStates = Object.values(SandboxState).filter((s) => !excludeStates.has(s));
985
+ return this.waitForState(targetStates, errorStates, timeout, 'Sandbox resize did not complete within the timeout period', (state) => `Sandbox ${this.id} resize failed with state: ${state}, error reason: ${this.errorReason}`);
980
986
  }
981
987
  /**
982
988
  * Creates an SSH access token for the sandbox.
@@ -1005,6 +1011,172 @@ let Sandbox = (() => {
1005
1011
  async validateSshAccess(token) {
1006
1012
  return (await this.sandboxApi.validateSshAccess(token)).data;
1007
1013
  }
1014
+ /**
1015
+ * Subscribes to real-time events for this sandbox.
1016
+ * Auto-updates sandbox metadata on every event.
1017
+ */
1018
+ subscribeToEvents() {
1019
+ if (this.subId) {
1020
+ return;
1021
+ }
1022
+ this.subId = this.subscriptionManager.subscribe(this.id, this.handleEvent.bind(this), [
1023
+ 'sandbox.state.updated',
1024
+ 'sandbox.created',
1025
+ ]);
1026
+ }
1027
+ ensureSubscribed() {
1028
+ if (this.subId) {
1029
+ if (this.subscriptionManager.refresh(this.subId)) {
1030
+ return;
1031
+ }
1032
+ this.subId = undefined;
1033
+ }
1034
+ this.subscribeToEvents();
1035
+ }
1036
+ handleEvent(eventName, data) {
1037
+ if (!data || typeof data !== 'object')
1038
+ return;
1039
+ const raw = data.sandbox ?? data;
1040
+ if (!raw || typeof raw !== 'object')
1041
+ return;
1042
+ if (eventName === 'sandbox.created') {
1043
+ this.processSandboxDto(raw);
1044
+ return;
1045
+ }
1046
+ const newState = raw.state ?? data.newState;
1047
+ if (newState) {
1048
+ this.applyState(newState);
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Waits for the sandbox to reach one of the target states.
1053
+ * Throws on error states or timeout.
1054
+ *
1055
+ * @param targetStates - States that indicate success.
1056
+ * @param errorStates - States that indicate failure.
1057
+ * @param timeout - Maximum time to wait in seconds. 0 means no timeout.
1058
+ * @param timeoutMessage - Error message when timeout is reached.
1059
+ * @param errorMessageFn - Function that produces an error message from the current state.
1060
+ * @param safeRefresh - If true, use refreshDataSafe() for polling (for delete operations where 404 is expected).
1061
+ */
1062
+ waitForState(targetStates, errorStates, timeout, timeoutMessage, errorMessageFn, safeRefresh = false) {
1063
+ this.ensureSubscribed();
1064
+ return new Promise((resolve, reject) => {
1065
+ let timeoutTimer = null;
1066
+ let pollTimer = null;
1067
+ let settled = false;
1068
+ const waiter = {
1069
+ targetStates: new Set(targetStates),
1070
+ errorStates: new Set(errorStates),
1071
+ resolve: (_) => {
1072
+ if (settled)
1073
+ return;
1074
+ cleanup();
1075
+ resolve();
1076
+ },
1077
+ reject: (err) => {
1078
+ if (settled)
1079
+ return;
1080
+ cleanup();
1081
+ reject(err);
1082
+ },
1083
+ };
1084
+ this.stateWaiters.push(waiter);
1085
+ this.stateWaiterErrorMessageFns.set(waiter, errorMessageFn);
1086
+ const cleanup = () => {
1087
+ if (settled)
1088
+ return;
1089
+ settled = true;
1090
+ if (timeoutTimer)
1091
+ clearTimeout(timeoutTimer);
1092
+ if (pollTimer)
1093
+ clearTimeout(pollTimer);
1094
+ this.removeStateWaiter(waiter);
1095
+ };
1096
+ // Fast-path only on cached *target* states (parity with main's pre-check).
1097
+ // Cached error states are deliberately NOT evaluated here — main always
1098
+ // refreshed before failing, so a stale ERROR must survive one refresh.
1099
+ if (this.state && waiter.targetStates.has(this.state)) {
1100
+ waiter.resolve(this.state);
1101
+ return;
1102
+ }
1103
+ if (timeout !== 0) {
1104
+ timeoutTimer = setTimeout(() => {
1105
+ void (async () => {
1106
+ if (settled)
1107
+ return;
1108
+ // Parity with main: complete one final refresh-then-evaluate before
1109
+ // rejecting, so a clamped/short timeout still observes the latest state.
1110
+ let refreshed = false;
1111
+ try {
1112
+ if (safeRefresh) {
1113
+ refreshed = await this.refreshDataSafe();
1114
+ }
1115
+ else {
1116
+ await this.refreshData();
1117
+ refreshed = true;
1118
+ }
1119
+ }
1120
+ catch {
1121
+ // fall through to the timeout rejection below
1122
+ }
1123
+ if (!settled) {
1124
+ // Explicit evaluation: applyState() no-ops on unchanged state, so a
1125
+ // persistent error state would otherwise time out generically here.
1126
+ // Only evaluate when the refresh succeeded — a failed refresh leaves
1127
+ // the cached state stale and must not be treated as authoritative.
1128
+ if (refreshed && this.checkStateWaiter(waiter, this.state)) {
1129
+ return;
1130
+ }
1131
+ cleanup();
1132
+ reject(new DaytonaTimeoutError(timeoutMessage));
1133
+ }
1134
+ })();
1135
+ }, timeout * 1000);
1136
+ }
1137
+ // Poll as a safety net for missed state changes. With an active event subscription
1138
+ // a sparse 1s cadence suffices; without events (polling mode) replicate main's
1139
+ // cadence exactly: 100ms steady for the first 5s, then exponential backoff capped at 1s.
1140
+ const streaming = !!this.subId;
1141
+ const pollStart = Date.now();
1142
+ let pollInterval = streaming ? 1000 : 100;
1143
+ const doPoll = async () => {
1144
+ if (settled)
1145
+ return;
1146
+ let refreshed = false;
1147
+ try {
1148
+ if (safeRefresh) {
1149
+ refreshed = await this.refreshDataSafe();
1150
+ }
1151
+ else {
1152
+ await this.refreshData();
1153
+ refreshed = true;
1154
+ }
1155
+ }
1156
+ catch (error) {
1157
+ waiter.reject(error instanceof Error ? error : new DaytonaError(String(error)));
1158
+ return;
1159
+ }
1160
+ if (!settled) {
1161
+ // Evaluate the refreshed state explicitly: applyState() no-ops when the
1162
+ // state is unchanged, so a persistent error state would otherwise never
1163
+ // reject the waiter. Only evaluate when the refresh succeeded — a failed
1164
+ // safe refresh leaves the cached state stale, so keep polling instead.
1165
+ if (refreshed && this.checkStateWaiter(waiter, this.state)) {
1166
+ return;
1167
+ }
1168
+ if (!streaming && Date.now() - pollStart > 5000) {
1169
+ pollInterval = Math.min(pollInterval * 1.1, 1000);
1170
+ }
1171
+ pollTimer = setTimeout(() => {
1172
+ void doPoll();
1173
+ }, pollInterval);
1174
+ }
1175
+ };
1176
+ // First poll runs immediately (main refreshed before any state evaluation).
1177
+ void doPoll();
1178
+ });
1179
+ }
1008
1180
  /**
1009
1181
  * Assigns the API sandbox data to the Sandbox object.
1010
1182
  *
@@ -1012,6 +1184,7 @@ let Sandbox = (() => {
1012
1184
  * @returns {void}
1013
1185
  */
1014
1186
  processSandboxDto(sandboxDto) {
1187
+ const newState = sandboxDto.state;
1015
1188
  this.id = sandboxDto.id;
1016
1189
  this.name = sandboxDto.name;
1017
1190
  this.organizationId = sandboxDto.organizationId;
@@ -1024,17 +1197,17 @@ let Sandbox = (() => {
1024
1197
  this.gpu = sandboxDto.gpu;
1025
1198
  this.memory = sandboxDto.memory;
1026
1199
  this.disk = sandboxDto.disk;
1027
- this.state = sandboxDto.state;
1028
1200
  this.errorReason = sandboxDto.errorReason;
1029
1201
  this.recoverable = sandboxDto.recoverable;
1030
1202
  this.backupState = sandboxDto.backupState;
1031
1203
  this.autoStopInterval = sandboxDto.autoStopInterval;
1204
+ this.autoPauseInterval = sandboxDto.autoPauseInterval;
1032
1205
  this.autoArchiveInterval = sandboxDto.autoArchiveInterval;
1033
1206
  this.autoDeleteInterval = sandboxDto.autoDeleteInterval;
1207
+ this.autoDestroyAt = sandboxDto.autoDestroyAt;
1034
1208
  this.createdAt = sandboxDto.createdAt;
1035
1209
  this.updatedAt = sandboxDto.updatedAt;
1036
1210
  this.lastActivityAt = sandboxDto.lastActivityAt;
1037
- this.toolboxProxyUrl = sandboxDto.toolboxProxyUrl;
1038
1211
  // Fields only present in the full SandboxDto (not returned by list endpoint)
1039
1212
  if ('env' in sandboxDto) {
1040
1213
  this.env = sandboxDto.env;
@@ -1046,6 +1219,21 @@ let Sandbox = (() => {
1046
1219
  this.buildInfo = sandboxDto.buildInfo;
1047
1220
  this.backupCreatedAt = sandboxDto.backupCreatedAt;
1048
1221
  }
1222
+ const newProxyUrl = sandboxDto.toolboxProxyUrl;
1223
+ if (newProxyUrl && newProxyUrl !== this.toolboxProxyUrl && this.axiosInstance) {
1224
+ let baseUrl = newProxyUrl;
1225
+ if (!baseUrl.endsWith('/')) {
1226
+ baseUrl += '/';
1227
+ }
1228
+ this.axiosInstance.defaults.baseURL = baseUrl + this.id;
1229
+ this.clientConfig.basePath = this.axiosInstance.defaults.baseURL;
1230
+ }
1231
+ if (newProxyUrl) {
1232
+ this.toolboxProxyUrl = newProxyUrl;
1233
+ }
1234
+ if (newState) {
1235
+ this.applyState(newState);
1236
+ }
1049
1237
  }
1050
1238
  /**
1051
1239
  * Refreshes the Sandbox data from the API, but does not throw an error if the sandbox has been deleted.
@@ -1053,17 +1241,113 @@ let Sandbox = (() => {
1053
1241
  *
1054
1242
  * @returns {Promise<void>}
1055
1243
  */
1244
+ /**
1245
+ * @returns true when the refresh produced authoritative state (success, or 404
1246
+ * mapped to DESTROYED); false when a transient error was swallowed and
1247
+ * the cached state is stale.
1248
+ */
1056
1249
  async refreshDataSafe() {
1057
1250
  try {
1058
1251
  await this.refreshData();
1252
+ return true;
1059
1253
  }
1060
1254
  catch (error) {
1061
1255
  if (error instanceof DaytonaNotFoundError) {
1062
- this.state = SandboxState.DESTROYED;
1256
+ this.applyState(SandboxState.DESTROYED);
1257
+ return true;
1063
1258
  }
1259
+ // Other errors are deliberately swallowed (parity with main): transient
1260
+ // failures (e.g. 502) mid-poll must not abort stop()/delete() waits.
1261
+ return false;
1262
+ }
1263
+ }
1264
+ applyState(newState) {
1265
+ if (newState === this.state) {
1266
+ return;
1267
+ }
1268
+ this.state = newState;
1269
+ for (const waiter of [...this.stateWaiters]) {
1270
+ this.checkStateWaiter(waiter, this.state);
1064
1271
  }
1065
1272
  }
1273
+ checkStateWaiter(waiter, state) {
1274
+ if (!state) {
1275
+ return false;
1276
+ }
1277
+ if (waiter.targetStates.has(state)) {
1278
+ waiter.resolve(state);
1279
+ return true;
1280
+ }
1281
+ if (waiter.errorStates.has(state)) {
1282
+ const errorMessageFn = this.stateWaiterErrorMessageFns.get(waiter);
1283
+ waiter.reject(new DaytonaError(errorMessageFn ? errorMessageFn(state) : `Sandbox ${this.id} failed with state: ${state}`));
1284
+ return true;
1285
+ }
1286
+ return false;
1287
+ }
1288
+ removeStateWaiter(waiter) {
1289
+ const index = this.stateWaiters.indexOf(waiter);
1290
+ if (index !== -1) {
1291
+ this.stateWaiters.splice(index, 1);
1292
+ }
1293
+ this.stateWaiterErrorMessageFns.delete(waiter);
1294
+ }
1066
1295
  };
1067
1296
  })();
1068
1297
  export { Sandbox };
1298
+ const SANDBOX_METRIC_FIELD_BY_NAME = {
1299
+ 'daytona.sandbox.cpu.utilization': 'cpuUsedPct',
1300
+ 'daytona.sandbox.cpu.limit': 'cpuCount',
1301
+ 'daytona.sandbox.memory.usage': 'memUsed',
1302
+ 'daytona.sandbox.memory.limit': 'memTotal',
1303
+ 'daytona.sandbox.memory.cache': 'memCache',
1304
+ 'daytona.sandbox.filesystem.usage': 'diskUsed',
1305
+ 'daytona.sandbox.filesystem.total': 'diskTotal',
1306
+ };
1307
+ const SANDBOX_METRIC_NAMES = Object.keys(SANDBOX_METRIC_FIELD_BY_NAME);
1308
+ function sandboxMetricsFromSystemMetrics(m) {
1309
+ return {
1310
+ cpuCount: m.cpuCount ?? 0,
1311
+ cpuUsedPct: m.cpuUsedPct ?? 0,
1312
+ diskTotal: m.diskTotal ?? 0,
1313
+ diskUsed: m.diskUsed ?? 0,
1314
+ memTotal: m.memTotal ?? 0,
1315
+ memUsed: m.memUsed ?? 0,
1316
+ memCache: m.memCache ?? 0,
1317
+ timestamp: m.timestamp ? new Date(m.timestamp) : new Date(),
1318
+ };
1319
+ }
1320
+ function buildSandboxMetrics(buckets) {
1321
+ return [...buckets.keys()].sort().map((ts) => {
1322
+ const v = buckets.get(ts);
1323
+ return {
1324
+ cpuCount: v.cpuCount ?? 0,
1325
+ cpuUsedPct: v.cpuUsedPct ?? 0,
1326
+ diskTotal: v.diskTotal ?? 0,
1327
+ diskUsed: v.diskUsed ?? 0,
1328
+ memTotal: v.memTotal ?? 0,
1329
+ memUsed: v.memUsed ?? 0,
1330
+ memCache: v.memCache ?? 0,
1331
+ timestamp: new Date(ts),
1332
+ };
1333
+ });
1334
+ }
1335
+ function pivotMetricTriples(triples) {
1336
+ const buckets = new Map();
1337
+ for (const [name, timestamp, value] of triples) {
1338
+ const field = name ? SANDBOX_METRIC_FIELD_BY_NAME[name] : undefined;
1339
+ if (!field || timestamp === undefined || value === undefined)
1340
+ continue;
1341
+ const bucket = buckets.get(timestamp) ?? {};
1342
+ bucket[field] = value;
1343
+ buckets.set(timestamp, bucket);
1344
+ }
1345
+ return buildSandboxMetrics(buckets);
1346
+ }
1347
+ function pivotSandboxMetrics(series) {
1348
+ return pivotMetricTriples((series ?? []).flatMap((s) => (s.dataPoints ?? []).map((p) => [s.metricName, p.timestamp, p.value])));
1349
+ }
1350
+ function pivotSandboxMetricPoints(points) {
1351
+ return pivotMetricTriples((points ?? []).map((p) => [p.metricName, p.timestamp, p.value]));
1352
+ }
1069
1353
  //# sourceMappingURL=Sandbox.js.map