From 826d651ee27746ba7945db13eda1a3846139ac97 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 Dec 2025 13:44:04 +0000 Subject: [PATCH] Add documentation for extending Sandbox class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add new guide explaining how to extend Sandbox with custom methods - Update getSandbox() API reference to document generic type parameter - Include TypeScript examples showing custom Sandbox usage - Add best practices for custom methods This documents the fix from PR #264 which made getSandbox and SandboxEnv generic to support extending the Sandbox class without type errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/content/docs/sandbox/api/lifecycle.mdx | 40 ++- .../docs/sandbox/guides/extend-sandbox.mdx | 273 ++++++++++++++++++ 2 files changed, 309 insertions(+), 4 deletions(-) create mode 100644 src/content/docs/sandbox/guides/extend-sandbox.mdx diff --git a/src/content/docs/sandbox/api/lifecycle.mdx b/src/content/docs/sandbox/api/lifecycle.mdx index 7f5ba4701d249c..96a48c4fa84133 100644 --- a/src/content/docs/sandbox/api/lifecycle.mdx +++ b/src/content/docs/sandbox/api/lifecycle.mdx @@ -16,13 +16,16 @@ Create and manage sandbox containers. Get sandbox instances, configure options, Get or create a sandbox instance by ID. ```ts -const sandbox = getSandbox( - binding: DurableObjectNamespace, +function getSandbox( + binding: DurableObjectNamespace, sandboxId: string, options?: SandboxOptions -): Sandbox +): T ``` +**Type parameters**: +- `T` - The Sandbox class type (use when extending Sandbox with custom methods) + **Parameters**: - `binding` - The Durable Object namespace binding from your Worker environment - `sandboxId` - Unique identifier for this sandbox. The same ID always returns the same sandbox instance @@ -32,7 +35,7 @@ const sandbox = getSandbox( - `containerTimeouts` - Configure container startup timeouts - `normalizeId` - Lowercase sandbox IDs for preview URL compatibility (default: `false`) -**Returns**: `Sandbox` instance +**Returns**: Sandbox instance of type `T` :::note The container starts lazily on first operation. Calling `getSandbox()` returns immediately—the container only spins up when you execute a command, write a file, or perform other operations. See [Sandbox lifecycle](/sandbox/concepts/sandboxes/) for details. @@ -52,6 +55,35 @@ export default { ``` +**Using with custom Sandbox classes**: + +When extending the `Sandbox` class with custom methods, specify the type parameter to get proper TypeScript inference: + + +``` +import { getSandbox, Sandbox } from '@cloudflare/sandbox'; + +export class CustomSandbox extends Sandbox { + async customMethod(): Promise { + return 'custom result'; + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + // Specify the custom type to access custom methods + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + // TypeScript knows about customMethod() + const result = await sandbox.customMethod(); + return Response.json({ result }); + } +}; +``` + + +See [Extend Sandbox class](/sandbox/guides/extend-sandbox/) for a complete guide on creating custom Sandbox classes. + :::caution When using `keepAlive: true`, you **must** call `destroy()` when finished to prevent containers running indefinitely. ::: diff --git a/src/content/docs/sandbox/guides/extend-sandbox.mdx b/src/content/docs/sandbox/guides/extend-sandbox.mdx new file mode 100644 index 00000000000000..b4669498f29b5f --- /dev/null +++ b/src/content/docs/sandbox/guides/extend-sandbox.mdx @@ -0,0 +1,273 @@ +--- +title: Extend Sandbox class +pcx_content_type: how-to +sidebar: + order: 12 +--- + +import { TypeScriptExample, WranglerConfig } from "~/components"; + +Add custom methods to the Sandbox class to create reusable, domain-specific operations. + +## Overview + +You can extend the `Sandbox` class to add custom methods that encapsulate common operations for your application. This is useful for: + +- Creating domain-specific APIs (e.g., `runAnalysis()`, `deployApp()`) +- Encapsulating complex multi-step operations +- Adding custom validation or error handling +- Building reusable sandbox behaviors across your Worker + +## Create a custom Sandbox class + +Define a class that extends `Sandbox` and add your custom methods: + + +``` +import { Sandbox } from '@cloudflare/sandbox'; + +export class CustomSandbox extends Sandbox { + async runAnalysis(dataFile: string): Promise { + // Custom method that runs a multi-step analysis + await this.writeFile('/workspace/data.csv', dataFile); + await this.exec('pip install pandas matplotlib'); + + const result = await this.exec('python analyze.py'); + return result.stdout; + } + + async deployApp(code: string): Promise { + // Custom method that deploys an application + await this.writeFile('/workspace/app.js', code); + await this.exec('npm install'); + await this.startProcess('node app.js'); + } +} +``` + + +## Configure your Worker + +Update your Worker configuration to use the custom Sandbox class: + +### 1. Update wrangler configuration + + +```toml +name = "my-worker" +main = "src/index.ts" + +[[durable_objects.bindings]] +name = "Sandbox" +class_name = "CustomSandbox" +script_name = "my-worker" + +[observability] +enabled = true +``` + + +### 2. Export the custom class + +In your Worker entry point, export your custom Sandbox class: + + +``` +import { getSandbox } from '@cloudflare/sandbox'; +import { CustomSandbox } from './custom-sandbox'; + +// Export your custom Sandbox class as a Durable Object +export { CustomSandbox }; + +interface Env { + Sandbox: DurableObjectNamespace; +} + +export default { + async fetch(request: Request, env: Env): Promise { + // Use getSandbox with your custom type + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + // Call your custom methods + const result = await sandbox.runAnalysis('data content here'); + + return Response.json({ result }); + } +}; +``` + + +:::note +When using custom Sandbox classes, specify the type parameter in `getSandbox()` to get proper TypeScript inference for your custom methods. +::: + +## Type-safe environment interface + +Define a type-safe environment interface for your Worker: + + +``` +import { SandboxEnv } from '@cloudflare/sandbox'; +import { CustomSandbox } from './custom-sandbox'; + +// Extend SandboxEnv with your custom type +interface Env extends SandboxEnv { + // Add other bindings as needed + MY_BUCKET: R2Bucket; + MY_KV: KVNamespace; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + // TypeScript knows about your custom methods + await sandbox.runAnalysis('data'); + await sandbox.deployApp('code'); + + return new Response('OK'); + } +}; +``` + + +## Best practices + +### Keep methods focused + +Each custom method should have a single, clear purpose: + + +``` +export class CustomSandbox extends Sandbox { + // Good: Focused, reusable method + async installDependencies(packages: string[]): Promise { + await this.exec(`pip install ${packages.join(' ')}`); + } + + // Good: Domain-specific operation + async runTests(testFile: string): Promise { + const result = await this.exec(`python -m pytest ${testFile}`); + return result.exitCode === 0; + } +} +``` + + +### Handle errors appropriately + +Add proper error handling in your custom methods: + + +``` +export class CustomSandbox extends Sandbox { + async processData(input: string): Promise { + try { + await this.writeFile('/tmp/input.txt', input); + const result = await this.exec('python process.py /tmp/input.txt'); + + if (result.exitCode !== 0) { + throw new Error(`Processing failed: ${result.stderr}`); + } + + return result.stdout; + } catch (error) { + // Add context to errors + throw new Error(`Data processing failed: ${error.message}`); + } + } +} +``` + + +### Validate inputs + +Add validation to prevent common errors: + + +``` +export class CustomSandbox extends Sandbox { + async runScript(scriptPath: string): Promise { + // Validate input + if (!scriptPath.startsWith('/workspace/')) { + throw new Error('Script must be in /workspace directory'); + } + + // Check file exists before running + const files = await this.listFiles('/workspace'); + const fileName = scriptPath.split('/').pop(); + + if (!files.includes(fileName)) { + throw new Error(`Script not found: ${scriptPath}`); + } + + await this.exec(`python ${scriptPath}`); + } +} +``` + + +## Complete example + +A practical example of a custom Sandbox class for a code execution service: + + +``` +import { Sandbox } from '@cloudflare/sandbox'; + +export interface ExecutionResult { + success: boolean; + output: string; + executionTime: number; +} + +export class CodeExecutorSandbox extends Sandbox { + async executeCode( + language: 'python' | 'javascript', + code: string, + timeout: number = 30 + ): Promise { + const startTime = Date.now(); + + try { + // Write code to file + const fileName = language === 'python' ? 'script.py' : 'script.js'; + await this.writeFile(`/tmp/${fileName}`, code); + + // Execute based on language + const command = language === 'python' + ? `python /tmp/${fileName}` + : `node /tmp/${fileName}`; + + const result = await this.exec(command, { timeout }); + + return { + success: result.exitCode === 0, + output: result.exitCode === 0 ? result.stdout : result.stderr, + executionTime: Date.now() - startTime + }; + } catch (error) { + return { + success: false, + output: error.message, + executionTime: Date.now() - startTime + }; + } + } + + async installPackages(language: 'python' | 'javascript', packages: string[]): Promise { + const command = language === 'python' + ? `pip install ${packages.join(' ')}` + : `npm install ${packages.join(' ')}`; + + await this.exec(command); + } +} +``` + + +## Related resources + +- [Lifecycle API](/sandbox/api/lifecycle/) - Base Sandbox class methods +- [Sessions API](/sandbox/api/sessions/) - Manage execution contexts +- [Configuration reference](/sandbox/configuration/sandbox-options/) - Configure sandbox behavior