Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/docs/src/content/docs/en/typescript-sdk/sandbox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ new Sandbox(
sandboxDto: SandboxListItem | Sandbox,
clientConfig: Configuration,
axiosInstance: AxiosInstance,
sandboxApi: SandboxApi): Sandbox
sandboxApi: SandboxApi,
snapshotsApi?: SnapshotsApi): Sandbox
```

Creates a new Sandbox instance
Expand All @@ -78,6 +79,7 @@ Creates a new Sandbox instance
- `clientConfig` _Configuration_
- `axiosInstance` _AxiosInstance_
- `sandboxApi` _SandboxApi_
- `snapshotsApi?` _SnapshotsApi_


**Returns**:
Expand All @@ -96,7 +98,9 @@ Creates a snapshot from the current state of the Sandbox.

This captures the Sandbox's filesystem into a reusable snapshot that can be
used to create new Sandboxes. The Sandbox will temporarily enter a
'snapshotting' state and return to its previous state when complete.
'snapshotting' state and return to its previous state when complete. The
method waits until the snapshot record reaches the 'active' state and throws
a DaytonaError carrying the snapshot's error reason if the capture fails.

**Parameters**:

Expand Down
21 changes: 13 additions & 8 deletions libs/sdk-typescript/src/Daytona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export type CreateSandboxFromSnapshotParams = CreateSandboxBaseParams & {
export class Daytona implements AsyncDisposable {
private readonly clientConfig: Configuration
private readonly sandboxApi: SandboxApi
private readonly snapshotsApi: SnapshotsApi
private readonly objectStorageApi: ObjectStorageApi
private readonly configApi: ConfigApi
private readonly target?: string
Expand Down Expand Up @@ -323,15 +324,11 @@ export class Daytona implements AsyncDisposable {
const axiosInstance = Daytona.createAxiosInstance()

this.sandboxApi = new SandboxApi(configuration, '', axiosInstance)
this.snapshotsApi = new SnapshotsApi(configuration, '', axiosInstance)
this.objectStorageApi = new ObjectStorageApi(configuration, '', axiosInstance)
this.configApi = new ConfigApi(configuration, '', axiosInstance)
this.volume = new VolumeService(new VolumesApi(configuration, '', axiosInstance))
this.snapshot = new SnapshotService(
configuration,
new SnapshotsApi(configuration, '', axiosInstance),
this.objectStorageApi,
this.target,
)
this.snapshot = new SnapshotService(configuration, this.snapshotsApi, this.objectStorageApi, this.target)
this.clientConfig = configuration

const env = envReader()
Expand Down Expand Up @@ -620,6 +617,7 @@ export class Daytona implements AsyncDisposable {
new Configuration(structuredClone(this.clientConfig)),
Daytona.createAxiosInstance(),
this.sandboxApi,
this.snapshotsApi,
)

if (sandbox.state !== 'started') {
Expand Down Expand Up @@ -660,6 +658,7 @@ export class Daytona implements AsyncDisposable {
structuredClone(this.clientConfig),
Daytona.createAxiosInstance(),
this.sandboxApi,
this.snapshotsApi,
)
}

Expand All @@ -675,7 +674,7 @@ export class Daytona implements AsyncDisposable {
* }
*/
public list(query?: ListSandboxesQuery): AsyncIterableIterator<Sandbox> {
const { sandboxApi, clientConfig } = this
const { sandboxApi, snapshotsApi, clientConfig } = this
const tracer = trace.getTracer('')

async function* generator(): AsyncGenerator<Sandbox> {
Expand Down Expand Up @@ -746,7 +745,13 @@ export class Daytona implements AsyncDisposable {

for (const sandbox of response.data.items) {
// Sandbox ctor mutates clientConfig.basePath — clone per item.
yield new Sandbox(sandbox, structuredClone(clientConfig), Daytona.createAxiosInstance(), sandboxApi)
yield new Sandbox(
sandbox,
structuredClone(clientConfig),
Daytona.createAxiosInstance(),
sandboxApi,
snapshotsApi,
)
}

cursor = response.data.nextCursor ?? undefined
Expand Down
77 changes: 63 additions & 14 deletions libs/sdk-typescript/src/Sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { SandboxState, SandboxApi, SandboxBackupStateEnum, Configuration } from '@daytona/api-client'
import {
SandboxState,
SandboxApi,
SandboxBackupStateEnum,
Configuration,
SnapshotsApi,
SnapshotState,
} from '@daytona/api-client'
import type {
Sandbox as SandboxDto,
SandboxListItem as SandboxListItemDto,
Expand All @@ -18,6 +25,7 @@ import type {
UpdateSandboxNetworkSettings,
SandboxListSortField,
SandboxListSortDirection,
SnapshotDto,
} from '@daytona/api-client'
import { Daytona } from './Daytona'
import type { Resources } from './Daytona'
Expand Down Expand Up @@ -127,6 +135,7 @@ export class Sandbox {
public toolboxProxyUrl: string

private infoApi: InfoApi
private readonly snapshotsApi: SnapshotsApi

/**
* Creates a new Sandbox instance
Expand All @@ -138,7 +147,14 @@ export class Sandbox {
private readonly clientConfig: Configuration,
private readonly axiosInstance: AxiosInstance,
private readonly sandboxApi: SandboxApi,
snapshotsApi?: SnapshotsApi,
) {
// clientConfig still targets the Daytona API at this point (it is repurposed
// for the toolbox below), so a default SnapshotsApi built from a copy of it
// polls the correct host when the caller does not provide one.
this.snapshotsApi =
snapshotsApi ?? new SnapshotsApi(new Configuration({ ...clientConfig }), '', Daytona.createAxiosInstance())

this.processSandboxDto(sandboxDto)

// Set the toolbox base URL
Expand Down Expand Up @@ -369,6 +385,7 @@ export class Sandbox {
structuredClone(this.clientConfig),
Daytona.createAxiosInstance(),
this.sandboxApi,
this.snapshotsApi,
)

const timeElapsed = Date.now() - startTime
Expand All @@ -382,7 +399,9 @@ export class Sandbox {
*
* This captures the Sandbox's filesystem into a reusable snapshot that can be
* used to create new Sandboxes. The Sandbox will temporarily enter a
* 'snapshotting' state and return to its previous state when complete.
* 'snapshotting' state and return to its previous state when complete. The
* method waits until the snapshot record reaches the 'active' state and throws
* a DaytonaError carrying the snapshot's error reason if the capture fails.
*
* @param {string} name - Name for the new snapshot
* @param {number} [timeout] - Maximum time to wait in seconds. 0 means no timeout.
Expand Down Expand Up @@ -412,25 +431,55 @@ export class Sandbox {

const timeElapsed = Date.now() - startTime
const remainingTimeout = timeout ? Math.max(0.001, timeout - timeElapsed / 1000) : timeout
await this.waitForSnapshotComplete(remainingTimeout)
await this.waitForSnapshotComplete(name, remainingTimeout)
}

private async waitForSnapshotComplete(timeout: number) {
private async waitForSnapshotComplete(name: string, timeout: number) {
let checkInterval = 100
const startTime = Date.now()

while (this.state === SandboxState.SNAPSHOTTING) {
await this.refreshData()

// @ts-expect-error this.refreshData() can modify this.state so this check is fine
if (this.state === SandboxState.ERROR || this.state === SandboxState.BUILD_FAILED) {
throw new DaytonaError(
`Sandbox ${this.id} snapshot failed with state: ${this.state}, error reason: ${this.errorReason}`,
)
while (true) {
let snapshot: SnapshotDto | undefined
try {
snapshot = (await this.snapshotsApi.getSnapshot(name)).data
} catch (error) {
if (!(error instanceof DaytonaNotFoundError)) {
throw error
}
// Legacy APIs create the snapshot record only after a successful capture.
// Tolerate the missing record while the sandbox is still snapshotting.
await this.refreshData()
if (this.state !== SandboxState.SNAPSHOTTING) {
// Pre-#4991 APIs persist the record and restore the sandbox state in separate steps,
// so a poll can observe the reverted sandbox before the record lands. Re-read once
// before declaring the capture failed.
try {
snapshot = (await this.snapshotsApi.getSnapshot(name)).data
} catch (graceError) {
if (!(graceError instanceof DaytonaNotFoundError)) {
throw graceError
}
throw new DaytonaError(
`Sandbox ${this.id} snapshot '${name}' failed: no snapshot record exists and sandbox is no longer snapshotting (state: ${this.state}, error reason: ${this.errorReason})`,
)
}
}
}

if (this.state !== SandboxState.SNAPSHOTTING) {
return
if (snapshot) {
if (snapshot.state === SnapshotState.ACTIVE) {
// Record polling bypasses the sandbox, so sync local data
// (state would otherwise stay SNAPSHOTTING) before resolving.
// Best-effort: a refresh failure must never reject a successful capture.
await this.refreshDataSafe()
return
}
if (snapshot.state === SnapshotState.ERROR || snapshot.state === SnapshotState.BUILD_FAILED) {
await this.refreshDataSafe()
throw new DaytonaError(
`Snapshot ${name} failed with state: ${snapshot.state}, error reason: ${snapshot.errorReason}`,
)
}
}

if (timeout !== 0 && Date.now() - startTime > timeout * 1000) {
Expand Down
Loading