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