Skip to content
Draft
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
21 changes: 17 additions & 4 deletions src/resources/devboxes/devboxes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { type Response } from '../../_shims/index';
import {
longPollUntil,
LongPollRequestOptions,
PollingTimeoutError,
resolveLongPollTimeoutMs,
} from '@runloop/api-client/lib/polling';
import { awaitDevboxState } from '@runloop/api-client/lib/devbox-state';
Expand All @@ -47,6 +48,11 @@ import { uuidv7 } from 'uuidv7';
type DevboxStatus = DevboxView['status'];
const DEVBOX_BOOTING_STATES: DevboxStatus[] = ['provisioning', 'initializing'];

export type CreateAndAwaitRunningOptions = LongPollRequestOptions<DevboxView> & {
/** Shutdown the created devbox when waiting for it to run times out. Defaults to true. */
shutdownOnTimeout?: boolean;
};

export class Devboxes extends APIResource {
diskSnapshots: DiskSnapshotsAPI.DiskSnapshots = new DiskSnapshotsAPI.DiskSnapshots(this._client);
logs: LogsAPI.Logs = new LogsAPI.Logs(this._client);
Expand Down Expand Up @@ -124,15 +130,22 @@ export class Devboxes extends APIResource {
* This is a convenience method that combines create() and awaitDevboxRunning().
*
* @param body - DevboxCreateParams
* @param options - request options with optional long-poll configuration.
* @param options - request options with optional long-poll and timeout cleanup configuration.
*/
async createAndAwaitRunning(
body?: DevboxCreateParams,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<DevboxView> {
const { longPoll, polling, ...requestOptions } = options ?? {};
const { longPoll, polling, shutdownOnTimeout = true, ...requestOptions } = options ?? {};
const devbox = await this.create(body, requestOptions);
return this.awaitRunning(devbox.id, { ...requestOptions, longPoll, polling });
try {
return await this.awaitRunning(devbox.id, { ...requestOptions, longPoll, polling });
} catch (error) {
if (!(error instanceof PollingTimeoutError)) throw error;
if (!shutdownOnTimeout) return devbox;
await this.shutdown(devbox.id);
throw error;
}
}
/**
* Updates a devbox by doing a complete update the existing name,metadata fields.
Expand Down
21 changes: 9 additions & 12 deletions src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { Secret } from './sdk/secret';
import type {
DevboxCreateParams,
DevboxListParams,
DevboxView,
DevboxListDiskSnapshotsParams,
CreateAndAwaitRunningOptions,
} from './resources/devboxes/devboxes';
import type { BlueprintListParams } from './resources/blueprints';
import type { ObjectCreateParams, ObjectListParams } from './resources/objects';
Expand Down Expand Up @@ -581,13 +581,10 @@ export class DevboxOps {
* ```
*
* @param {SDKDevboxCreateParams} [params] - Parameters for creating the devbox, with SDK mount syntax support.
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration.
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration.
* @returns {Promise<Devbox>} A {@link Devbox} instance.
*/
async create(
params?: SDKDevboxCreateParams,
options?: LongPollRequestOptions<DevboxView>,
): Promise<Devbox> {
async create(params?: SDKDevboxCreateParams, options?: CreateAndAwaitRunningOptions): Promise<Devbox> {
const transformedParams = transformSDKDevboxCreateParams(params);
return Devbox.create(this.client, transformedParams, options);
}
Expand All @@ -596,13 +593,13 @@ export class DevboxOps {
* Create a new devbox from a blueprint ID.
* @param {string} blueprintId - The ID of the blueprint to use.
* @param {Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>} [params] - Additional parameters for creating the devbox (excluding blueprint_id, snapshot_id, and blueprint_name).
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration.
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration.
* @returns {Promise<Devbox>} A {@link Devbox} instance.
*/
async createFromBlueprintId(
blueprintId: string,
params?: Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<Devbox> {
return Devbox.createFromBlueprintId(this.client, blueprintId, params, options);
}
Expand All @@ -611,13 +608,13 @@ export class DevboxOps {
* Create a new devbox from a blueprint name.
* @param {string} blueprintName - The name of the blueprint to use.
* @param {Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>} [params] - Additional parameters for creating the devbox (excluding blueprint_id, snapshot_id, and blueprint_name).
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration.
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration.
* @returns {Promise<Devbox>} A {@link Devbox} instance.
*/
async createFromBlueprintName(
blueprintName: string,
params?: Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<Devbox> {
return Devbox.createFromBlueprintName(this.client, blueprintName, params, options);
}
Expand All @@ -636,13 +633,13 @@ export class DevboxOps {
*
* @param {string} snapshotId - The ID of the snapshot to use.
* @param {Omit<DevboxCreateParams, 'snapshot_id' | 'blueprint_id' | 'blueprint_name'>} [params] - Additional parameters for creating the devbox (excluding snapshot_id, blueprint_id, and blueprint_name).
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration.
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration.
* @returns {Promise<Devbox>} A {@link Devbox} instance.
*/
async createFromSnapshot(
snapshotId: string,
params?: Omit<DevboxCreateParams, 'snapshot_id' | 'blueprint_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<Devbox> {
return Devbox.createFromSnapshot(this.client, snapshotId, params, options);
}
Expand Down
25 changes: 13 additions & 12 deletions src/sdk/devbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
DevboxSnapshotView,
DevboxKeepAliveResponse,
TunnelView,
CreateAndAwaitRunningOptions,
} from '../resources/devboxes/devboxes';
import type { DevboxLogsListView, LogListParams } from '../resources/devboxes/logs';
import { LongPollRequestOptions, PollingOptions } from '../lib/polling';
Expand Down Expand Up @@ -565,13 +566,13 @@ export class Devbox {
*
* @param {Runloop} client - The Runloop client instance
* @param {DevboxCreateParams} [params] - Parameters for creating the devbox
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance, normally in the running state
*/
static async create(
client: Runloop,
params?: DevboxCreateParams,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<Devbox> {
const devboxData = await client.devboxes.createAndAwaitRunning(params, options);
return new Devbox(client, devboxData.id);
Expand All @@ -586,14 +587,14 @@ export class Devbox {
* @param {Runloop} client - The Runloop client instance
* @param {string} blueprintId - The blueprint ID to create from
* @param {Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>} [params] - Additional devbox creation parameters
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance, normally in the running state
*/
static async createFromBlueprintId(
client: Runloop,
blueprintId: string,
params?: Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<Devbox> {
const createParams: DevboxCreateParams = {
...params,
Expand All @@ -612,14 +613,14 @@ export class Devbox {
* @param {Runloop} client - The Runloop client instance
* @param {string} blueprintName - The blueprint name to create from
* @param {Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>} [params] - Additional devbox creation parameters
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance, normally in the running state
*/
static async createFromBlueprintName(
client: Runloop,
blueprintName: string,
params?: Omit<DevboxCreateParams, 'blueprint_id' | 'snapshot_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<Devbox> {
const createParams: DevboxCreateParams = {
...params,
Expand Down Expand Up @@ -647,14 +648,14 @@ export class Devbox {
* @param {Runloop} client - The Runloop client instance
* @param {string} snapshotId - The snapshot ID to create from
* @param {Omit<DevboxCreateParams, 'snapshot_id' | 'blueprint_id' | 'blueprint_name'>} [params] - Additional devbox creation parameters
* @param {LongPollRequestOptions<DevboxView>} [options] - Request options with optional long-poll configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance in the running state
* @param {CreateAndAwaitRunningOptions} [options] - Request options with optional long-poll and timeout cleanup configuration
* @returns {Promise<Devbox>} A {@link Devbox} instance, normally in the running state
*/
static async createFromSnapshot(
client: Runloop,
snapshotId: string,
params?: Omit<DevboxCreateParams, 'snapshot_id' | 'blueprint_id' | 'blueprint_name'>,
options?: LongPollRequestOptions<DevboxView>,
options?: CreateAndAwaitRunningOptions,
): Promise<Devbox> {
const createParams: DevboxCreateParams = {
...params,
Expand Down
30 changes: 30 additions & 0 deletions tests/api-resources/devboxes/devboxes.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

import { Runloop, toFile } from '@runloop/api-client';
import type { DevboxView } from '../../../src/resources/devboxes/devboxes';
import { Response } from 'node-fetch';
import { APIError } from '../../../src/error';
import { PollingTimeoutError } from '../../../src/lib/polling';

const client = new Runloop({
bearerToken: 'My Bearer Token',
Expand Down Expand Up @@ -745,6 +747,34 @@ describe('resource devboxes', () => {
mockPost.mockRestore();
});

test('createAndAwaitRunning: shuts down the devbox and rethrows when waiting times out', async () => {
const devbox = { id: 'new-devbox-id', status: 'provisioning' } as DevboxView;
const timeout = new PollingTimeoutError('Timed out', devbox);
jest.spyOn(client.devboxes, 'create').mockResolvedValueOnce(devbox);
jest.spyOn(client.devboxes, 'awaitRunning').mockRejectedValueOnce(timeout);
const shutdown = jest.spyOn(client.devboxes, 'shutdown').mockResolvedValueOnce(devbox);

await expect(client.devboxes.createAndAwaitRunning()).rejects.toBe(timeout);

expect(shutdown).toHaveBeenCalledWith('new-devbox-id');
jest.restoreAllMocks();
});

test('createAndAwaitRunning: returns the devbox without shutting it down when configured', async () => {
const devbox = { id: 'new-devbox-id', status: 'provisioning' } as DevboxView;
const timeout = new PollingTimeoutError('Timed out', devbox);
jest.spyOn(client.devboxes, 'create').mockResolvedValueOnce(devbox);
jest.spyOn(client.devboxes, 'awaitRunning').mockRejectedValueOnce(timeout);
const shutdown = jest.spyOn(client.devboxes, 'shutdown');

await expect(
client.devboxes.createAndAwaitRunning(undefined, { shutdownOnTimeout: false }),
).resolves.toBe(devbox);

expect(shutdown).not.toHaveBeenCalled();
jest.restoreAllMocks();
});

test('executeAndAwaitCompletion: passes last_n to waitForCommand when execute is not completed', async () => {
const mockPost = jest.spyOn(client.devboxes['_client'], 'post');
try {
Expand Down
4 changes: 2 additions & 2 deletions tests/objects/devbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ describe('Devbox (New API)', () => {
it('should pass options to the API client', async () => {
mockClient.devboxes.createAndAwaitRunning.mockResolvedValue(mockDevboxData);

await Devbox.create(mockClient, { name: 'test-devbox' }, { polling: { maxAttempts: 10 } });
await Devbox.create(mockClient, { name: 'test-devbox' }, { shutdownOnTimeout: false });

expect(mockClient.devboxes.createAndAwaitRunning).toHaveBeenCalledWith(
{ name: 'test-devbox' },
{ polling: { maxAttempts: 10 } },
{ shutdownOnTimeout: false },
);
});
});
Expand Down
Loading