diff --git a/.changeset/light-berries-sort.md b/.changeset/light-berries-sort.md new file mode 100644 index 00000000..8ba3bf81 --- /dev/null +++ b/.changeset/light-berries-sort.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/sandbox": patch +--- + +configurable sleepAfter diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 8d49cbae..f2ec2534 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -13,6 +13,7 @@ import type { ProcessOptions, ProcessStatus, RunCodeOptions, + SandboxOptions, SessionOptions, StreamOptions } from "@repo/shared"; @@ -29,24 +30,30 @@ import { } from "./security"; import { parseSSEStream } from "./sse-parser"; -export function getSandbox(ns: DurableObjectNamespace, id: string, options?: { - baseUrl: string -}) { +export function getSandbox( + ns: DurableObjectNamespace, + 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 extends Container 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; @@ -118,6 +125,11 @@ export class Sandbox extends Container implements ISandbox { } } + // RPC method to set the sleep timeout + async setSleepAfter(sleepAfter: string | number): Promise { + this.sleepAfter = sleepAfter; + } + // RPC method to set environment variables async setEnvVars(envVars: Record): Promise { // Update local state for new sessions diff --git a/packages/sandbox/tests/get-sandbox.test.ts b/packages/sandbox/tests/get-sandbox.test.ts new file mode 100644 index 00000000..6f77f175 --- /dev/null +++ b/packages/sandbox/tests/get-sandbox.test.ts @@ -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); + } + }); +}); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 05b04e67..25e6f8f5 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -86,6 +86,8 @@ export type { ProcessStatus, ReadFileResult, RenameFileResult, + // Sandbox configuration options + SandboxOptions, // Session management result types SessionCreateResult, SessionDeleteResult, diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 732fbb87..eb7e2bbd 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -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; - + /** * 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()