@cloudflare/sandbox 0.0.0-e53d7e7 → 0.0.0-e79ac80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/Dockerfile +12 -48
- package/README.md +0 -2
- package/dist/{chunk-53JFOF7F.js → chunk-WK36EMRB.js} +32 -10
- package/dist/chunk-WK36EMRB.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/interpreter.d.ts +1 -1
- package/dist/request-handler.d.ts +1 -1
- package/dist/request-handler.js +1 -1
- package/dist/{sandbox-D9K2ypln.d.ts → sandbox-B1TT7PZP.d.ts} +8 -5
- package/dist/sandbox.d.ts +1 -1
- package/dist/sandbox.js +1 -1
- package/package.json +1 -1
- package/src/sandbox.ts +47 -16
- package/tests/get-sandbox.test.ts +110 -0
- package/dist/chunk-53JFOF7F.js.map +0 -1
package/dist/interpreter.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CreateContextOptions, CodeContext, RunCodeOptions, Execution } from '@repo/shared';
|
|
2
|
-
import { b as Sandbox } from './sandbox-
|
|
2
|
+
import { b as Sandbox } from './sandbox-B1TT7PZP.js';
|
|
3
3
|
import 'cloudflare:workers';
|
|
4
4
|
import '@cloudflare/containers';
|
|
5
5
|
|
package/dist/request-handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _repo_shared from '@repo/shared';
|
|
2
|
-
import { Logger, MkdirResult, WriteFileResult, ReadFileResult, DeleteFileResult, RenameFileResult, MoveFileResult, ListFilesOptions, ListFilesResult, GitCheckoutResult, CreateContextOptions, CodeContext, OutputMessage, Result, ExecutionError, PortExposeResult, PortCloseResult, PortListResult, ProcessStartResult, ProcessListResult, ProcessInfoResult, ProcessKillResult, ProcessCleanupResult, ProcessLogsResult, ISandbox, ExecOptions, ExecResult, ProcessOptions, Process, StreamOptions, SessionOptions, ExecutionSession, RunCodeOptions, ExecutionResult } from '@repo/shared';
|
|
2
|
+
import { Logger, MkdirResult, WriteFileResult, ReadFileResult, DeleteFileResult, RenameFileResult, MoveFileResult, ListFilesOptions, ListFilesResult, GitCheckoutResult, CreateContextOptions, CodeContext, OutputMessage, Result, ExecutionError, PortExposeResult, PortCloseResult, PortListResult, ProcessStartResult, ProcessListResult, ProcessInfoResult, ProcessKillResult, ProcessCleanupResult, ProcessLogsResult, ISandbox, ExecOptions, ExecResult, ProcessOptions, Process, StreamOptions, SessionOptions, ExecutionSession, RunCodeOptions, ExecutionResult, SandboxOptions } from '@repo/shared';
|
|
3
3
|
import { DurableObject } from 'cloudflare:workers';
|
|
4
4
|
import { Container } from '@cloudflare/containers';
|
|
5
5
|
|
|
@@ -435,12 +435,10 @@ declare class SandboxClient {
|
|
|
435
435
|
constructor(options: HttpClientOptions);
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
declare function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string, options?:
|
|
439
|
-
baseUrl: string;
|
|
440
|
-
}): DurableObjectStub<Sandbox<unknown>>;
|
|
438
|
+
declare function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string, options?: SandboxOptions): DurableObjectStub<Sandbox<unknown>>;
|
|
441
439
|
declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
442
440
|
defaultPort: number;
|
|
443
|
-
sleepAfter: string;
|
|
441
|
+
sleepAfter: string | number;
|
|
444
442
|
client: SandboxClient;
|
|
445
443
|
private codeInterpreter;
|
|
446
444
|
private sandboxName;
|
|
@@ -452,6 +450,7 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
|
|
|
452
450
|
constructor(ctx: DurableObject['ctx'], env: Env);
|
|
453
451
|
setSandboxName(name: string): Promise<void>;
|
|
454
452
|
setBaseUrl(baseUrl: string): Promise<void>;
|
|
453
|
+
setSleepAfter(sleepAfter: string | number): Promise<void>;
|
|
455
454
|
setEnvVars(envVars: Record<string, string>): Promise<void>;
|
|
456
455
|
/**
|
|
457
456
|
* Cleanup and destroy the sandbox container
|
|
@@ -465,6 +464,10 @@ declare class Sandbox<Env = unknown> extends Container<Env> implements ISandbox
|
|
|
465
464
|
/**
|
|
466
465
|
* Ensure default session exists - lazy initialization
|
|
467
466
|
* This is called automatically by all public methods that need a session
|
|
467
|
+
*
|
|
468
|
+
* The session is persisted to Durable Object storage to survive hot reloads
|
|
469
|
+
* during development. If a session already exists in the container after reload,
|
|
470
|
+
* we reuse it instead of trying to create a new one.
|
|
468
471
|
*/
|
|
469
472
|
private ensureDefaultSession;
|
|
470
473
|
exec(command: string, options?: ExecOptions): Promise<ExecResult>;
|
package/dist/sandbox.d.ts
CHANGED
package/dist/sandbox.js
CHANGED
package/package.json
CHANGED
package/src/sandbox.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ProcessOptions,
|
|
14
14
|
ProcessStatus,
|
|
15
15
|
RunCodeOptions,
|
|
16
|
+
SandboxOptions,
|
|
16
17
|
SessionOptions,
|
|
17
18
|
StreamOptions
|
|
18
19
|
} from "@repo/shared";
|
|
@@ -29,24 +30,30 @@ import {
|
|
|
29
30
|
} from "./security";
|
|
30
31
|
import { parseSSEStream } from "./sse-parser";
|
|
31
32
|
|
|
32
|
-
export function getSandbox(
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
export function getSandbox(
|
|
34
|
+
ns: DurableObjectNamespace<Sandbox>,
|
|
35
|
+
id: string,
|
|
36
|
+
options?: SandboxOptions
|
|
37
|
+
) {
|
|
35
38
|
const stub = getContainer(ns, id);
|
|
36
39
|
|
|
37
40
|
// Store the name on first access
|
|
38
41
|
stub.setSandboxName?.(id);
|
|
39
42
|
|
|
40
|
-
if(options?.baseUrl) {
|
|
43
|
+
if (options?.baseUrl) {
|
|
41
44
|
stub.setBaseUrl(options.baseUrl);
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
if (options?.sleepAfter !== undefined) {
|
|
48
|
+
stub.setSleepAfter(options.sleepAfter);
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
return stub;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
48
55
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
49
|
-
sleepAfter = "
|
|
56
|
+
sleepAfter: string | number = "10m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
50
57
|
|
|
51
58
|
client: SandboxClient;
|
|
52
59
|
private codeInterpreter: CodeInterpreter;
|
|
@@ -84,9 +91,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
84
91
|
// The CodeInterpreter extracts client.interpreter from the sandbox
|
|
85
92
|
this.codeInterpreter = new CodeInterpreter(this);
|
|
86
93
|
|
|
87
|
-
// Load the sandbox name
|
|
94
|
+
// Load the sandbox name, port tokens, and default session from storage on initialization
|
|
88
95
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
89
96
|
this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
|
|
97
|
+
this.defaultSession = await this.ctx.storage.get<string>('defaultSession') || null;
|
|
90
98
|
const storedTokens = await this.ctx.storage.get<Record<string, string>>('portTokens') || {};
|
|
91
99
|
|
|
92
100
|
// Convert stored tokens back to Map
|
|
@@ -110,7 +118,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
110
118
|
if (!this.baseUrl) {
|
|
111
119
|
this.baseUrl = baseUrl;
|
|
112
120
|
await this.ctx.storage.put('baseUrl', baseUrl);
|
|
113
|
-
console.log(`[Sandbox] Stored base URL: ${baseUrl}`);
|
|
114
121
|
} else {
|
|
115
122
|
if(this.baseUrl !== baseUrl) {
|
|
116
123
|
throw new Error('Base URL already set and different from one previously provided');
|
|
@@ -118,6 +125,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
128
|
+
// RPC method to set the sleep timeout
|
|
129
|
+
async setSleepAfter(sleepAfter: string | number): Promise<void> {
|
|
130
|
+
this.sleepAfter = sleepAfter;
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
// RPC method to set environment variables
|
|
122
134
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
123
135
|
// Update local state for new sessions
|
|
@@ -200,20 +212,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
200
212
|
/**
|
|
201
213
|
* Ensure default session exists - lazy initialization
|
|
202
214
|
* This is called automatically by all public methods that need a session
|
|
215
|
+
*
|
|
216
|
+
* The session is persisted to Durable Object storage to survive hot reloads
|
|
217
|
+
* during development. If a session already exists in the container after reload,
|
|
218
|
+
* we reuse it instead of trying to create a new one.
|
|
203
219
|
*/
|
|
204
220
|
private async ensureDefaultSession(): Promise<string> {
|
|
205
221
|
if (!this.defaultSession) {
|
|
206
222
|
const sessionId = `sandbox-${this.sandboxName || 'default'}`;
|
|
207
223
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
try {
|
|
225
|
+
// Try to create session in container
|
|
226
|
+
await this.client.utils.createSession({
|
|
227
|
+
id: sessionId,
|
|
228
|
+
env: this.envVars || {},
|
|
229
|
+
cwd: '/workspace',
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
this.defaultSession = sessionId;
|
|
233
|
+
// Persist to storage so it survives hot reloads
|
|
234
|
+
await this.ctx.storage.put('defaultSession', sessionId);
|
|
235
|
+
this.logger.debug('Default session initialized', { sessionId });
|
|
236
|
+
} catch (error: any) {
|
|
237
|
+
// If session already exists (e.g., after hot reload), reuse it
|
|
238
|
+
if (error?.message?.includes('already exists') || error?.message?.includes('Session')) {
|
|
239
|
+
this.logger.debug('Reusing existing session after reload', { sessionId });
|
|
240
|
+
this.defaultSession = sessionId;
|
|
241
|
+
// Persist to storage in case it wasn't saved before
|
|
242
|
+
await this.ctx.storage.put('defaultSession', sessionId);
|
|
243
|
+
} else {
|
|
244
|
+
// Re-throw other errors
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
217
248
|
}
|
|
218
249
|
return this.defaultSession;
|
|
219
250
|
}
|
|
@@ -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
|
+
});
|