From 47817bfe75a6fd8ad99682242d0669203d4bcf71 Mon Sep 17 00:00:00 2001 From: katereznykova Date: Tue, 21 Oct 2025 11:58:00 +0100 Subject: [PATCH 1/5] configurable sleepAfter --- packages/sandbox/src/sandbox.ts | 19 +++- packages/sandbox/tests/get-sandbox.test.ts | 107 +++++++++++++++++++++ packages/shared/src/index.ts | 2 + packages/shared/src/types.ts | 25 ++++- 4 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 packages/sandbox/tests/get-sandbox.test.ts diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 8d49cbae..3e320a19 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,32 @@ 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) { + // Set sleepAfter property on the stub + // Cast to any to work around RPC type inference issues + (stub as any).sleepAfter = 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 = "3m"; // Sleep the sandbox if no requests are made in this timeframe client: SandboxClient; private codeInterpreter: CodeInterpreter; diff --git a/packages/sandbox/tests/get-sandbox.test.ts b/packages/sandbox/tests/get-sandbox.test.ts new file mode 100644 index 00000000..31053002 --- /dev/null +++ b/packages/sandbox/tests/get-sandbox.test.ts @@ -0,0 +1,107 @@ +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 = '3m'; + 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: '3m', + setSandboxName: vi.fn(), + setBaseUrl: vi.fn(), + }; + + // 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('3m'); + }); + + 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..92e41d2b 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: "3m" (3 minutes) + */ + sleepAfter?: string | number; + + /** + * Base URL for the sandbox API + */ + baseUrl?: string; +} + /** * Execution session - isolated execution context within a sandbox * Returned by sandbox.createSession() From cbcda36a27a05dc3036bdeee9d5fd80b56da2ba9 Mon Sep 17 00:00:00 2001 From: katereznykova Date: Tue, 21 Oct 2025 12:05:40 +0100 Subject: [PATCH 2/5] fix types casting --- packages/sandbox/src/sandbox.ts | 9 ++++++--- packages/sandbox/tests/get-sandbox.test.ts | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 3e320a19..aa48be89 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -45,9 +45,7 @@ export function getSandbox( } if (options?.sleepAfter !== undefined) { - // Set sleepAfter property on the stub - // Cast to any to work around RPC type inference issues - (stub as any).sleepAfter = options.sleepAfter; + stub.setSleepAfter(options.sleepAfter); } return stub; @@ -127,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 index 31053002..bfcece8c 100644 --- a/packages/sandbox/tests/get-sandbox.test.ts +++ b/packages/sandbox/tests/get-sandbox.test.ts @@ -27,6 +27,9 @@ describe('getSandbox', () => { sleepAfter: '3m', setSandboxName: vi.fn(), setBaseUrl: vi.fn(), + setSleepAfter: vi.fn((value: string | number) => { + mockStub.sleepAfter = value; + }), }; // Mock getContainer to return our stub From 08f4b0c2eaa8a0b815b4565d774100a6c8b86e00 Mon Sep 17 00:00:00 2001 From: katereznykova Date: Tue, 21 Oct 2025 12:23:14 +0100 Subject: [PATCH 3/5] increase default to 10m --- packages/sandbox/src/sandbox.ts | 2 +- packages/sandbox/tests/get-sandbox.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index aa48be89..f2ec2534 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -53,7 +53,7 @@ export function getSandbox( export class Sandbox extends Container implements ISandbox { defaultPort = 3000; // Default port for the container's Bun server - sleepAfter: string | number = "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; diff --git a/packages/sandbox/tests/get-sandbox.test.ts b/packages/sandbox/tests/get-sandbox.test.ts index bfcece8c..6f77f175 100644 --- a/packages/sandbox/tests/get-sandbox.test.ts +++ b/packages/sandbox/tests/get-sandbox.test.ts @@ -6,7 +6,7 @@ vi.mock('@cloudflare/containers', () => ({ Container: class Container { ctx: any; env: any; - sleepAfter: string | number = '3m'; + sleepAfter: string | number = '10m'; constructor(ctx: any, env: any) { this.ctx = ctx; this.env = env; @@ -24,7 +24,7 @@ describe('getSandbox', () => { // Create a fresh mock stub for each test mockStub = { - sleepAfter: '3m', + sleepAfter: '10m', setSandboxName: vi.fn(), setBaseUrl: vi.fn(), setSleepAfter: vi.fn((value: string | number) => { @@ -89,7 +89,7 @@ describe('getSandbox', () => { const sandbox = getSandbox(mockNamespace, 'test-sandbox'); // Should remain default value from Container - expect(sandbox.sleepAfter).toBe('3m'); + expect(sandbox.sleepAfter).toBe('10m'); }); it('should accept various time string formats for sleepAfter', () => { From 0cf59c6d7adee9abff5d5929d2078641255b918d Mon Sep 17 00:00:00 2001 From: whoiskatrin Date: Tue, 21 Oct 2025 12:24:41 +0100 Subject: [PATCH 4/5] Update default value in types.ts --- packages/shared/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 92e41d2b..eb7e2bbd 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -261,7 +261,7 @@ export interface SandboxOptions { * Can be: * - A string like "30s", "3m", "5m", "1h" (seconds, minutes, or hours) * - A number representing seconds (e.g., 180 for 3 minutes) - * Default: "3m" (3 minutes) + * Default: "10m" (10 minutes) */ sleepAfter?: string | number; From bf6d758772bb204389e618713b109bf0fc46cd5c Mon Sep 17 00:00:00 2001 From: whoiskatrin Date: Tue, 21 Oct 2025 12:25:07 +0100 Subject: [PATCH 5/5] Create light-berries-sort.md --- .changeset/light-berries-sort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/light-berries-sort.md 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