@daytona/sdk 0.197.0 → 0.198.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/cjs/Sandbox.js CHANGED
@@ -19,6 +19,13 @@ const Daytona_2 = require("./Daytona");
19
19
  const ComputerUse_1 = require("./ComputerUse");
20
20
  const CodeInterpreter_1 = require("./CodeInterpreter");
21
21
  const otel_decorator_1 = require("./utils/otel.decorator");
22
+ function withEvents(target, _context) {
23
+ return function (...args) {
24
+ ;
25
+ this.ensureSubscribed();
26
+ return target.apply(this, args);
27
+ };
28
+ }
22
29
  /**
23
30
  * Represents a Daytona Sandbox.
24
31
  *
@@ -79,6 +86,7 @@ let Sandbox = (() => {
79
86
  let _createLspServer_decorators;
80
87
  let _setLabels_decorators;
81
88
  let _start_decorators;
89
+ let _recover_decorators;
82
90
  let _stop_decorators;
83
91
  let __experimental_fork_decorators;
84
92
  let __experimental_createSnapshot_decorators;
@@ -87,6 +95,7 @@ let Sandbox = (() => {
87
95
  let _waitUntilStarted_decorators;
88
96
  let _waitUntilStopped_decorators;
89
97
  let _refreshData_decorators;
98
+ let _refreshActivity_decorators;
90
99
  let _setAutostopInterval_decorators;
91
100
  let _setAutoPauseInterval_decorators;
92
101
  let _setAutoArchiveInterval_decorators;
@@ -95,6 +104,8 @@ let Sandbox = (() => {
95
104
  let _updateSecrets_decorators;
96
105
  let _updateEnv_decorators;
97
106
  let _getPreviewLink_decorators;
107
+ let _getSignedPreviewUrl_decorators;
108
+ let _expireSignedPreviewUrl_decorators;
98
109
  let _archive_decorators;
99
110
  let _resize_decorators;
100
111
  let _waitForResizeComplete_decorators;
@@ -104,36 +115,40 @@ let Sandbox = (() => {
104
115
  return class Sandbox {
105
116
  static {
106
117
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
107
- _getUserHomeDir_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
118
+ _getUserHomeDir_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
108
119
  _getMetricsLatest_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
109
120
  _getMetrics_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
110
- _getUserRootDir_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
111
- _getWorkDir_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
112
- _createLspServer_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
113
- _setLabels_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
114
- _start_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
115
- _stop_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
116
- __experimental_fork_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
117
- __experimental_createSnapshot_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
118
- _pause_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
119
- _delete_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
120
- _waitUntilStarted_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
121
- _waitUntilStopped_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
122
- _refreshData_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
123
- _setAutostopInterval_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
121
+ _getUserRootDir_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
122
+ _getWorkDir_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
123
+ _createLspServer_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
124
+ _setLabels_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
125
+ _start_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
126
+ _recover_decorators = [withEvents];
127
+ _stop_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
128
+ __experimental_fork_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
129
+ __experimental_createSnapshot_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
130
+ _pause_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
131
+ _delete_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
132
+ _waitUntilStarted_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
133
+ _waitUntilStopped_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
134
+ _refreshData_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
135
+ _refreshActivity_decorators = [withEvents];
136
+ _setAutostopInterval_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
124
137
  _setAutoPauseInterval_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
125
- _setAutoArchiveInterval_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
126
- _setAutoDeleteInterval_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
138
+ _setAutoArchiveInterval_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
139
+ _setAutoDeleteInterval_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
127
140
  _updateNetworkSettings_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
128
141
  _updateSecrets_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
129
142
  _updateEnv_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
130
- _getPreviewLink_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
131
- _archive_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
132
- _resize_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
133
- _waitForResizeComplete_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
134
- _createSshAccess_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
135
- _revokeSshAccess_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
136
- _validateSshAccess_decorators = [(0, otel_decorator_1.WithInstrumentation)()];
143
+ _getPreviewLink_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
144
+ _getSignedPreviewUrl_decorators = [withEvents];
145
+ _expireSignedPreviewUrl_decorators = [withEvents];
146
+ _archive_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
147
+ _resize_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
148
+ _waitForResizeComplete_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
149
+ _createSshAccess_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
150
+ _revokeSshAccess_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
151
+ _validateSshAccess_decorators = [(0, otel_decorator_1.WithInstrumentation)(), withEvents];
137
152
  tslib_1.__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);
138
153
  tslib_1.__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);
139
154
  tslib_1.__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);
@@ -142,6 +157,7 @@ let Sandbox = (() => {
142
157
  tslib_1.__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);
143
158
  tslib_1.__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);
144
159
  tslib_1.__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
+ tslib_1.__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);
145
161
  tslib_1.__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);
146
162
  tslib_1.__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);
147
163
  tslib_1.__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);
@@ -150,6 +166,7 @@ let Sandbox = (() => {
150
166
  tslib_1.__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);
151
167
  tslib_1.__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);
152
168
  tslib_1.__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
+ tslib_1.__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);
153
170
  tslib_1.__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);
154
171
  tslib_1.__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);
155
172
  tslib_1.__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);
@@ -158,6 +175,8 @@ let Sandbox = (() => {
158
175
  tslib_1.__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);
159
176
  tslib_1.__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);
160
177
  tslib_1.__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);
178
+ tslib_1.__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);
179
+ tslib_1.__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);
161
180
  tslib_1.__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);
162
181
  tslib_1.__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);
163
182
  tslib_1.__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);
@@ -170,6 +189,7 @@ let Sandbox = (() => {
170
189
  axiosInstance;
171
190
  sandboxApi;
172
191
  getAnalyticsApiUrl;
192
+ subscriptionManager;
173
193
  fs;
174
194
  git;
175
195
  process;
@@ -210,25 +230,27 @@ let Sandbox = (() => {
210
230
  infoApi;
211
231
  serverApi;
212
232
  systemApi;
233
+ stateWaiters = [];
234
+ stateWaiterErrorMessageFns = new WeakMap();
235
+ subId;
213
236
  /**
214
- * Creates a new Sandbox instance
237
+ * Creates a new Sandbox instance.
238
+ *
239
+ * Internal: obtain sandboxes via {@link Daytona.create}, {@link Daytona.get}, or
240
+ * {@link Daytona.list} rather than constructing directly.
215
241
  *
216
242
  * @param {SandboxDto} sandboxDto - The API Sandbox instance
243
+ * @param {SandboxApi} sandboxApi - API client for Sandbox operations
244
+ * @param {InfoApi} infoApi - API client for info operations
245
+ * @param {EventSubscriptionManager} subscriptionManager - Event subscription manager for real-time updates
217
246
  */
