Skip to content

Commit e79ac80

Browse files
authored
configurable sleepAfter (#127)
* configurable sleepAfter * fix types casting * increase default to 10m * Update default value in types.ts * Create light-berries-sort.md
1 parent 104f455 commit e79ac80

File tree

5 files changed

+155
-9
lines changed

5 files changed

+155
-9
lines changed

.changeset/light-berries-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/sandbox": patch
3+
---
4+
5+
configurable sleepAfter

packages/sandbox/src/sandbox.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
ProcessOptions,
1414
ProcessStatus,
1515
RunCodeOptions,
16+
SandboxOptions,
1617
SessionOptions,
1718
StreamOptions
1819
} from "@repo/shared";
@@ -29,24 +30,30 @@ import {
2930
} from "./security";
3031
import { parseSSEStream } from "./sse-parser";
3132

32-
export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string, options?: {
33-
baseUrl: string
34-
}) {
33+
export function getSandbox(
34+
ns: DurableObjectNamespace<Sandbox>,
35+
id: string,
36+
options?: SandboxOptions
37+
) {
3538
const stub = getContainer(ns, id);
3639

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

40-
if(options?.baseUrl) {
43+
if (options?.baseUrl) {
4144
stub.setBaseUrl(options.baseUrl);
4245
}
4346

47+
if (options?.sleepAfter !== undefined) {
48+
stub.setSleepAfter(options.sleepAfter);
49+
}
50+
4451
return stub;
4552
}
4653

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

5158
client: SandboxClient;
5259
private codeInterpreter: CodeInterpreter;
@@ -118,6 +125,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
118125
}
119126
}
120127

128+
// RPC method to set the sleep timeout
129+
async setSleepAfter(sleepAfter: string | number): Promise<void> {
130+
this.sleepAfter = sleepAfter;
131+
}
132+
121133
// RPC method to set environment variables
122134
async setEnvVars(envVars: Record<string, string>): Promise<void> {
123135
// Update local state for new sessions
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { getSandbox } from '../src/sandbox';
3+
4+
// Mock the Container module
5+
vi.mock('@cloudflare/containers', () => ({
6+
Container: class Container {
7+
ctx: any;
8+
env: any;
9+
sleepAfter: string | number = '10m';
10+
constructor(ctx: any, env: any) {
11+
this.ctx = ctx;
12+
this.env = env;
13+
}
14+
},
15+
getContainer: vi.fn(),
16+
}));
17+
18+
describe('getSandbox', () => {
19+
let mockStub: any;
20+
let mockGetContainer: any;
21+
22+
beforeEach(async () => {
23+
vi.clearAllMocks();
24+
25+
// Create a fresh mock stub for each test
26+
mockStub = {
27+
sleepAfter: '10m',
28+
setSandboxName: vi.fn(),
29+
setBaseUrl: vi.fn(),
30+
setSleepAfter: vi.fn((value: string | number) => {
31+
mockStub.sleepAfter = value;
32+
}),
33+
};
34+
35+
// Mock getContainer to return our stub
36+
const containers = await import('@cloudflare/containers');
37+
mockGetContainer = vi.mocked(containers.getContainer);
38+
mockGetContainer.mockReturnValue(mockStub);
39+
});
40+
41+
it('should create a sandbox instance with default sleepAfter', () => {
42+
const mockNamespace = {} as any;
43+
const sandbox = getSandbox(mockNamespace, 'test-sandbox');
44+
45+
expect(sandbox).toBeDefined();
46+
expect(sandbox.setSandboxName).toHaveBeenCalledWith('test-sandbox');
47+
});
48+
49+
it('should apply sleepAfter option when provided as string', () => {
50+
const mockNamespace = {} as any;
51+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
52+
sleepAfter: '5m',
53+
});
54+
55+
expect(sandbox.sleepAfter).toBe('5m');
56+
});
57+
58+
it('should apply sleepAfter option when provided as number', () => {
59+
const mockNamespace = {} as any;
60+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
61+
sleepAfter: 300, // 5 minutes in seconds
62+
});
63+
64+
expect(sandbox.sleepAfter).toBe(300);
65+
});
66+
67+
it('should apply baseUrl option when provided', () => {
68+
const mockNamespace = {} as any;
69+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
70+
baseUrl: 'https://example.com',
71+
});
72+
73+
expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
74+
});
75+
76+
it('should apply both sleepAfter and baseUrl options together', () => {
77+
const mockNamespace = {} as any;
78+
const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
79+
sleepAfter: '10m',
80+
baseUrl: 'https://example.com',
81+
});
82+
83+
expect(sandbox.sleepAfter).toBe('10m');
84+
expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
85+
});
86+
87+
it('should not apply sleepAfter when not provided', () => {
88+
const mockNamespace = {} as any;
89+
const sandbox = getSandbox(mockNamespace, 'test-sandbox');
90+
91+
// Should remain default value from Container
92+
expect(sandbox.sleepAfter).toBe('10m');
93+
});
94+
95+
it('should accept various time string formats for sleepAfter', () => {
96+
const mockNamespace = {} as any;
97+
const testCases = ['30s', '1m', '10m', '1h', '2h'];
98+
99+
for (const timeString of testCases) {
100+
// Reset the mock stub for each iteration
101+
mockStub.sleepAfter = '3m';
102+
103+
const sandbox = getSandbox(mockNamespace, `test-sandbox-${timeString}`, {
104+
sleepAfter: timeString,
105+
});
106+
107+
expect(sandbox.sleepAfter).toBe(timeString);
108+
}
109+
});
110+
});

packages/shared/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export type {
8686
ProcessStatus,
8787
ReadFileResult,
8888
RenameFileResult,
89+
// Sandbox configuration options
90+
SandboxOptions,
8991
// Session management result types
9092
SessionCreateResult,
9193
SessionDeleteResult,

packages/shared/src/types.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,28 +232,45 @@ export interface SessionOptions {
232232
* Optional session ID (auto-generated if not provided)
233233
*/
234234
id?: string;
235-
235+
236236
/**
237237
* Session name for identification
238238
*/
239239
name?: string;
240-
240+
241241
/**
242242
* Environment variables for this session
243243
*/
244244
env?: Record<string, string>;
245-
245+
246246
/**
247247
* Working directory
248248
*/
249249
cwd?: string;
250-
250+
251251
/**
252252
* Enable PID namespace isolation (requires CAP_SYS_ADMIN)
253253
*/
254254
isolation?: boolean;
255255
}
256256

257+
// Sandbox configuration options
258+
export interface SandboxOptions {
259+
/**
260+
* Duration after which the sandbox instance will sleep if no requests are received
261+
* Can be:
262+
* - A string like "30s", "3m", "5m", "1h" (seconds, minutes, or hours)
263+
* - A number representing seconds (e.g., 180 for 3 minutes)
264+
* Default: "10m" (10 minutes)
265+
*/
266+
sleepAfter?: string | number;
267+
268+
/**
269+
* Base URL for the sandbox API
270+
*/
271+
baseUrl?: string;
272+
}
273+
257274
/**
258275
* Execution session - isolated execution context within a sandbox
259276
* Returned by sandbox.createSession()

0 commit comments

Comments
 (0)