@cloudflare/sandbox 0.4.3 → 0.4.5
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +12 -0
- package/README.md +0 -2
- package/dist/{chunk-EXQOIRZI.js → chunk-WK36EMRB.js} +32 -9
- 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 -15
- package/tests/get-sandbox.test.ts +110 -0
- package/dist/chunk-EXQOIRZI.js.map +0 -1
|
@@ -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
|
|
@@ -117,6 +125,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
// RPC method to set the sleep timeout
|
|
129
|
+
async setSleepAfter(sleepAfter: string | number): Promise<void> {
|
|
130
|
+
this.sleepAfter = sleepAfter;
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
// RPC method to set environment variables
|
|
121
134
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
122
135
|
// Update local state for new sessions
|
|
@@ -199,20 +212,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
199
212
|
/**
|
|
200
213
|
* Ensure default session exists - lazy initialization
|
|
201
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.
|
|
202
219
|
*/
|
|
203
220
|
private async ensureDefaultSession(): Promise<string> {
|
|
204
221
|
if (!this.defaultSession) {
|
|
205
222
|
const sessionId = `sandbox-${this.sandboxName || 'default'}`;
|
|
206
223
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
}
|
|
216
248
|
}
|
|
217
249
|
return this.defaultSession;
|
|
218
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
|
+
});
|