218
- constructor(sandboxDto, clientConfig, axiosInstance, sandboxApi, getAnalyticsApiUrl) {
247
+ constructor(sandboxDto, clientConfig, axiosInstance, sandboxApi, getAnalyticsApiUrl, subscriptionManager) {
219
248
  this.clientConfig = clientConfig;
220
249
  this.axiosInstance = axiosInstance;
221
250
  this.sandboxApi = sandboxApi;
222
251
  this.getAnalyticsApiUrl = getAnalyticsApiUrl;
252
+ this.subscriptionManager = subscriptionManager;
223
253
  this.processSandboxDto(sandboxDto);
224
- // Set the toolbox base URL
225
- let baseUrl = this.toolboxProxyUrl;
226
- if (!baseUrl.endsWith('/')) {
227
- baseUrl += '/';
228
- }
229
- this.axiosInstance.defaults.baseURL = baseUrl + this.id;
230
- this.clientConfig.basePath = this.axiosInstance.defaults.baseURL;
231
- // Initialize Services
232
254
  const getPreviewToken = async () => (await this.getPreviewLink(1)).token;
233
255
  this.fs = new FileSystem_1.FileSystem(this.clientConfig, new toolbox_api_client_1.FileSystemApi(this.clientConfig, '', this.axiosInstance));
234
256
  this.git = new Git_1.Git(new toolbox_api_client_1.GitApi(this.clientConfig, '', this.axiosInstance));
@@ -239,6 +261,7 @@ let Sandbox = (() => {
239
261
  this.infoApi = new toolbox_api_client_1.InfoApi(this.clientConfig, '', this.axiosInstance);
240
262
  this.serverApi = new toolbox_api_client_1.ServerApi(this.clientConfig, '', this.axiosInstance);
241
263
  this.systemApi = new toolbox_api_client_1.SystemApi(this.clientConfig, '', this.axiosInstance);
264
+ this.subscribeToEvents();
242
265
  }
243
266
  /**
244
267
  * Gets the user's home directory path for the logged in user inside the Sandbox.
@@ -457,9 +480,16 @@ let Sandbox = (() => {
457
480
  timeout: timeout * 1000,
458
481
  });
459
482
  const sandboxDto = response.data;
460
- const forkedSandbox = new Sandbox(sandboxDto, structuredClone(this.clientConfig), Daytona_1.Daytona.createAxiosInstance(), this.sandboxApi, this.getAnalyticsApiUrl);
483
+ const sandboxWithProxyUrl = sandboxDto.toolboxProxyUrl
484
+ ? sandboxDto
485
+ : {
486
+ ...sandboxDto,
487
+ toolboxProxyUrl: (await this.sandboxApi.getToolboxProxyUrl(sandboxDto.id)).data.url,
488
+ };
489
+ const forkedSandbox = new Sandbox(sandboxWithProxyUrl, structuredClone(this.clientConfig), Daytona_1.Daytona.createAxiosInstance(), this.sandboxApi, this.getAnalyticsApiUrl, this.subscriptionManager);
461
490
  const timeElapsed = Date.now() - startTime;
462
- await forkedSandbox.waitUntilStarted(timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout);
491
+ const remainingTimeout = timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout;
492
+ await forkedSandbox.waitUntilStarted(remainingTimeout);
463
493
  return forkedSandbox;
464
494
  }
465
495
  /**
@@ -487,34 +517,19 @@ let Sandbox = (() => {
487
517
  }
488
518
  const startTime = Date.now();
489
519
  const req = { name };
490
- await this.sandboxApi.createSandboxSnapshot(this.id, req, undefined, {
520
+ const response = await this.sandboxApi.createSandboxSnapshot(this.id, req, undefined, {
491
521
  timeout: timeout * 1000,
492
522
  });
493
- await this.refreshData();
523
+ this.processSandboxDto(response.data);
494
524
  const timeElapsed = Date.now() - startTime;
495
525
  const remainingTimeout = timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout;
496
526
  await this.waitForSnapshotComplete(remainingTimeout);
497
527
  }
498
528
  async waitForSnapshotComplete(timeout) {
499
- let checkInterval = 100;
500
- const startTime = Date.now();
501
- while (this.state === api_client_1.SandboxState.SNAPSHOTTING) {
502
- await this.refreshData();
503
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
504
- if (this.state === api_client_1.SandboxState.ERROR || this.state === api_client_1.SandboxState.BUILD_FAILED) {
505
- throw new DaytonaError_1.DaytonaError(`Sandbox ${this.id} snapshot failed with state: ${this.state}, error reason: ${this.errorReason}`);
506
- }
507
- if (this.state !== api_client_1.SandboxState.SNAPSHOTTING) {
508
- return;
509
- }
510
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
511
- throw new DaytonaError_1.DaytonaTimeoutError('Sandbox snapshot did not complete within the timeout period');
512
- }
513
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
514
- if (Date.now() - startTime > 5000) {
515
- checkInterval = Math.min(checkInterval * 1.1, 1000);
516
- }
517
- }
529
+ const errorStates = [api_client_1.SandboxState.ERROR, api_client_1.SandboxState.BUILD_FAILED];
530
+ const excludeStates = new Set([api_client_1.SandboxState.SNAPSHOTTING, ...errorStates]);
531
+ const targetStates = Object.values(api_client_1.SandboxState).filter((s) => !excludeStates.has(s));
532
+ 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}`);
518
533
  }
519
534
  /**
520
535
  * Pauses the Sandbox, freezing all running processes.
@@ -545,31 +560,47 @@ let Sandbox = (() => {
545
560
  await this.refreshData();
546
561
  const timeElapsed = Date.now() - startTime;
547
562
  const remainingTimeout = timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout;
548
- await this.waitForPauseComplete(remainingTimeout);
549
- }
550
- async waitForPauseComplete(timeout) {
551
- let checkInterval = 100;
552
- const startTime = Date.now();
553
- while (this.state === api_client_1.SandboxState.PAUSING) {
554
- await this.refreshData();
555
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
556
- if (this.state === api_client_1.SandboxState.ERROR) {
557
- throw new DaytonaError_1.DaytonaError(`Sandbox ${this.id} pause failed with state: ${this.state}, error reason: ${this.errorReason}`);
558
- }
559
- if (timeout > 0 && (Date.now() - startTime) / 1000 >= timeout) {
560
- throw new DaytonaError_1.DaytonaError(`Sandbox ${this.id} failed to pause within ${timeout} seconds`);
561
- }
562
- await new Promise((resolve) => globalThis.setTimeout(resolve, checkInterval));
563
- checkInterval = Math.min(checkInterval * 1.5, 1000);
564
- }
563
+ // Main's contract: pause completes when the sandbox has *left* PAUSING
564
+ // (paused, stopped, archived, ...), not only on exactly PAUSED.
565
+ const errorStates = [api_client_1.SandboxState.ERROR, api_client_1.SandboxState.BUILD_FAILED];
566
+ const excludeStates = new Set([api_client_1.SandboxState.PAUSING, ...errorStates]);
567
+ const targetStates = Object.values(api_client_1.SandboxState).filter((s) => !excludeStates.has(s));
568
+ 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}`);
565
569
  }
566
570
  /**
567
571
  * Deletes the Sandbox.
572
+ *
573
+ * By default this returns as soon as the deletion request is accepted (matching
574
+ * historical behavior). Pass `wait = true` to block until the Sandbox reaches
575
+ * the 'destroyed' state.
576
+ *
577
+ * @param {number} [timeout] - Timeout in seconds for the request — and, when
578
+ * `wait` is true, for reaching 'destroyed'. 0 means no timeout.
579
+ * Defaults to 60-second timeout.
580
+ * @param {boolean} [wait] - If true, wait until the Sandbox is destroyed. Defaults to false.
568
581
  * @returns {Promise<void>}
569
582
  */
570
- async delete(timeout = 60) {
571
- await this.sandboxApi.deleteSandbox(this.id, undefined, { timeout: timeout * 1000 });
572
- this.refreshDataSafe();
583
+ async delete(timeout = 60, wait = false) {
584
+ if (timeout < 0) {
585
+ throw new DaytonaError_1.DaytonaValidationError('Timeout must be a non-negative number');
586
+ }
587
+ const startTime = Date.now();
588
+ const response = await this.sandboxApi.deleteSandbox(this.id, undefined, { timeout: timeout * 1000 });
589
+ if (response.data) {
590
+ this.processSandboxDto(response.data);
591
+ }
592
+ try {
593
+ if (wait && this.state !== api_client_1.SandboxState.DESTROYED) {
594
+ const timeElapsed = Date.now() - startTime;
595
+ await this.waitForState([api_client_1.SandboxState.DESTROYED], [api_client_1.SandboxState.ERROR, api_client_1.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);
596
+ }
597
+ }
598
+ finally {
599
+ if (this.subId) {
600
+ this.subscriptionManager.unsubscribe(this.subId);
601
+ this.subId = undefined;
602
+ }
603
+ }
573
604
  }
574
605
  /**
575
606
  * Waits for the Sandbox to reach the 'started' state.
@@ -586,26 +617,10 @@ let Sandbox = (() => {
586
617
  if (timeout < 0) {
587
618
  throw new DaytonaError_1.DaytonaValidationError('Timeout must be a non-negative number');
588
619
  }
589
- let checkInterval = 100;
590
- const startTime = Date.now();
591
- while (this.state !== 'started') {
592
- await this.refreshData();
593
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
594
- if (this.state === 'started') {
595
- return;
596
- }
597
- if (this.state === 'error') {
598
- const errMsg = `Sandbox ${this.id} failed to start with status: ${this.state}, error reason: ${this.errorReason}`;
599
- throw new DaytonaError_1.DaytonaError(errMsg);
600
- }
601
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
602
- throw new DaytonaError_1.DaytonaTimeoutError('Sandbox failed to become ready within the timeout period');
603
- }
604
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
605
- if (Date.now() - startTime > 5000) {
606
- checkInterval = Math.min(checkInterval * 1.1, 1000);
607
- }
620
+ if (this.state === api_client_1.SandboxState.STARTED) {
621
+ return;
608
622
  }
623
+ return this.waitForState([api_client_1.SandboxState.STARTED], [api_client_1.SandboxState.ERROR, api_client_1.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}`);
609
624
  }
610
625
  /**
611
626
  * Wait for Sandbox to reach 'stopped' state.
@@ -622,27 +637,11 @@ let Sandbox = (() => {
622
637
  if (timeout < 0) {
623
638
  throw new DaytonaError_1.DaytonaValidationError('Timeout must be a non-negative number');
624
639
  }
625
- let checkInterval = 100;
626
- const startTime = Date.now();
627
640
  // Treat destroyed as stopped to cover ephemeral sandboxes that are automatically deleted after stopping
628
- while (this.state !== 'stopped' && this.state !== 'destroyed') {
629
- this.refreshDataSafe();
630
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
631
- if (this.state === 'stopped' || this.state === 'destroyed') {
632
- return;
633
- }
634
- if (this.state === 'error') {
635
- const errMsg = `Sandbox failed to stop with status: ${this.state}, error reason: ${this.errorReason}`;
636
- throw new DaytonaError_1.DaytonaError(errMsg);
637
- }
638
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
639
- throw new DaytonaError_1.DaytonaTimeoutError('Sandbox failed to become stopped 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
- }
641
+ if (this.state === api_client_1.SandboxState.STOPPED || this.state === api_client_1.SandboxState.DESTROYED) {
642
+ return;
645
643
  }
644
+ return this.waitForState([api_client_1.SandboxState.STOPPED, api_client_1.SandboxState.DESTROYED], [api_client_1.SandboxState.ERROR, api_client_1.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);
646
645
  }
647
646
  /**
648
647
  * Refreshes the Sandbox data from the API.
@@ -951,25 +950,13 @@ let Sandbox = (() => {
951
950
  if (timeout < 0) {
952
951
  throw new DaytonaError_1.DaytonaValidationError('Timeout must be a non-negative number');
953
952
  }
954
- let checkInterval = 100;
955
- const startTime = Date.now();
956
- while (this.state === api_client_1.SandboxState.RESIZING) {
957
- await this.refreshData();
958
- // @ts-expect-error this.refreshData() can modify this.state so this check is fine
959
- if (this.state === api_client_1.SandboxState.ERROR || this.state === api_client_1.SandboxState.BUILD_FAILED) {
960
- throw new DaytonaError_1.DaytonaError(`Sandbox ${this.id} resize failed with state: ${this.state}, error reason: ${this.errorReason}`);
961
- }
962
- if (this.state !== api_client_1.SandboxState.RESIZING) {
963
- return;
964
- }
965
- if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
966
- throw new DaytonaError_1.DaytonaTimeoutError('Sandbox resize did not complete within the timeout period');
967
- }
968
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
969
- if (Date.now() - startTime > 5000) {
970
- checkInterval = Math.min(checkInterval * 1.1, 1000);
971
- }
953
+ if (this.state !== api_client_1.SandboxState.RESIZING) {
954
+ return;
972
955
  }
956
+ const errorStates = [api_client_1.SandboxState.ERROR, api_client_1.SandboxState.BUILD_FAILED];
957
+ const excludeStates = new Set([api_client_1.SandboxState.RESIZING, ...errorStates]);
958
+ const targetStates = Object.values(api_client_1.SandboxState).filter((s) => !excludeStates.has(s));
959
+ 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}`);
973
960
  }
974
961
  /**
975
962
  * Creates an SSH access token for the sandbox.
@@ -998,6 +985,172 @@ let Sandbox = (() => {
998
985
  async validateSshAccess(token) {
999
986
  return (await this.sandboxApi.validateSshAccess(token)).data;
1000
987
  }
988
+ /**
989
+ * Subscribes to real-time events for this sandbox.
990
+ * Auto-updates sandbox metadata on every event.
991
+ */
992
+ subscribeToEvents() {
993
+ if (this.subId) {
994
+ return;
995
+ }
996
+ this.subId = this.subscriptionManager.subscribe(this.id, this.handleEvent.bind(this), [
997
+ 'sandbox.state.updated',
998
+ 'sandbox.created',
999
+ ]);
1000
+ }
1001
+ ensureSubscribed() {
1002
+ if (this.subId) {
1003
+ if (this.subscriptionManager.refresh(this.subId)) {
1004
+ return;
1005
+ }
1006
+ this.subId = undefined;
1007
+ }
1008
+ this.subscribeToEvents();
1009
+ }
1010
+ handleEvent(eventName, data) {
1011
+ if (!data || typeof data !== 'object')
1012
+ return;
1013
+ const raw = data.sandbox ?? data;
1014
+ if (!raw || typeof raw !== 'object')
1015
+ return;
1016
+ if (eventName === 'sandbox.created') {
1017
+ this.processSandboxDto(raw);
1018
+ return;
1019
+ }
1020
+ const newState = raw.state ?? data.newState;
1021
+ if (newState) {
1022
+ this.applyState(newState);
1023
+ }
1024
+ }
1025
+ /**
1026
+ * Waits for the sandbox to reach one of the target states.
1027
+ * Throws on error states or timeout.
1028
+ *
1029
+ * @param targetStates - States that indicate success.
1030
+ * @param errorStates - States that indicate failure.
1031
+ * @param timeout - Maximum time to wait in seconds. 0 means no timeout.
1032
+ * @param timeoutMessage - Error message when timeout is reached.
1033
+ * @param errorMessageFn - Function that produces an error message from the current state.
1034
+ * @param safeRefresh - If true, use refreshDataSafe() for polling (for delete operations where 404 is expected).
1035
+ */
1036
+ waitForState(targetStates, errorStates, timeout, timeoutMessage, errorMessageFn, safeRefresh = false) {
1037
+ this.ensureSubscribed();
1038
+ return new Promise((resolve, reject) => {
1039
+ let timeoutTimer = null;
1040
+ let pollTimer = null;
1041
+ let settled = false;
1042
+ const waiter = {
1043
+ targetStates: new Set(targetStates),
1044
+ errorStates: new Set(errorStates),
1045
+ resolve: (_) => {
1046
+ if (settled)
1047
+ return;
1048
+ cleanup();
1049
+ resolve();
1050
+ },
1051
+ reject: (err) => {
1052
+ if (settled)
1053
+ return;
1054
+ cleanup();
1055
+ reject(err);
1056
+ },
1057
+ };
1058
+ this.stateWaiters.push(waiter);
1059
+ this.stateWaiterErrorMessageFns.set(waiter, errorMessageFn);
1060
+ const cleanup = () => {
1061
+ if (settled)
1062
+ return;
1063
+ settled = true;
1064
+ if (timeoutTimer)
1065
+ clearTimeout(timeoutTimer);
1066
+ if (pollTimer)
1067
+ clearTimeout(pollTimer);
1068
+ this.removeStateWaiter(waiter);
1069
+ };
1070
+ // Fast-path only on cached *target* states (parity with main's pre-check).
1071
+ // Cached error states are deliberately NOT evaluated here — main always
1072
+ // refreshed before failing, so a stale ERROR must survive one refresh.
1073
+ if (this.state && waiter.targetStates.has(this.state)) {
1074
+ waiter.resolve(this.state);
1075
+ return;
1076
+ }
1077
+ if (timeout !== 0) {
1078
+ timeoutTimer = setTimeout(() => {
1079
+ void (async () => {
1080
+ if (settled)
1081
+ return;
1082
+ // Parity with main: complete one final refresh-then-evaluate before
1083
+ // rejecting, so a clamped/short timeout still observes the latest state.
1084
+ let refreshed = false;
1085
+ try {
1086
+ if (safeRefresh) {
1087
+ refreshed = await this.refreshDataSafe();
1088
+ }
1089
+ else {
1090
+ await this.refreshData();
1091
+ refreshed = true;
1092
+ }
1093
+ }
1094
+ catch {
1095
+ // fall through to the timeout rejection below
1096
+ }
1097
+ if (!settled) {
1098
+ // Explicit evaluation: applyState() no-ops on unchanged state, so a
1099
+ // persistent error state would otherwise time out generically here.
1100
+ // Only evaluate when the refresh succeeded — a failed refresh leaves
1101
+ // the cached state stale and must not be treated as authoritative.
1102
+ if (refreshed && this.checkStateWaiter(waiter, this.state)) {
1103
+ return;
1104
+ }
1105
+ cleanup();
1106
+ reject(new DaytonaError_1.DaytonaTimeoutError(timeoutMessage));
1107
+ }
1108
+ })();
1109
+ }, timeout * 1000);
1110
+ }
1111
+ // Poll as a safety net for missed state changes. With an active event subscription
1112
+ // a sparse 1s cadence suffices; without events (polling mode) replicate main's
1113
+ // cadence exactly: 100ms steady for the first 5s, then exponential backoff capped at 1s.
1114
+ const streaming = !!this.subId;
1115
+ const pollStart = Date.now();
1116
+ let pollInterval = streaming ? 1000 : 100;
1117
+ const doPoll = async () => {
1118
+ if (settled)
1119
+ return;
1120
+ let refreshed = false;
1121
+ try {
1122
+ if (safeRefresh) {
1123
+ refreshed = await this.refreshDataSafe();
1124
+ }
1125
+ else {
1126
+ await this.refreshData();
1127
+ refreshed = true;
1128
+ }
1129
+ }
1130
+ catch (error) {
1131
+ waiter.reject(error instanceof Error ? error : new DaytonaError_1.DaytonaError(String(error)));
1132
+ return;
1133
+ }
1134
+ if (!settled) {
1135
+ // Evaluate the refreshed state explicitly: applyState() no-ops when the
1136
+ // state is unchanged, so a persistent error state would otherwise never
1137
+ // reject the waiter. Only evaluate when the refresh succeeded — a failed
1138
+ // safe refresh leaves the cached state stale, so keep polling instead.
1139
+ if (refreshed && this.checkStateWaiter(waiter, this.state)) {
1140
+ return;
1141
+ }
1142
+ if (!streaming && Date.now() - pollStart > 5000) {
1143
+ pollInterval = Math.min(pollInterval * 1.1, 1000);
1144
+ }
1145
+ pollTimer = setTimeout(() => {
1146
+ void doPoll();
1147
+ }, pollInterval);
1148
+ }
1149
+ };
1150
+ // First poll runs immediately (main refreshed before any state evaluation).
1151
+ void doPoll();
1152
+ });
1153
+ }
1001
1154
  /**
1002
1155
  * Assigns the API sandbox data to the Sandbox object.
1003
1156
  *
@@ -1005,6 +1158,7 @@ let Sandbox = (() => {
1005
1158
  * @returns {void}
1006
1159
  */
1007
1160
  processSandboxDto(sandboxDto) {
1161
+ const newState = sandboxDto.state;
1008
1162
  this.id = sandboxDto.id;
1009
1163
  this.name = sandboxDto.name;
1010
1164
  this.organizationId = sandboxDto.organizationId;
@@ -1017,7 +1171,6 @@ let Sandbox = (() => {
1017
1171
  this.gpu = sandboxDto.gpu;
1018
1172
  this.memory = sandboxDto.memory;
1019
1173
  this.disk = sandboxDto.disk;
1020
- this.state = sandboxDto.state;
1021
1174
  this.errorReason = sandboxDto.errorReason;
1022
1175
  this.recoverable = sandboxDto.recoverable;
1023
1176
  this.backupState = sandboxDto.backupState;
@@ -1028,7 +1181,6 @@ let Sandbox = (() => {
1028
1181
  this.createdAt = sandboxDto.createdAt;
1029
1182
  this.updatedAt = sandboxDto.updatedAt;
1030
1183
  this.lastActivityAt = sandboxDto.lastActivityAt;
1031
- this.toolboxProxyUrl = sandboxDto.toolboxProxyUrl;
1032
1184
  // Fields only present in the full SandboxDto (not returned by list endpoint)
1033
1185
  if ('env' in sandboxDto) {
1034
1186
  this.env = sandboxDto.env;
@@ -1040,6 +1192,21 @@ let Sandbox = (() => {
1040
1192
  this.buildInfo = sandboxDto.buildInfo;
1041
1193
  this.backupCreatedAt = sandboxDto.backupCreatedAt;
1042
1194
  }
1195
+ const newProxyUrl = sandboxDto.toolboxProxyUrl;
1196
+ if (newProxyUrl && newProxyUrl !== this.toolboxProxyUrl && this.axiosInstance) {
1197
+ let baseUrl = newProxyUrl;
1198
+ if (!baseUrl.endsWith('/')) {
1199
+ baseUrl += '/';
1200
+ }
1201
+ this.axiosInstance.defaults.baseURL = baseUrl + this.id;
1202
+ this.clientConfig.basePath = this.axiosInstance.defaults.baseURL;
1203
+ }
1204
+ if (newProxyUrl) {
1205
+ this.toolboxProxyUrl = newProxyUrl;
1206
+ }
1207
+ if (newState) {
1208
+ this.applyState(newState);
1209
+ }
1043
1210
  }
1044
1211
  /**
1045
1212
  * Refreshes the Sandbox data from the API, but does not throw an error if the sandbox has been deleted.
@@ -1047,15 +1214,56 @@ let Sandbox = (() => {
1047
1214
  *
1048
1215
  * @returns {Promise<void>}
1049
1216
  */
1217
+ /**
1218
+ * @returns true when the refresh produced authoritative state (success, or 404
1219
+ * mapped to DESTROYED); false when a transient error was swallowed and
1220
+ * the cached state is stale.
1221
+ */
1050
1222
  async refreshDataSafe() {
1051
1223
  try {
1052
1224
  await this.refreshData();
1225
+ return true;
1053
1226
  }
1054
1227
  catch (error) {
1055
1228
  if (error instanceof DaytonaError_1.DaytonaNotFoundError) {
1056
- this.state = api_client_1.SandboxState.DESTROYED;
1229
+ this.applyState(api_client_1.SandboxState.DESTROYED);
1230
+ return true;
1057
1231
  }
1232
+ // Other errors are deliberately swallowed (parity with main): transient
1233
+ // failures (e.g. 502) mid-poll must not abort stop()/delete() waits.
1234
+ return false;
1235
+ }
1236
+ }
1237
+ applyState(newState) {
1238
+ if (newState === this.state) {
1239
+ return;
1240
+ }
1241
+ this.state = newState;
1242
+ for (const waiter of [...this.stateWaiters]) {
1243
+ this.checkStateWaiter(waiter, this.state);
1244
+ }
1245
+ }
1246
+ checkStateWaiter(waiter, state) {
1247
+ if (!state) {
1248
+ return false;
1249
+ }
1250
+ if (waiter.targetStates.has(state)) {
1251
+ waiter.resolve(state);
1252
+ return true;
1253
+ }
1254
+ if (waiter.errorStates.has(state)) {
1255
+ const errorMessageFn = this.stateWaiterErrorMessageFns.get(waiter);
1256
+ waiter.reject(new DaytonaError_1.DaytonaError(errorMessageFn ? errorMessageFn(state) : `Sandbox ${this.id} failed with state: ${state}`));
1257
+ return true;
1258
+ }
1259
+ return false;
1260
+ }
1261
+ removeStateWaiter(waiter) {
1262
+ const index = this.stateWaiters.indexOf(waiter);
1263
+ if (index !== -1) {
1264
+ this.stateWaiters.splice(index, 1);
1058
1265
  }
1266
+ this.stateWaiterErrorMessageFns.delete(waiter);
1059
1267
  }
1060
1268
  };
1061
1269
  })();