Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/light-berries-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/sandbox": patch
---

configurable sleepAfter
22 changes: 17 additions & 5 deletions packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ProcessOptions,
ProcessStatus,
RunCodeOptions,
SandboxOptions,
SessionOptions,
StreamOptions
} from "@repo/shared";
Expand All @@ -29,24 +30,30 @@ import {
} from "./security";
import { parseSSEStream } from "./sse-parser";

export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string, options?: {
baseUrl: string
}) {
export function getSandbox(
ns: DurableObjectNamespace<Sandbox>,
id: string,
options?: SandboxOptions
) {
const stub = getContainer(ns, id);

// Store the name on first access
stub.setSandboxName?.(id);

if(options?.baseUrl) {
if (options?.baseUrl) {
stub.setBaseUrl(options.baseUrl);
}

if (options?.sleepAfter !== undefined) {
stub.setSleepAfter(options.sleepAfter);
}

return stub;
}

export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
defaultPort = 3000; // Default port for the container's Bun server
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
sleepAfter: string | number = "10m"; // Sleep the sandbox if no requests are made in this timeframe

client: SandboxClient;
private codeInterpreter: CodeInterpreter;
Expand Down Expand Up @@ -118,6 +125,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
}
}

// RPC method to set the sleep timeout
async setSleepAfter(sleepAfter: string | number): Promise<void> {
this.sleepAfter = sleepAfter;
}

// RPC method to set environment variables
async setEnvVars(envVars: Record<string, string>): Promise<void> {
// Update local state for new sessions
Expand Down
110 changes: 110 additions & 0 deletions packages/sandbox/tests/get-sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getSandbox } from '../src/sandbox';

// Mock the Container module
vi.mock('@cloudflare/containers', () => ({
Container: class Container {
ctx: any;
env: any;
sleepAfter: string | number = '10m';
constructor(ctx: any, env: any) {
this.ctx = ctx;
this.env = env;
}
},
getContainer: vi.fn(),
}));

describe('getSandbox', () => {
let mockStub: any;
let mockGetContainer: any;

beforeEach(async () => {
vi.clearAllMocks();

// Create a fresh mock stub for each test
mockStub = {
sleepAfter: '10m',
setSandboxName: vi.fn(),
setBaseUrl: vi.fn(),
setSleepAfter: vi.fn((value: string | number) => {
mockStub.sleepAfter = value;
}),
};

// Mock getContainer to return our stub
const containers = await import('@cloudflare/containers');
mockGetContainer = vi.mocked(containers.getContainer);
mockGetContainer.mockReturnValue(mockStub);
});

it('should create a sandbox instance with default sleepAfter', () => {
const mockNamespace = {} as any;
const sandbox = getSandbox(mockNamespace, 'test-sandbox');

expect(sandbox).toBeDefined();
expect(sandbox.setSandboxName).toHaveBeenCalledWith('test-sandbox');
});

it('should apply sleepAfter option when provided as string', () => {
const mockNamespace = {} as any;
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
sleepAfter: '5m',
});

expect(sandbox.sleepAfter).toBe('5m');
});

it('should apply sleepAfter option when provided as number', () => {
const mockNamespace = {} as any;
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
sleepAfter: 300, // 5 minutes in seconds
});

expect(sandbox.sleepAfter).toBe(300);
});

it('should apply baseUrl option when provided', () => {
const mockNamespace = {} as any;
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
baseUrl: 'https://example.com',
});

expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
});

it('should apply both sleepAfter and baseUrl options together', () => {
const mockNamespace = {} as any;
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
sleepAfter: '10m',
baseUrl: 'https://example.com',
});

expect(sandbox.sleepAfter).toBe('10m');
expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
});

it('should not apply sleepAfter when not provided', () => {
const mockNamespace = {} as any;
const sandbox = getSandbox(mockNamespace, 'test-sandbox');

// Should remain default value from Container
expect(sandbox.sleepAfter).toBe('10m');
});

it('should accept various time string formats for sleepAfter', () => {
const mockNamespace = {} as any;
const testCases = ['30s', '1m', '10m', '1h', '2h'];

for (const timeString of testCases) {
// Reset the mock stub for each iteration
mockStub.sleepAfter = '3m';

const sandbox = getSandbox(mockNamespace, `test-sandbox-${timeString}`, {
sleepAfter: timeString,
});

expect(sandbox.sleepAfter).toBe(timeString);
}
});
});
2 changes: 2 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export type {
ProcessStatus,
ReadFileResult,
RenameFileResult,
// Sandbox configuration options
SandboxOptions,
// Session management result types
SessionCreateResult,
SessionDeleteResult,
Expand Down
25 changes: 21 additions & 4 deletions packages/shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,28 +232,45 @@ export interface SessionOptions {
* Optional session ID (auto-generated if not provided)
*/
id?: string;

/**
* Session name for identification
*/
name?: string;

/**
* Environment variables for this session
*/
env?: Record<string, string>;

/**
* Working directory
*/
cwd?: string;

/**
* Enable PID namespace isolation (requires CAP_SYS_ADMIN)
*/
isolation?: boolean;
}

// Sandbox configuration options
export interface SandboxOptions {
/**
* Duration after which the sandbox instance will sleep if no requests are received
* Can be:
* - A string like "30s", "3m", "5m", "1h" (seconds, minutes, or hours)
* - A number representing seconds (e.g., 180 for 3 minutes)
* Default: "10m" (10 minutes)
*/
sleepAfter?: string | number;

/**
* Base URL for the sandbox API
*/
baseUrl?: string;
}

/**
* Execution session - isolated execution context within a sandbox
* Returned by sandbox.createSession()
Expand Down
Loading