@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.
@@ -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
@@ -1,4 +1,4 @@
1
1
  import '@repo/shared';
2
2
  import 'cloudflare:workers';
3
3
  import '@cloudflare/containers';
4
- export { b as Sandbox, g as getSandbox } from './sandbox-D9K2ypln.js';
4
+ export { b as Sandbox, g as getSandbox } from './sandbox-B1TT7PZP.js';
package/dist/sandbox.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Sandbox,
3
3
  getSandbox
4
- } from "./chunk-EXQOIRZI.js";
4
+ } from "./chunk-WK36EMRB.js";
5
5
  import "./chunk-JXZMAU2C.js";
6
6
  import "./chunk-Z532A7QC.js";
7
7
  import "./chunk-EKSWCBCA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/sandbox-sdk"
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(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
+ ) {
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 = "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
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 and port tokens from storage on initialization
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
- // Create session in container
208
- await this.client.utils.createSession({
209
- id: sessionId,
210
- env: this.envVars || {},
211
- cwd: '/workspace',
212
- });
213
-
214
- this.defaultSession = sessionId;
215
- this.logger.debug('Default session initialized', { sessionId });
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
+ });