From 7a73adaea1d7c09c48a4a73f369bd630144fb03b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 20:07:35 +0000 Subject: [PATCH 1/5] Add file watching documentation - Add API reference for file watching with watch() method documentation - Add comprehensive how-to guide for file watching workflows including hot reloading, test automation, and file synchronization - Update API index to include file watching section This documentation covers the new file watching capabilities with inotify support added in cloudflare/sandbox-sdk PR #324. --- .../docs/sandbox/api/file-watching.mdx | 195 ++++++++ src/content/docs/sandbox/api/index.mdx | 8 + .../docs/sandbox/guides/watch-files.mdx | 425 ++++++++++++++++++ 3 files changed, 628 insertions(+) create mode 100644 src/content/docs/sandbox/api/file-watching.mdx create mode 100644 src/content/docs/sandbox/guides/watch-files.mdx diff --git a/src/content/docs/sandbox/api/file-watching.mdx b/src/content/docs/sandbox/api/file-watching.mdx new file mode 100644 index 00000000000000..9f8dc5583ce71b --- /dev/null +++ b/src/content/docs/sandbox/api/file-watching.mdx @@ -0,0 +1,195 @@ +--- +title: File Watching +pcx_content_type: concept +sidebar: + order: 4 +--- + +import { TypeScriptExample } from "~/components"; + +Watch filesystem changes in real-time using Linux's native inotify system. The file watcher monitors file and directory events (create, modify, delete, move) and streams them to your Worker. + +## Overview + +File watching provides real-time notifications for filesystem changes in the sandbox. This is useful for: + +- Hot reloading development servers +- Automated testing when files change +- Live synchronization of files +- Building file processing pipelines + +The watcher uses Linux's inotify system under the hood for efficient, kernel-level event notifications. + +## Methods + +### `watch()` + +Start watching a directory for filesystem changes. + +```ts +const watcher = await sandbox.watch(path: string, options?: WatchOptions): Promise +``` + +**Parameters**: +- `path` - Path to watch (absolute or relative to `/workspace`) +- `options` (optional): + - `recursive` - Watch subdirectories recursively (default: `true`) + - `include` - Glob patterns to include (e.g., `['*.ts', '*.js']`) + - `exclude` - Glob patterns to exclude (default: `['.git', 'node_modules', '.DS_Store']`) + - `signal` - AbortSignal to cancel the watch + - `onEvent` - Callback for file change events + - `onError` - Callback for watch errors + +**Returns**: `Promise` - Handle to control the watcher + + +``` +// Watch all changes in a directory +const watcher = await sandbox.watch('/workspace/src', { + onEvent: (event) => { + console.log(`${event.type}: ${event.path}`); + }, + onError: (error) => { + console.error('Watch error:', error); + } +}); + +// Watch only TypeScript files +const tsWatcher = await sandbox.watch('/workspace', { + include: ['*.ts', '*.tsx'], + onEvent: (event) => { + if (event.type === 'modify') { + console.log(`TypeScript file changed: ${event.path}`); + } + } +}); + +// Use AbortController for cancellation +const controller = new AbortController(); +const watcher = await sandbox.watch('/workspace', { + signal: controller.signal, + onEvent: (event) => console.log(event) +}); + +// Later: controller.abort(); +``` + + +:::note[Performance considerations] +Use `include` patterns to limit watches to relevant files, especially in large directories. The default `exclude` patterns already filter out common directories that generate many irrelevant events. +::: + +## Types + +### `WatchHandle` + +Handle returned by `watch()` to control the watcher. + +```ts +interface WatchHandle { + readonly id: string; // Unique watch identifier + readonly path: string; // Path being watched + stop(): Promise; // Stop watching and clean up +} +``` + + +``` +const watcher = await sandbox.watch('/workspace'); + +console.log(`Watch ID: ${watcher.id}`); +console.log(`Watching: ${watcher.path}`); + +// Stop when done +await watcher.stop(); +``` + + +### `WatchEvent` + +Event object passed to the `onEvent` callback. + +```ts +interface WatchEvent { + type: 'create' | 'modify' | 'delete' | 'rename'; + path: string; // Absolute path to changed file/directory + isDirectory: boolean; // Whether the path is a directory +} +``` + + +``` +const watcher = await sandbox.watch('/workspace', { + onEvent: (event) => { + switch (event.type) { + case 'create': + console.log(`Created: ${event.path}${event.isDirectory ? ' (directory)' : ''}`); + break; + case 'modify': + console.log(`Modified: ${event.path}`); + break; + case 'delete': + console.log(`Deleted: ${event.path}`); + break; + case 'rename': + console.log(`Renamed: ${event.path}`); + break; + } + } +}); +``` + + +### `WatchOptions` + +Configuration options for file watching. + +```ts +interface WatchOptions { + recursive?: boolean; // Watch subdirectories (default: true) + include?: string[]; // Glob patterns to include + exclude?: string[]; // Glob patterns to exclude + signal?: AbortSignal; // Cancellation signal + onEvent?: (event: WatchEvent) => void; // Event callback + onError?: (error: Error) => void; // Error callback +} +``` + +## Error Handling + +Watch errors are reported through the `onError` callback. Common error scenarios: + +- **Watch process died**: The underlying inotify process stopped unexpectedly +- **Path not found**: The watched path does not exist +- **Permission denied**: Insufficient permissions to watch the path +- **Resource limits**: Too many watches (Linux inotify limits) + + +``` +const watcher = await sandbox.watch('/workspace', { + onError: (error) => { + if (error.message.includes('No such file or directory')) { + console.error('Watched path was deleted'); + } else if (error.message.includes('permission denied')) { + console.error('Insufficient permissions'); + } else { + console.error('Watch error:', error); + } + + // Optionally restart the watch + restartWatch(); + } +}); +``` + + +## Limitations + +- **Linux only**: File watching uses inotify and only works on Linux containers +- **Resource limits**: Linux has default limits on the number of inotify watches (typically 8192) +- **Network filesystems**: Some network filesystems may not generate inotify events +- **High-frequency changes**: Very rapid file changes may be coalesced or missed + +:::note[Container lifecycle] +Watches are automatically stopped when the sandbox sleeps or is destroyed. You may need to recreate watches after the sandbox resumes. +::: \ No newline at end of file diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index 37c4472e359f17..d3aa1ee8616fd9 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -35,6 +35,14 @@ The Sandbox SDK provides a comprehensive API for executing code, managing files, Read, write, and manage files in the sandbox filesystem. Includes directory operations and file metadata. + + Monitor filesystem changes in real-time using Linux inotify. Get notified of file creation, modification, deletion, and moves. + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +const watcher = await sandbox.watch('/workspace/src', { + onEvent: (event) => { + console.log(`${event.type}: ${event.path}`); + + if (event.isDirectory) { + console.log('Directory changed'); + } else { + console.log('File changed'); + } + }, + onError: (error) => { + console.error('Watch failed:', error); + } +}); + +console.log(`Started watching: ${watcher.path}`); +``` + + +The watcher will immediately start monitoring the specified path and call your `onEvent` callback for each filesystem change. + +## Filter watched files + +Use include and exclude patterns to focus on relevant files: + + +``` +// Watch only JavaScript and TypeScript files +const jsWatcher = await sandbox.watch('/workspace', { + include: ['*.js', '*.ts', '*.jsx', '*.tsx'], + onEvent: (event) => { + console.log(`JS/TS file ${event.type}: ${event.path}`); + } +}); + +// Watch everything except logs and build artifacts +const filtered = await sandbox.watch('/workspace', { + exclude: ['*.log', 'dist/*', 'build/*', 'node_modules/*'], + onEvent: (event) => { + console.log(`Source change: ${event.path}`); + } +}); +``` + + +:::note[Pattern matching] +Glob patterns match against the filename only, not the full path. Use patterns like `*.ts` for TypeScript files or `test*` for files starting with "test". +::: + +## Hot reloading server + +Build a hot reloading development server that restarts when code changes: + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +async function setupHotReload(sandbox) { + let serverProcess = null; + + // Start the initial server + async function startServer() { + if (serverProcess) { + await serverProcess.kill(); + } + + console.log('Starting development server...'); + serverProcess = await sandbox.startProcess({ + command: 'npm run dev', + cwd: '/workspace' + }); + + console.log('Server started, waiting for changes...'); + } + + // Watch for source code changes + const watcher = await sandbox.watch('/workspace/src', { + include: ['*.js', '*.ts', '*.jsx', '*.tsx'], + onEvent: async (event) => { + if (event.type === 'modify' || event.type === 'create') { + console.log(`Code changed: ${event.path}`); + console.log('Restarting server...'); + await startServer(); + } + }, + onError: (error) => { + console.error('Watch error:', error); + } + }); + + // Start initial server + await startServer(); + + return { watcher, serverProcess }; +} + +// Usage +const sandbox = getSandbox(env.Sandbox, 'dev-sandbox'); +const { watcher } = await setupHotReload(sandbox); + +// Stop watching when done +// await watcher.stop(); +``` + + +## Test runner automation + +Automatically run tests when source files change: + + +``` +async function setupAutoTesting(sandbox) { + let isTestRunning = false; + + async function runTests() { + if (isTestRunning) { + console.log('Tests already running, skipping...'); + return; + } + + isTestRunning = true; + console.log('Running tests...'); + + try { + const result = await sandbox.exec('npm test', { + cwd: '/workspace' + }); + + if (result.exitCode === 0) { + console.log('✅ Tests passed'); + } else { + console.log('❌ Tests failed'); + console.log(result.stderr); + } + } catch (error) { + console.error('Test execution failed:', error); + } finally { + isTestRunning = false; + } + } + + const watcher = await sandbox.watch('/workspace', { + include: ['*.js', '*.ts', '*.test.js', '*.spec.ts'], + onEvent: async (event) => { + if (event.type === 'modify' || event.type === 'create') { + console.log(`File changed: ${event.path}`); + // Debounce rapid changes + setTimeout(runTests, 500); + } + } + }); + + return watcher; +} + +const sandbox = getSandbox(env.Sandbox, 'test-sandbox'); +await setupAutoTesting(sandbox); +``` + + +## File synchronization + +Sync files between local development and the sandbox: + + +``` +async function syncFiles(sandbox, localFiles) { + const watcher = await sandbox.watch('/workspace', { + onEvent: async (event) => { + // Sync sandbox changes back to local storage + if (event.type === 'modify' && !event.isDirectory) { + try { + const file = await sandbox.readFile(event.path); + await storeFile(event.path, file.content); + console.log(`Synced: ${event.path}`); + } catch (error) { + console.error(`Failed to sync ${event.path}:`, error); + } + } + } + }); + + // Upload local files to sandbox + for (const [path, content] of Object.entries(localFiles)) { + await sandbox.writeFile(`/workspace/${path}`, content); + } + + return watcher; +} + +// Mock storage functions +async function storeFile(path, content) { + // Store to your preferred backend (R2, KV, etc.) +} + +// Usage +const localFiles = { + 'app.js': 'console.log("Hello World");', + 'package.json': JSON.stringify({ name: 'my-app' }) +}; + +const sandbox = getSandbox(env.Sandbox, 'sync-sandbox'); +const watcher = await syncFiles(sandbox, localFiles); +``` + + +## Handle watch lifecycle + +Properly manage watch lifecycle with AbortController: + + +``` +async function managedWatch(sandbox, duration = 60000) { + const controller = new AbortController(); + + // Auto-cleanup after duration + const timeout = setTimeout(() => { + console.log('Auto-stopping watch...'); + controller.abort(); + }, duration); + + try { + const watcher = await sandbox.watch('/workspace', { + signal: controller.signal, + onEvent: (event) => { + console.log(`Change: ${event.path}`); + }, + onError: (error) => { + if (error.name === 'AbortError') { + console.log('Watch cancelled'); + } else { + console.error('Watch error:', error); + } + } + }); + + console.log('Watch started, will auto-stop in 60s'); + return watcher; + + } finally { + clearTimeout(timeout); + } +} + +// Usage +const sandbox = getSandbox(env.Sandbox, 'temp-sandbox'); +await managedWatch(sandbox, 30000); // 30 second watch +``` + + +## Performance optimization + +For large directories, optimize watch performance: + + +``` +// Efficient watching for large projects +const watcher = await sandbox.watch('/workspace', { + // Only watch source directories + recursive: true, + + // Include only relevant file types + include: [ + '*.js', '*.ts', '*.jsx', '*.tsx', // JavaScript/TypeScript + '*.json', '*.md', // Config and docs + '*.css', '*.scss' // Styles + ], + + // Exclude heavy directories (already in defaults) + exclude: [ + '.git', 'node_modules', '.DS_Store', + '*.log', 'dist', 'build', '.next', + 'coverage', '.nyc_output' + ], + + onEvent: (event) => { + // Process only meaningful events + if (event.type === 'modify' && !event.isDirectory) { + handleFileChange(event.path); + } + } +}); + +function handleFileChange(path) { + // Your change handling logic + console.log(`Processing: ${path}`); +} +``` + + +## Error recovery + +Handle watch failures gracefully: + + +``` +async function createResilientWatch(sandbox, path, options = {}) { + let attempts = 0; + const maxAttempts = 3; + + async function setupWatch() { + attempts++; + + try { + return await sandbox.watch(path, { + ...options, + onError: async (error) => { + console.error(`Watch error (attempt ${attempts}):`, error); + + if (attempts < maxAttempts) { + console.log('Attempting to restart watch...'); + setTimeout(setupWatch, 2000); // Wait 2s before retry + } else { + console.error('Max watch restart attempts reached'); + } + } + }); + } catch (error) { + if (attempts < maxAttempts) { + console.log(`Watch setup failed, retrying in 2s... (${attempts}/${maxAttempts})`); + setTimeout(setupWatch, 2000); + } else { + throw new Error(`Failed to setup watch after ${maxAttempts} attempts`); + } + } + } + + return setupWatch(); +} + +// Usage with retry logic +const sandbox = getSandbox(env.Sandbox, 'resilient-sandbox'); +const watcher = await createResilientWatch(sandbox, '/workspace', { + onEvent: (event) => console.log(event) +}); +``` + + +## Multiple watchers + +Coordinate multiple watches for different purposes: + + +``` +async function setupMultiWatch(sandbox) { + const watchers = []; + + // Watch source code for hot reload + const srcWatcher = await sandbox.watch('/workspace/src', { + include: ['*.js', '*.ts'], + onEvent: (event) => handleSourceChange(event) + }); + watchers.push(srcWatcher); + + // Watch config files for restart + const configWatcher = await sandbox.watch('/workspace', { + include: ['package.json', '*.config.js', '.env*'], + onEvent: (event) => handleConfigChange(event) + }); + watchers.push(configWatcher); + + // Watch tests for auto-run + const testWatcher = await sandbox.watch('/workspace/tests', { + include: ['*.test.js', '*.spec.ts'], + onEvent: (event) => handleTestChange(event) + }); + watchers.push(testWatcher); + + // Cleanup function + const stopAll = async () => { + await Promise.all(watchers.map(w => w.stop())); + console.log('All watchers stopped'); + }; + + return { watchers, stopAll }; +} + +function handleSourceChange(event) { + console.log(`Source changed: ${event.path}`); + // Hot reload logic +} + +function handleConfigChange(event) { + console.log(`Config changed: ${event.path}`); + // Restart server logic +} + +function handleTestChange(event) { + console.log(`Test changed: ${event.path}`); + // Run specific tests +} + +// Usage +const sandbox = getSandbox(env.Sandbox, 'multi-watch'); +const { stopAll } = await setupMultiWatch(sandbox); + +// Later: await stopAll(); +``` + + +:::note[Resource management] +Each watch consumes system resources. Stop watches when they are no longer needed using `await watcher.stop()` or by aborting with `AbortController`. +::: \ No newline at end of file From f1a20898d98b55d79a13fef4f5b4f6ef2958e141 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 20:25:21 +0000 Subject: [PATCH 2/5] docs(sandbox): add file watching API documentation Adds comprehensive documentation for the new file watching feature: - Documents watch() method with inotify-based filesystem monitoring - Covers event types (create, modify, delete, rename) - Explains WatchHandle interface and lifecycle management - Provides filtering examples with include/exclude patterns - Includes real-world use cases (hot reload, test runners, config monitoring) - Documents performance considerations and error handling - Updates API index page to include watching documentation Syncs with feat: add file watching capabilities with inotify support (sandbox-sdk PR #324) --- src/content/docs/sandbox/api/index.mdx | 8 + src/content/docs/sandbox/api/watching.mdx | 295 ++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 src/content/docs/sandbox/api/watching.mdx diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index d3aa1ee8616fd9..57a117d362e19d 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -75,4 +75,12 @@ The Sandbox SDK provides a comprehensive API for executing code, managing files, Create isolated execution contexts within a sandbox. Each session maintains its own shell state, environment variables, and working directory. + + Monitor filesystem changes in real-time using inotify. Watch for file creation, modification, deletion, and move operations with event-based notifications. + + diff --git a/src/content/docs/sandbox/api/watching.mdx b/src/content/docs/sandbox/api/watching.mdx new file mode 100644 index 00000000000000..98451749b9ac1c --- /dev/null +++ b/src/content/docs/sandbox/api/watching.mdx @@ -0,0 +1,295 @@ +--- +title: File Watching +pcx_content_type: concept +sidebar: + order: 8 +--- + +import { TypeScriptExample } from "~/components"; + +Monitor filesystem changes in real-time using Linux's native inotify system. Watch for file creation, modification, deletion, and move operations with efficient event-based notifications. + +## Methods + +### `watch()` + +Start watching a directory for filesystem changes. Returns a handle that emits events for file operations. + +```ts +const handle = await sandbox.watch(path: string, options?: WatchOptions): Promise +``` + +**Parameters**: +- `path` - Directory path to watch (absolute or relative to `/workspace`) +- `options` (optional): + - `recursive` - Watch subdirectories recursively (default: `true`) + - `include` - Array of glob patterns to include (e.g., `["*.js", "*.ts"]`) + - `exclude` - Array of patterns to exclude (default: `[".git", "node_modules", ".DS_Store"]`) + - `onEvent` - Callback function for file events + - `onError` - Callback function for watch errors + - `signal` - AbortSignal to stop watching + +**Returns**: `Promise` with `stop()` method and properties + + +```typescript +// Watch all changes in src directory +const watcher = await sandbox.watch('/workspace/src', { + recursive: true, + onEvent: (event) => { + console.log(`${event.type}: ${event.path}`); + // Outputs: "create: /workspace/src/new-file.js" + }, + onError: (error) => console.error('Watch error:', error) +}); + +// With file filtering +const tsWatcher = await sandbox.watch('/workspace', { + include: ['*.ts', '*.tsx'], + exclude: ['node_modules', '.git', 'dist'], + onEvent: (event) => { + if (event.type === 'modify') { + console.log(`TypeScript file changed: ${event.path}`); + } + } +}); + +// Stop watching when done +await watcher.stop(); +``` + + +### Using AbortController + +Control the watch lifecycle with AbortController for automatic cleanup. + + +```typescript +const controller = new AbortController(); + +const watcher = await sandbox.watch('/workspace/src', { + signal: controller.signal, + onEvent: (event) => console.log(event), + onError: (error) => console.error(error) +}); + +// Stop watching after 30 seconds +setTimeout(() => controller.abort(), 30000); + +// Or stop manually +await watcher.stop(); +``` + + +## Event Types + +Watch events contain information about filesystem operations: + +```ts +interface WatchEvent { + type: 'create' | 'modify' | 'delete' | 'rename'; + path: string; // Full path of the affected file + isDirectory: boolean; // true if the path is a directory + timestamp: string; // ISO timestamp when event occurred +} +``` + +**Event Types**: +- `create` - New file or directory created +- `modify` - File content or metadata changed +- `delete` - File or directory deleted +- `rename` - File or directory moved/renamed + + +```typescript +await sandbox.watch('/workspace', { + onEvent: (event) => { + switch (event.type) { + case 'create': + if (event.isDirectory) { + console.log(`New directory: ${event.path}`); + } else { + console.log(`New file: ${event.path}`); + } + break; + case 'modify': + console.log(`File changed: ${event.path} at ${event.timestamp}`); + break; + case 'delete': + console.log(`Deleted: ${event.path}`); + break; + case 'rename': + console.log(`Moved: ${event.path}`); + break; + } + } +}); +``` + + +## WatchHandle + +The returned handle provides control over the watch operation: + +```ts +interface WatchHandle { + stop(): Promise; // Stop watching and clean up + id: string; // Unique watch identifier + path: string; // Path being watched +} +``` + + +```typescript +const watcher = await sandbox.watch('/workspace/src'); + +console.log(`Watch ID: ${watcher.id}`); +console.log(`Watching: ${watcher.path}`); + +// Stop when done +await watcher.stop(); +``` + + +## Patterns and Filtering + +Use glob patterns to control which files trigger events: + + +```typescript +// Watch only JavaScript and TypeScript files +const jsWatcher = await sandbox.watch('/workspace', { + include: ['*.js', '*.ts', '*.jsx', '*.tsx'], + onEvent: (event) => console.log(`JS/TS change: ${event.path}`) +}); + +// Watch configuration files +const configWatcher = await sandbox.watch('/workspace', { + include: ['*.json', '*.yaml', '*.toml', '.env*'], + onEvent: (event) => { + if (event.type === 'modify') { + console.log(`Config updated: ${event.path}`); + } + } +}); + +// Exclude build artifacts and dependencies +const sourceWatcher = await sandbox.watch('/workspace', { + exclude: [ + 'node_modules', '.git', 'dist', 'build', + '*.log', '.DS_Store', 'Thumbs.db' + ], + onEvent: (event) => console.log(`Source change: ${event.path}`) +}); +``` + + +## Use Cases + +File watching enables real-time development workflows: + +### Hot Reload Development Server + + +```typescript +// Monitor source files for automatic rebuild +const devWatcher = await sandbox.watch('/workspace/src', { + include: ['*.js', '*.jsx', '*.ts', '*.tsx', '*.css'], + onEvent: async (event) => { + if (event.type === 'modify' || event.type === 'create') { + console.log(`Rebuilding due to ${event.type}: ${event.path}`); + await sandbox.exec('npm run build'); + console.log('Rebuild complete'); + } + } +}); +``` + + +### Test Runner Integration + + +```typescript +// Run tests when source or test files change +const testWatcher = await sandbox.watch('/workspace', { + include: ['**/*.test.js', 'src/**/*.js'], + onEvent: async (event) => { + if (event.type !== 'delete') { + console.log(`Running tests due to change: ${event.path}`); + await sandbox.exec('npm test'); + } + } +}); +``` + + +### Configuration Monitoring + + +```typescript +// React to configuration changes +const configWatcher = await sandbox.watch('/workspace', { + include: ['package.json', '.env*', 'config/*'], + onEvent: async (event) => { + if (event.path.includes('package.json')) { + console.log('Dependencies changed, reinstalling...'); + await sandbox.exec('npm install'); + } else if (event.path.includes('.env')) { + console.log('Environment variables updated'); + } + } +}); +``` + + +## Performance Considerations + +- **inotify limits**: Linux systems have limits on the number of watches (typically 8192). Use specific `include` patterns rather than watching entire filesystems. +- **Recursive watching**: Large directory trees can consume many watch descriptors. Consider watching specific subdirectories instead. +- **Event frequency**: High-frequency file operations (like log files) can generate many events. Use debouncing in your event handlers when needed. +- **Cleanup**: Always call `stop()` on watch handles to free system resources. + + +```typescript +// Debounced event handler for high-frequency changes +let debounceTimer: NodeJS.Timeout | null = null; + +const watcher = await sandbox.watch('/workspace/logs', { + onEvent: (event) => { + // Clear previous timer + if (debounceTimer) clearTimeout(debounceTimer); + + // Set new timer + debounceTimer = setTimeout(() => { + console.log(`Log file activity: ${event.path}`); + debounceTimer = null; + }, 1000); // 1 second debounce + } +}); +``` + + +## Error Handling + +Watch operations can fail due to filesystem issues, permission problems, or system limits: + + +```typescript +try { + const watcher = await sandbox.watch('/workspace/src', { + onError: (error) => { + console.error('Watch error:', error.message); + // Handle errors like process death, permission issues + } + }); +} catch (error) { + if (error.message.includes('does not exist')) { + console.error('Directory not found:', '/workspace/src'); + } else if (error.message.includes('permission')) { + console.error('Permission denied accessing:', '/workspace/src'); + } else { + console.error('Failed to start watching:', error.message); + } +} +``` + \ No newline at end of file From e5a777b8a2b6d6540238e0c8ff6a1f161a566383 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 20:52:21 +0000 Subject: [PATCH 3/5] docs: add file watching capabilities documentation - Add comprehensive API reference for file watching methods (watch, watchStream, stopWatch, listWatches) - Add detailed how-to guide covering basic usage, hot reloading, build integration, config monitoring - Include TypeScript examples, performance considerations, and best practices - Update main overview page with file watching feature highlight and example tab - Add file watching to API index navigation Relates to cloudflare/sandbox-sdk PR #324 --- .../docs/sandbox/api/file-watching.mdx | 269 +++++++---- src/content/docs/sandbox/api/index.mdx | 6 +- .../docs/sandbox/guides/file-watching.mdx | 447 ++++++++++++++++++ src/content/docs/sandbox/index.mdx | 40 ++ 4 files changed, 654 insertions(+), 108 deletions(-) create mode 100644 src/content/docs/sandbox/guides/file-watching.mdx diff --git a/src/content/docs/sandbox/api/file-watching.mdx b/src/content/docs/sandbox/api/file-watching.mdx index 9f8dc5583ce71b..7b054604eb1139 100644 --- a/src/content/docs/sandbox/api/file-watching.mdx +++ b/src/content/docs/sandbox/api/file-watching.mdx @@ -7,189 +7,248 @@ sidebar: import { TypeScriptExample } from "~/components"; -Watch filesystem changes in real-time using Linux's native inotify system. The file watcher monitors file and directory events (create, modify, delete, move) and streams them to your Worker. - -## Overview - -File watching provides real-time notifications for filesystem changes in the sandbox. This is useful for: - -- Hot reloading development servers -- Automated testing when files change -- Live synchronization of files -- Building file processing pipelines - -The watcher uses Linux's inotify system under the hood for efficient, kernel-level event notifications. +Watch for real-time file system changes using Linux's native inotify system. Get notified instantly when files are created, modified, deleted, or renamed. ## Methods ### `watch()` -Start watching a directory for filesystem changes. +Start watching a directory for file changes with callback handlers. ```ts const watcher = await sandbox.watch(path: string, options?: WatchOptions): Promise ``` **Parameters**: -- `path` - Path to watch (absolute or relative to `/workspace`) +- `path` - Absolute path to watch (e.g., `/workspace/src`) - `options` (optional): - - `recursive` - Watch subdirectories recursively (default: `true`) - - `include` - Glob patterns to include (e.g., `['*.ts', '*.js']`) - - `exclude` - Glob patterns to exclude (default: `['.git', 'node_modules', '.DS_Store']`) - - `signal` - AbortSignal to cancel the watch - - `onEvent` - Callback for file change events - - `onError` - Callback for watch errors + - `recursive` - Watch subdirectories (default: `true`) + - `include` - Array of glob patterns to include (e.g., `['*.ts', '*.js']`) + - `exclude` - Array of glob patterns to exclude (default: `['.git', 'node_modules', '.DS_Store']`) + - `onEvent` - Event callback function + - `onError` - Error callback function + - `signal` - AbortSignal for cancellation -**Returns**: `Promise` - Handle to control the watcher +**Returns**: `Promise` with `stop()` method ``` -// Watch all changes in a directory +// Basic file watching const watcher = await sandbox.watch('/workspace/src', { onEvent: (event) => { - console.log(`${event.type}: ${event.path}`); + console.log(`File ${event.type}: ${event.path}`); + console.log(`Is directory: ${event.isDirectory}`); }, onError: (error) => { console.error('Watch error:', error); } }); -// Watch only TypeScript files -const tsWatcher = await sandbox.watch('/workspace', { - include: ['*.ts', '*.tsx'], +// Stop watching +await watcher.stop(); + +// Watch specific file types +const jsWatcher = await sandbox.watch('/workspace', { + recursive: true, + include: ['*.js', '*.ts', '*.jsx', '*.tsx'], + exclude: ['node_modules', 'dist', '.git'], onEvent: (event) => { if (event.type === 'modify') { - console.log(`TypeScript file changed: ${event.path}`); + console.log(`Code file changed: ${event.path}`); } } }); -// Use AbortController for cancellation +// Use AbortSignal for cancellation const controller = new AbortController(); -const watcher = await sandbox.watch('/workspace', { +const watcher = await sandbox.watch('/workspace/config', { signal: controller.signal, - onEvent: (event) => console.log(event) + onEvent: (event) => { + console.log(`Config change: ${event.path}`); + } }); -// Later: controller.abort(); +// Cancel from elsewhere +controller.abort(); ``` -:::note[Performance considerations] -Use `include` patterns to limit watches to relevant files, especially in large directories. The default `exclude` patterns already filter out common directories that generate many irrelevant events. +:::note[Event types] +Watch events include: +- `create` - New file or directory created +- `modify` - File content changed +- `delete` - File or directory deleted +- `rename` - File or directory moved/renamed ::: -## Types +### `watchStream()` -### `WatchHandle` - -Handle returned by `watch()` to control the watcher. +Start watching and return a raw stream of Server-Sent Events for advanced use cases. ```ts -interface WatchHandle { - readonly id: string; // Unique watch identifier - readonly path: string; // Path being watched - stop(): Promise; // Stop watching and clean up +const stream = await sandbox.watchStream(path: string, options?: WatchStreamOptions): Promise> +``` + +**Parameters**: +- `path` - Absolute path to watch +- `options` (optional): + - `recursive` - Watch subdirectories (default: `true`) + - `include` - Array of glob patterns to include + - `exclude` - Array of glob patterns to exclude + +**Returns**: `Promise>` - Stream of SSE data + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Get raw stream for custom processing +const stream = await sandbox.watchStream('/workspace/src', { + include: ['*.ts', '*.js'] +}); + +// Parse SSE events manually +const reader = stream.getReader(); +const decoder = new TextDecoder(); + +try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + // Parse SSE format: "data: {...}\n\n" + const lines = chunk.split('\n'); + for (const line of lines) { + if (line.startsWith('data: ')) { + const eventData = JSON.parse(line.slice(6)); + console.log('Raw event:', eventData); + } + } + } +} finally { + reader.releaseLock(); } ``` + + +### `stopWatch()` + +Stop a specific watch by its ID. + +```ts +const result = await sandbox.stopWatch(watchId: string): Promise<{ success: boolean }> +``` + +**Parameters**: +- `watchId` - Watch ID from the 'watching' event + +**Returns**: `Promise<{ success: boolean }>` ``` +// Usually you use the WatchHandle from watch() const watcher = await sandbox.watch('/workspace'); +await watcher.stop(); // Preferred method -console.log(`Watch ID: ${watcher.id}`); -console.log(`Watching: ${watcher.path}`); - -// Stop when done -await watcher.stop(); +// For advanced cases with stream API +const stream = await sandbox.watchStream('/workspace'); +// ... get watchId from 'watching' event in stream +await sandbox.stopWatch(watchId); ``` -### `WatchEvent` +### `listWatches()` -Event object passed to the `onEvent` callback. +List all active file watches. ```ts -interface WatchEvent { - type: 'create' | 'modify' | 'delete' | 'rename'; - path: string; // Absolute path to changed file/directory - isDirectory: boolean; // Whether the path is a directory -} +const result = await sandbox.listWatches(): Promise ``` +**Returns**: `Promise` with active watch information + ``` -const watcher = await sandbox.watch('/workspace', { - onEvent: (event) => { - switch (event.type) { - case 'create': - console.log(`Created: ${event.path}${event.isDirectory ? ' (directory)' : ''}`); - break; - case 'modify': - console.log(`Modified: ${event.path}`); - break; - case 'delete': - console.log(`Deleted: ${event.path}`); - break; - case 'rename': - console.log(`Renamed: ${event.path}`); - break; - } - } -}); +const result = await sandbox.listWatches(); +console.log(`Active watches: ${result.count}`); + +for (const watch of result.watches) { + console.log(`- ${watch.id}: watching ${watch.path} since ${watch.startedAt}`); +} + +// Example output: +// Active watches: 2 +// - watch-1-1234567890: watching /workspace/src since 2024-01-15T10:30:00Z +// - watch-2-1234567891: watching /workspace/config since 2024-01-15T10:35:00Z ``` +## Types + ### `WatchOptions` Configuration options for file watching. ```ts interface WatchOptions { - recursive?: boolean; // Watch subdirectories (default: true) + recursive?: boolean; // Default: true include?: string[]; // Glob patterns to include - exclude?: string[]; // Glob patterns to exclude + exclude?: string[]; // Default: ['.git', 'node_modules', '.DS_Store'] + onEvent?: (event: WatchEvent) => void; // Event handler + onError?: (error: Error) => void; // Error handler signal?: AbortSignal; // Cancellation signal - onEvent?: (event: WatchEvent) => void; // Event callback - onError?: (error: Error) => void; // Error callback } ``` -## Error Handling - -Watch errors are reported through the `onError` callback. Common error scenarios: +### `WatchEvent` -- **Watch process died**: The underlying inotify process stopped unexpectedly -- **Path not found**: The watched path does not exist -- **Permission denied**: Insufficient permissions to watch the path -- **Resource limits**: Too many watches (Linux inotify limits) +File system event notification. - +```ts +interface WatchEvent { + type: 'create' | 'modify' | 'delete' | 'rename'; // Event type + path: string; // Absolute path to file/directory + isDirectory: boolean; // True if path is a directory +} ``` -const watcher = await sandbox.watch('/workspace', { - onError: (error) => { - if (error.message.includes('No such file or directory')) { - console.error('Watched path was deleted'); - } else if (error.message.includes('permission denied')) { - console.error('Insufficient permissions'); - } else { - console.error('Watch error:', error); - } - - // Optionally restart the watch - restartWatch(); - } -}); + +### `WatchHandle` + +Handle for controlling an active watch. + +```ts +interface WatchHandle { + stop(): Promise; // Stop the watch +} ``` - -## Limitations +## Performance considerations -- **Linux only**: File watching uses inotify and only works on Linux containers -- **Resource limits**: Linux has default limits on the number of inotify watches (typically 8192) -- **Network filesystems**: Some network filesystems may not generate inotify events -- **High-frequency changes**: Very rapid file changes may be coalesced or missed +- **Native efficiency**: Uses Linux inotify for optimal performance +- **Pattern filtering**: Use `include`/`exclude` patterns to reduce event volume +- **Resource cleanup**: Always call `stop()` or use AbortSignal to prevent memory leaks +- **Large directories**: Consider non-recursive watching for directories with many files -:::note[Container lifecycle] -Watches are automatically stopped when the sandbox sleeps or is destroyed. You may need to recreate watches after the sandbox resumes. +:::caution[Resource management] +File watchers consume system resources. Always stop watches when done: + +```ts +const watcher = await sandbox.watch('/workspace'); + +try { + // Your code +} finally { + await watcher.stop(); // Always cleanup +} + +// Or use AbortSignal for automatic cleanup +const controller = new AbortController(); +const watcher = await sandbox.watch('/workspace', { + signal: controller.signal +}); +// Automatically stops when signal is aborted +``` ::: \ No newline at end of file diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index 57a117d362e19d..8991d06871859a 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -77,10 +77,10 @@ The Sandbox SDK provides a comprehensive API for executing code, managing files, - Monitor filesystem changes in real-time using inotify. Watch for file creation, modification, deletion, and move operations with event-based notifications. + Watch for real-time file system changes using native inotify. Monitor files and directories for creates, modifications, deletes, and renames. diff --git a/src/content/docs/sandbox/guides/file-watching.mdx b/src/content/docs/sandbox/guides/file-watching.mdx new file mode 100644 index 00000000000000..48da638b2a573e --- /dev/null +++ b/src/content/docs/sandbox/guides/file-watching.mdx @@ -0,0 +1,447 @@ +--- +title: Watch files for changes +pcx_content_type: how-to +sidebar: + order: 11 +description: Monitor files and directories for real-time changes using native filesystem events. +--- + +import { TypeScriptExample } from "~/components"; + +File watching enables real-time monitoring of filesystem changes in your sandbox. This guide shows you how to set up file watchers for common development workflows like hot reloading, configuration monitoring, and build automation. + +## Basic file watching + +Start with a simple watcher that monitors a directory for any changes: + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + +// Watch for any changes in the src directory +const watcher = await sandbox.watch('/workspace/src', { + onEvent: (event) => { + console.log(`${event.type.toUpperCase()}: ${event.path}`); + if (event.isDirectory) { + console.log(' (directory)'); + } + }, + onError: (error) => { + console.error('Watch error:', error); + } +}); + +// Stop watching when done +await watcher.stop(); +``` + + +This will detect all file operations including creates, modifications, deletions, and renames. + +## Filter by file type + +Use glob patterns to watch only specific file types: + + +``` +// Watch only JavaScript and TypeScript files +const codeWatcher = await sandbox.watch('/workspace', { + recursive: true, + include: ['*.js', '*.ts', '*.jsx', '*.tsx'], + exclude: ['node_modules', 'dist', '.git', '*.test.js'], + onEvent: (event) => { + console.log(`Code file ${event.type}: ${event.path}`); + + if (event.type === 'modify') { + // Trigger rebuild or hot reload + console.log('Code changed, recompiling...'); + } + } +}); + +// Watch configuration files +const configWatcher = await sandbox.watch('/workspace', { + include: ['*.json', '*.yaml', '*.yml', '*.toml'], + onEvent: (event) => { + if (event.path.includes('package.json')) { + console.log('Dependencies may have changed'); + } else if (event.path.includes('tsconfig.json')) { + console.log('TypeScript config changed'); + } + } +}); +``` + + +## Development server hot reloading + +Implement hot reloading for a development server: + + +``` +import { getSandbox } from '@cloudflare/sandbox'; + +const sandbox = getSandbox(env.Sandbox, 'dev-sandbox'); + +// Start development server +await sandbox.writeFile('/workspace/server.js', ` +const http = require('http'); +const fs = require('fs'); + +const server = http.createServer((req, res) => { + if (req.url === '/') { + const html = fs.readFileSync('./index.html', 'utf8'); + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(html); + } else if (req.url === '/app.js') { + const js = fs.readFileSync('./app.js', 'utf8'); + res.writeHead(200, { 'Content-Type': 'text/javascript' }); + res.end(js); + } +}); + +server.listen(3000, () => console.log('Server running on port 3000')); +`); + +// Start the server process +const serverProcess = await sandbox.startProcess('node server.js', { + cwd: '/workspace' +}); + +// Watch for changes and restart server +let restartTimeout: NodeJS.Timeout | null = null; + +const watcher = await sandbox.watch('/workspace', { + include: ['*.html', '*.js', '*.css'], + exclude: ['node_modules'], + onEvent: (event) => { + if (event.type === 'modify') { + console.log(`File changed: ${event.path}`); + + // Debounce restarts + if (restartTimeout) { + clearTimeout(restartTimeout); + } + + restartTimeout = setTimeout(async () => { + console.log('Restarting development server...'); + await serverProcess.kill(); + + // Start new server process + const newProcess = await sandbox.startProcess('node server.js', { + cwd: '/workspace' + }); + + console.log('Server restarted'); + }, 500); + } + } +}); + +// Cleanup on shutdown +process.on('SIGINT', async () => { + await watcher.stop(); + await serverProcess.kill(); + process.exit(); +}); +``` + + +## Build system integration + +Integrate file watching with build processes: + + +``` +// Watch source files and trigger builds +const buildWatcher = await sandbox.watch('/workspace/src', { + include: ['*.ts', '*.tsx'], + onEvent: async (event) => { + if (event.type === 'modify' || event.type === 'create') { + console.log(`Source file ${event.type}: ${event.path}`); + + try { + // Run TypeScript compiler + const result = await sandbox.exec('npx tsc --noEmit', { + cwd: '/workspace' + }); + + if (result.success) { + console.log('✅ Type check passed'); + + // Run build + const buildResult = await sandbox.exec('npm run build', { + cwd: '/workspace' + }); + + if (buildResult.success) { + console.log('✅ Build completed'); + } else { + console.error('❌ Build failed:', buildResult.stderr); + } + } else { + console.error('❌ Type errors:', result.stderr); + } + } catch (error) { + console.error('Build error:', error); + } + } + } +}); + +// Also watch package.json for dependency changes +const depWatcher = await sandbox.watch('/workspace/package.json', { + recursive: false, + onEvent: async (event) => { + if (event.type === 'modify') { + console.log('Dependencies changed, reinstalling...'); + const result = await sandbox.exec('npm install', { + cwd: '/workspace' + }); + + if (result.success) { + console.log('✅ Dependencies updated'); + } else { + console.error('❌ npm install failed:', result.stderr); + } + } + } +}); +``` + + +## Configuration monitoring + +Monitor configuration files and reload application settings: + + +``` +interface AppConfig { + port: number; + debug: boolean; + apiUrl: string; +} + +let currentConfig: AppConfig = { + port: 3000, + debug: false, + apiUrl: 'https://api.example.com' +}; + +// Load initial config +const loadConfig = async () => { + try { + const configFile = await sandbox.readFile('/workspace/config.json'); + currentConfig = JSON.parse(configFile.content); + console.log('Config loaded:', currentConfig); + } catch (error) { + console.log('Using default config'); + } +}; + +await loadConfig(); + +// Watch for config changes +const configWatcher = await sandbox.watch('/workspace/config.json', { + recursive: false, + onEvent: async (event) => { + if (event.type === 'modify') { + console.log('Configuration file changed, reloading...'); + + try { + const configFile = await sandbox.readFile('/workspace/config.json'); + const newConfig = JSON.parse(configFile.content); + + // Validate config structure + if (typeof newConfig.port === 'number' && + typeof newConfig.debug === 'boolean' && + typeof newConfig.apiUrl === 'string') { + + currentConfig = newConfig; + console.log('✅ Config reloaded:', currentConfig); + + // Notify application of config change + // (restart server, update API client, etc.) + + } else { + console.error('❌ Invalid config format'); + } + } catch (error) { + console.error('❌ Config parsing error:', error); + } + } + } +}); +``` + + +## Advanced patterns + +### Multiple watchers with coordination + + +``` +// Coordinate multiple watchers for a complex workflow +class DevEnvironment { + private watchers: Map = new Map(); + + async start() { + // Watch source files + const sourceWatcher = await sandbox.watch('/workspace/src', { + include: ['*.ts', '*.tsx'], + onEvent: (event) => this.handleSourceChange(event) + }); + this.watchers.set('source', sourceWatcher); + + // Watch tests + const testWatcher = await sandbox.watch('/workspace/tests', { + include: ['*.test.ts', '*.spec.ts'], + onEvent: (event) => this.handleTestChange(event) + }); + this.watchers.set('test', testWatcher); + + // Watch config + const configWatcher = await sandbox.watch('/workspace', { + include: ['*.json', '*.yaml'], + onEvent: (event) => this.handleConfigChange(event) + }); + this.watchers.set('config', configWatcher); + } + + async stop() { + for (const [name, watcher] of this.watchers) { + console.log(`Stopping ${name} watcher`); + await watcher.stop(); + } + this.watchers.clear(); + } + + private async handleSourceChange(event: WatchEvent) { + console.log(`Source ${event.type}: ${event.path}`); + // Run linter, type check, and tests + await this.runChecks(); + } + + private async handleTestChange(event: WatchEvent) { + console.log(`Test ${event.type}: ${event.path}`); + // Run specific test file + await this.runTests(event.path); + } + + private async handleConfigChange(event: WatchEvent) { + console.log(`Config ${event.type}: ${event.path}`); + // Reload configuration + await this.reloadConfig(); + } + + private async runChecks() { + // Implementation details... + } + + private async runTests(testFile: string) { + // Implementation details... + } + + private async reloadConfig() { + // Implementation details... + } +} + +const devEnv = new DevEnvironment(); +await devEnv.start(); + +// Cleanup on process exit +process.on('SIGINT', async () => { + await devEnv.stop(); + process.exit(); +}); +``` + + +### Using AbortSignal for cancellation + + +``` +// Use AbortSignal for clean cancellation +const controller = new AbortController(); + +// Start multiple watchers with the same signal +const watchers = await Promise.all([ + sandbox.watch('/workspace/src', { + signal: controller.signal, + onEvent: (event) => console.log('Source:', event.path) + }), + sandbox.watch('/workspace/public', { + signal: controller.signal, + onEvent: (event) => console.log('Static:', event.path) + }), + sandbox.watch('/workspace/config', { + signal: controller.signal, + onEvent: (event) => console.log('Config:', event.path) + }) +]); + +// Stop all watchers at once +controller.abort(); + +// Or use timeout-based cancellation +const timeoutController = new AbortController(); +setTimeout(() => { + console.log('Stopping watchers after timeout'); + timeoutController.abort(); +}, 60000); // Stop after 1 minute + +const watcher = await sandbox.watch('/workspace', { + signal: timeoutController.signal, + onEvent: (event) => console.log('Event:', event.path) +}); +``` + + +## Best practices + +### Performance optimization + +- **Use specific patterns**: Filter with `include`/`exclude` to reduce event volume +- **Debounce rapid changes**: Batch multiple events to avoid excessive processing +- **Non-recursive for large directories**: Disable recursion for directories with many files + +### Resource management + +- **Always cleanup**: Use `watcher.stop()` or AbortSignal to prevent memory leaks +- **Limit concurrent watchers**: Too many watchers can impact performance +- **Handle errors gracefully**: Use `onError` callback to handle watch failures + +### Error handling + + +``` +const watcher = await sandbox.watch('/workspace/src', { + onEvent: (event) => { + try { + // Process event + handleFileChange(event); + } catch (error) { + console.error('Event processing error:', error); + } + }, + onError: (error) => { + console.error('Watch error:', error); + + // Attempt to restart watcher + setTimeout(async () => { + try { + await watcher.stop(); + // Create new watcher... + } catch (restartError) { + console.error('Failed to restart watcher:', restartError); + } + }, 1000); + } +}); +``` + + +File watching provides powerful capabilities for building responsive development tools and automated workflows. Use these patterns to create efficient, robust file monitoring systems in your sandbox applications. \ No newline at end of file diff --git a/src/content/docs/sandbox/index.mdx b/src/content/docs/sandbox/index.mdx index 434fa364158648..5e97e6b004c685 100644 --- a/src/content/docs/sandbox/index.mdx +++ b/src/content/docs/sandbox/index.mdx @@ -120,6 +120,40 @@ With Sandbox, you can execute Python scripts, run Node.js applications, analyze ``` + + ```typescript + import { getSandbox } from '@cloudflare/sandbox'; + + export { Sandbox } from '@cloudflare/sandbox'; + + export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'user-123'); + + // Watch for file changes in real-time + const watcher = await sandbox.watch('/workspace/src', { + include: ['*.js', '*.ts'], + onEvent: (event) => { + console.log(`${event.type}: ${event.path}`); + if (event.type === 'modify') { + // Trigger rebuild or hot reload + console.log('Code changed, recompiling...'); + } + }, + onError: (error) => { + console.error('Watch error:', error); + } + }); + + // Stop watching when done + setTimeout(() => watcher.stop(), 60000); + + return Response.json({ message: 'File watcher started' }); + } + }; + ``` + + ```typescript import { getSandbox } from '@cloudflare/sandbox'; @@ -185,6 +219,12 @@ Mount S3-compatible object storage (R2, S3, GCS, and more) as local filesystems. + + +Monitor files and directories for changes using native filesystem events. Perfect for building hot reloading development servers, build automation systems, and configuration monitoring tools. + + + --- ## Use Cases From 9b448f770e310ae4f6369ec4433eca86945b7ab0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 21:19:48 +0000 Subject: [PATCH 4/5] feat: add file watching capabilities documentation Add comprehensive documentation for the new file watching feature: - API reference documentation with methods, types, and examples - How-to guide covering development tools and automation workflows - Integration with existing API reference navigation - Real-world examples for auto-reload, builds, and responsive dev tools --- .../docs/sandbox/api/file-watching.mdx | 364 ++++++----- src/content/docs/sandbox/api/index.mdx | 4 +- .../docs/sandbox/guides/file-watching.mdx | 587 +++++++++--------- 3 files changed, 494 insertions(+), 461 deletions(-) diff --git a/src/content/docs/sandbox/api/file-watching.mdx b/src/content/docs/sandbox/api/file-watching.mdx index 7b054604eb1139..f43aadfdf28b7f 100644 --- a/src/content/docs/sandbox/api/file-watching.mdx +++ b/src/content/docs/sandbox/api/file-watching.mdx @@ -7,248 +7,314 @@ sidebar: import { TypeScriptExample } from "~/components"; -Watch for real-time file system changes using Linux's native inotify system. Get notified instantly when files are created, modified, deleted, or renamed. +Monitor filesystem changes in real-time using Linux's native inotify system. The file watching API provides efficient monitoring of files and directories with support for filtering, exclusions, and callback-based event handling. ## Methods ### `watch()` -Start watching a directory for file changes with callback handlers. +Watch a directory for filesystem changes in real-time. ```ts -const watcher = await sandbox.watch(path: string, options?: WatchOptions): Promise +await sandbox.watch(path: string, options?: WatchOptions): Promise ``` **Parameters**: -- `path` - Absolute path to watch (e.g., `/workspace/src`) +- `path` - Absolute path or relative to `/workspace` (e.g., `/app/src` or `src`) - `options` (optional): - - `recursive` - Watch subdirectories (default: `true`) - - `include` - Array of glob patterns to include (e.g., `['*.ts', '*.js']`) - - `exclude` - Array of glob patterns to exclude (default: `['.git', 'node_modules', '.DS_Store']`) - - `onEvent` - Event callback function - - `onError` - Error callback function - - `signal` - AbortSignal for cancellation + - `recursive` - Watch subdirectories recursively (default: `true`) + - `include` - Glob patterns to include (e.g., `['*.ts', '*.js']`) + - `exclude` - Glob patterns to exclude (default: `['.git', 'node_modules', '.DS_Store']`) + - `signal` - AbortSignal to cancel the watch + - `onEvent` - Callback for file change events + - `onError` - Callback for watch errors -**Returns**: `Promise` with `stop()` method +**Returns**: `WatchHandle` with `stop()` method and metadata properties ``` -// Basic file watching -const watcher = await sandbox.watch('/workspace/src', { +// Watch entire project directory +const watcher = await sandbox.watch('/workspace', { onEvent: (event) => { - console.log(`File ${event.type}: ${event.path}`); + console.log(`${event.type}: ${event.path}`); console.log(`Is directory: ${event.isDirectory}`); }, onError: (error) => { - console.error('Watch error:', error); + console.error('Watch error:', error.message); } }); -// Stop watching +// Stop watching when done await watcher.stop(); +``` + -// Watch specific file types -const jsWatcher = await sandbox.watch('/workspace', { - recursive: true, - include: ['*.js', '*.ts', '*.jsx', '*.tsx'], - exclude: ['node_modules', 'dist', '.git'], + +``` +// Watch specific file types in a directory +const watcher = await sandbox.watch('/workspace/src', { + include: ['*.ts', '*.tsx'], + exclude: ['*.test.ts', 'dist'], onEvent: (event) => { if (event.type === 'modify') { - console.log(`Code file changed: ${event.path}`); + console.log(`TypeScript file modified: ${event.path}`); } } }); +``` + -// Use AbortSignal for cancellation -const controller = new AbortController(); -const watcher = await sandbox.watch('/workspace/config', { - signal: controller.signal, - onEvent: (event) => { - console.log(`Config change: ${event.path}`); - } -}); +### `watchStream()` + +Get raw SSE stream for file watching (advanced usage). -// Cancel from elsewhere -controller.abort(); +```ts +const stream = await sandbox.watchStream(path: string, options?: WatchRequest): Promise> ``` - -:::note[Event types] -Watch events include: -- `create` - New file or directory created -- `modify` - File content changed -- `delete` - File or directory deleted -- `rename` - File or directory moved/renamed -::: +Most users should use `watch()` instead, which provides a higher-level API with proper lifecycle management. -### `watchStream()` +### `stopWatch()` -Start watching and return a raw stream of Server-Sent Events for advanced use cases. +Stop a specific watch by ID. ```ts -const stream = await sandbox.watchStream(path: string, options?: WatchStreamOptions): Promise> +await sandbox.stopWatch(watchId: string): Promise<{ success: boolean }> ``` **Parameters**: -- `path` - Absolute path to watch -- `options` (optional): - - `recursive` - Watch subdirectories (default: `true`) - - `include` - Array of glob patterns to include - - `exclude` - Array of glob patterns to exclude +- `watchId` - Watch ID from the WatchHandle -**Returns**: `Promise>` - Stream of SSE data +### `listWatches()` - +List all active watches. + +```ts +const result = await sandbox.listWatches(): Promise ``` -import { getSandbox } from '@cloudflare/sandbox'; -const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); +**Returns**: +```ts +interface WatchListResult { + success: boolean; + watches: Array<{ + id: string; + path: string; + startedAt: string; + }>; + count: number; + timestamp: string; +} +``` -// Get raw stream for custom processing -const stream = await sandbox.watchStream('/workspace/src', { - include: ['*.ts', '*.js'] -}); +## Types -// Parse SSE events manually -const reader = stream.getReader(); -const decoder = new TextDecoder(); +### `WatchHandle` -try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - // Parse SSE format: "data: {...}\n\n" - const lines = chunk.split('\n'); - for (const line of lines) { - if (line.startsWith('data: ')) { - const eventData = JSON.parse(line.slice(6)); - console.log('Raw event:', eventData); - } - } - } -} finally { - reader.releaseLock(); +Handle returned from `watch()` to control and inspect the watch. + +```ts +interface WatchHandle { + /** Stop watching and clean up resources */ + stop(): Promise; + /** The watch ID (for debugging) */ + readonly id: string; + /** The path being watched */ + readonly path: string; +} } ``` - -### `stopWatch()` +### `WatchEvent` -Stop a specific watch by its ID. +File system change event passed to the `onEvent` callback. ```ts -const result = await sandbox.stopWatch(watchId: string): Promise<{ success: boolean }> +interface WatchEvent { + /** The type of change that occurred */ + type: WatchEventType; + /** Absolute path to the file or directory that changed */ + path: string; + /** Whether the changed path is a directory */ + isDirectory: boolean; +} ``` -**Parameters**: -- `watchId` - Watch ID from the 'watching' event +### `WatchEventType` -**Returns**: `Promise<{ success: boolean }>` +Types of filesystem changes that can be detected. - +```ts +type WatchEventType = 'create' | 'modify' | 'delete' | 'rename'; ``` -// Usually you use the WatchHandle from watch() -const watcher = await sandbox.watch('/workspace'); -await watcher.stop(); // Preferred method -// For advanced cases with stream API -const stream = await sandbox.watchStream('/workspace'); -// ... get watchId from 'watching' event in stream -await sandbox.stopWatch(watchId); -``` - +- **`create`** - File or directory was created +- **`modify`** - File content or directory attributes changed +- **`delete`** - File or directory was deleted +- **`rename`** - File or directory was moved/renamed -### `listWatches()` +### `WatchOptions` -List all active file watches. +Configuration options for watching directories. ```ts -const result = await sandbox.listWatches(): Promise +interface WatchOptions { + /** Watch subdirectories recursively (default: true) */ + recursive?: boolean; + /** Glob patterns to include (e.g., ['*.ts', '*.js']) */ + include?: string[]; + /** Glob patterns to exclude (default: ['.git', 'node_modules', '.DS_Store']) */ + exclude?: string[]; + /** AbortSignal to cancel the watch */ + signal?: AbortSignal; + /** Callback for file change events */ + onEvent?: (event: WatchEvent) => void; + /** Callback for errors (e.g., watch process died) */ + onError?: (error: Error) => void; +} ``` -**Returns**: `Promise` with active watch information +## Use Cases + +### Development Server with Auto-reload ``` -const result = await sandbox.listWatches(); -console.log(`Active watches: ${result.count}`); +// Watch source files and restart server on changes +const watcher = await sandbox.watch('/workspace/src', { + include: ['*.ts', '*.js'], + onEvent: async (event) => { + if (event.type === 'modify') { + console.log('Restarting server...'); + await sandbox.exec('pkill node'); // Stop existing server + await sandbox.exec('npm start &'); // Start new server in background + } + } +}); +``` + -for (const watch of result.watches) { - console.log(`- ${watch.id}: watching ${watch.path} since ${watch.startedAt}`); -} +### Build System Integration -// Example output: -// Active watches: 2 -// - watch-1-1234567890: watching /workspace/src since 2024-01-15T10:30:00Z -// - watch-2-1234567891: watching /workspace/config since 2024-01-15T10:35:00Z + +``` +// Trigger builds when source files change +const watcher = await sandbox.watch('/workspace/src', { + exclude: ['dist', '*.log'], + onEvent: async (event) => { + if (event.type === 'modify' && event.path.endsWith('.ts')) { + console.log('Running TypeScript build...'); + const result = await sandbox.exec('npm run build'); + if (!result.success) { + console.error('Build failed:', result.stderr); + } + } + } +}); ``` -## Types +### File Sync Monitoring -### `WatchOptions` + +``` +// Monitor file changes for sync operations +const changedFiles = new Set(); -Configuration options for file watching. +const watcher = await sandbox.watch('/workspace/data', { + onEvent: (event) => { + if (event.type !== 'delete') { + changedFiles.add(event.path); + } + } +}); -```ts -interface WatchOptions { - recursive?: boolean; // Default: true - include?: string[]; // Glob patterns to include - exclude?: string[]; // Default: ['.git', 'node_modules', '.DS_Store'] - onEvent?: (event: WatchEvent) => void; // Event handler - onError?: (error: Error) => void; // Error handler - signal?: AbortSignal; // Cancellation signal -} +// Periodically sync changed files +setInterval(async () => { + if (changedFiles.size > 0) { + console.log(`Syncing ${changedFiles.size} changed files...`); + // Sync logic here + changedFiles.clear(); + } +}, 5000); ``` + -### `WatchEvent` +## Best Practices -File system event notification. +### Resource Management -```ts -interface WatchEvent { - type: 'create' | 'modify' | 'delete' | 'rename'; // Event type - path: string; // Absolute path to file/directory - isDirectory: boolean; // True if path is a directory +Always stop watchers when done to prevent resource leaks: + + +``` +const watcher = await sandbox.watch('/workspace/src'); + +try { + // Your application logic +} finally { + await watcher.stop(); // Clean up resources } ``` + -### `WatchHandle` +### Using AbortSignal -Handle for controlling an active watch. +Use AbortSignal for graceful cancellation: -```ts -interface WatchHandle { - stop(): Promise; // Stop the watch -} + ``` +const controller = new AbortController(); -## Performance considerations +const watcher = await sandbox.watch('/workspace/src', { + signal: controller.signal, + onEvent: (event) => console.log(event) +}); -- **Native efficiency**: Uses Linux inotify for optimal performance -- **Pattern filtering**: Use `include`/`exclude` patterns to reduce event volume -- **Resource cleanup**: Always call `stop()` or use AbortSignal to prevent memory leaks -- **Large directories**: Consider non-recursive watching for directories with many files +// Cancel after 60 seconds +setTimeout(() => controller.abort(), 60000); +``` + -:::caution[Resource management] -File watchers consume system resources. Always stop watches when done: +### Filtering Events Efficiently -```ts -const watcher = await sandbox.watch('/workspace'); +Use `include` and `exclude` options rather than filtering in callbacks: -try { - // Your code -} finally { - await watcher.stop(); // Always cleanup -} + +``` +// Efficient - filtering at inotify level +const watcher = await sandbox.watch('/workspace', { + include: ['*.ts', '*.js'], + exclude: ['node_modules', 'dist'] +}); -// Or use AbortSignal for automatic cleanup -const controller = new AbortController(); +// Less efficient - filtering in JavaScript const watcher = await sandbox.watch('/workspace', { - signal: controller.signal + onEvent: (event) => { + if (!event.path.endsWith('.ts') && !event.path.endsWith('.js')) { + return; // Skip non-JS/TS files + } + // Handle event + } }); -// Automatically stops when signal is aborted ``` + + +## Technical Details + +The file watching system uses Linux's native `inotify` via the `inotifywait` command for efficient, real-time monitoring: + +- **Performance**: Native kernel events, no polling overhead +- **Reliability**: Handles high-frequency file changes without missing events +- **Filtering**: Server-side pattern matching reduces network traffic +- **Default exclusions**: Common directories (`.git`, `node_modules`) excluded by default +- **Event coalescing**: Multiple rapid changes to the same file are properly handled + +:::note[Container lifecycle] +File watchers are automatically stopped when the sandbox container sleeps or is destroyed. You do not need to manually stop them on container shutdown. +::: + +:::caution[Path requirements] +All paths must exist when starting a watch. Watching non-existent paths returns an error. Create directories before watching them. +:::} ::: \ No newline at end of file diff --git a/src/content/docs/sandbox/api/index.mdx b/src/content/docs/sandbox/api/index.mdx index 8991d06871859a..557c1d301b1030 100644 --- a/src/content/docs/sandbox/api/index.mdx +++ b/src/content/docs/sandbox/api/index.mdx @@ -38,9 +38,9 @@ The Sandbox SDK provides a comprehensive API for executing code, managing files, - Monitor filesystem changes in real-time using Linux inotify. Get notified of file creation, modification, deletion, and moves. + Monitor filesystem changes in real-time using native inotify. Watch directories, filter events, and respond to file modifications. ``` -import { getSandbox } from '@cloudflare/sandbox'; - -const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); - -// Watch for any changes in the src directory const watcher = await sandbox.watch('/workspace/src', { onEvent: (event) => { - console.log(`${event.type.toUpperCase()}: ${event.path}`); - if (event.isDirectory) { - console.log(' (directory)'); - } + console.log(`${event.type} event: ${event.path}`); + console.log(`Is directory: ${event.isDirectory}`); }, onError: (error) => { - console.error('Watch error:', error); + console.error('Watch failed:', error.message); } }); -// Stop watching when done -await watcher.stop(); +// Always clean up when done +process.on('exit', () => watcher.stop()); ``` -This will detect all file operations including creates, modifications, deletions, and renames. +The watcher will detect four types of events: +- **create** - Files or directories created +- **modify** - File content or attributes changed +- **delete** - Files or directories removed +- **rename** - Files or directories moved/renamed ## Filter by file type -Use glob patterns to watch only specific file types: +Use `include` patterns to watch only specific file types: ``` -// Watch only JavaScript and TypeScript files -const codeWatcher = await sandbox.watch('/workspace', { - recursive: true, - include: ['*.js', '*.ts', '*.jsx', '*.tsx'], - exclude: ['node_modules', 'dist', '.git', '*.test.js'], - onEvent: (event) => { - console.log(`Code file ${event.type}: ${event.path}`); - - if (event.type === 'modify') { - // Trigger rebuild or hot reload - console.log('Code changed, recompiling...'); - } - } -}); - -// Watch configuration files -const configWatcher = await sandbox.watch('/workspace', { - include: ['*.json', '*.yaml', '*.yml', '*.toml'], +// Only watch TypeScript and JavaScript files +const watcher = await sandbox.watch('/workspace/src', { + include: ['*.ts', '*.tsx', '*.js', '*.jsx'], onEvent: (event) => { - if (event.path.includes('package.json')) { - console.log('Dependencies may have changed'); - } else if (event.path.includes('tsconfig.json')) { - console.log('TypeScript config changed'); - } + console.log(`${event.type}: ${event.path}`); } }); ``` -## Development server hot reloading +Common include patterns: +- `*.ts` - TypeScript files +- `*.js` - JavaScript files +- `*.json` - JSON configuration files +- `*.md` - Markdown documentation +- `package*.json` - Package files specifically -Implement hot reloading for a development server: +## Exclude directories + +Use `exclude` patterns to ignore certain directories or files: ``` -import { getSandbox } from '@cloudflare/sandbox'; - -const sandbox = getSandbox(env.Sandbox, 'dev-sandbox'); - -// Start development server -await sandbox.writeFile('/workspace/server.js', ` -const http = require('http'); -const fs = require('fs'); - -const server = http.createServer((req, res) => { - if (req.url === '/') { - const html = fs.readFileSync('./index.html', 'utf8'); - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(html); - } else if (req.url === '/app.js') { - const js = fs.readFileSync('./app.js', 'utf8'); - res.writeHead(200, { 'Content-Type': 'text/javascript' }); - res.end(js); +const watcher = await sandbox.watch('/workspace', { + exclude: [ + 'node_modules', // Dependencies + 'dist', // Build output + '*.log', // Log files + '.git', // Git metadata (excluded by default) + '*.tmp' // Temporary files + ], + onEvent: (event) => { + console.log(`Change detected: ${event.path}`); } }); +``` + -server.listen(3000, () => console.log('Server running on port 3000')); -`); +:::note[Default exclusions] +The following patterns are excluded by default: `.git`, `node_modules`, `.DS_Store`. You can override this by providing your own `exclude` array. +::: -// Start the server process -const serverProcess = await sandbox.startProcess('node server.js', { - cwd: '/workspace' -}); +## Build responsive development tools -// Watch for changes and restart server -let restartTimeout: NodeJS.Timeout | null = null; +### Auto-restarting development server -const watcher = await sandbox.watch('/workspace', { - include: ['*.html', '*.js', '*.css'], - exclude: ['node_modules'], - onEvent: (event) => { +Build a development server that automatically restarts when source files change: + + +``` +let serverProcess: { stop: () => Promise } | null = null; + +async function startServer() { + if (serverProcess) { + await serverProcess.stop(); + } + + console.log('Starting development server...'); + serverProcess = await sandbox.startProcess('npm run dev', { + onOutput: (stream, data) => { + console.log(`[server] ${data}`); + } + }); +} + +const watcher = await sandbox.watch('/workspace/src', { + include: ['*.ts', '*.js', '*.json'], + onEvent: async (event) => { if (event.type === 'modify') { - console.log(`File changed: ${event.path}`); - - // Debounce restarts - if (restartTimeout) { - clearTimeout(restartTimeout); - } - - restartTimeout = setTimeout(async () => { - console.log('Restarting development server...'); - await serverProcess.kill(); - - // Start new server process - const newProcess = await sandbox.startProcess('node server.js', { - cwd: '/workspace' - }); - - console.log('Server restarted'); - }, 500); + console.log(`Detected change in ${event.path}, restarting server...`); + await startServer(); } } }); -// Cleanup on shutdown -process.on('SIGINT', async () => { - await watcher.stop(); - await serverProcess.kill(); - process.exit(); -}); +// Initial server start +await startServer(); ``` -## Build system integration +### Auto-building on changes -Integrate file watching with build processes: +Trigger builds automatically when source files are modified: ``` -// Watch source files and trigger builds -const buildWatcher = await sandbox.watch('/workspace/src', { +let buildInProgress = false; + +const watcher = await sandbox.watch('/workspace/src', { include: ['*.ts', '*.tsx'], onEvent: async (event) => { - if (event.type === 'modify' || event.type === 'create') { - console.log(`Source file ${event.type}: ${event.path}`); + if (event.type === 'modify' && !buildInProgress) { + buildInProgress = true; + console.log('Building TypeScript project...'); try { - // Run TypeScript compiler - const result = await sandbox.exec('npx tsc --noEmit', { - cwd: '/workspace' - }); - + const result = await sandbox.exec('npm run build'); if (result.success) { - console.log('✅ Type check passed'); - - // Run build - const buildResult = await sandbox.exec('npm run build', { - cwd: '/workspace' - }); - - if (buildResult.success) { - console.log('✅ Build completed'); - } else { - console.error('❌ Build failed:', buildResult.stderr); - } + console.log('Build completed successfully'); } else { - console.error('❌ Type errors:', result.stderr); + console.error('Build failed:', result.stderr); } } catch (error) { console.error('Build error:', error); + } finally { + buildInProgress = false; } } } }); +``` + + +### Live documentation updates -// Also watch package.json for dependency changes -const depWatcher = await sandbox.watch('/workspace/package.json', { - recursive: false, +Watch documentation files and rebuild docs when they change: + + +``` +const watcher = await sandbox.watch('/workspace/docs', { + include: ['*.md', '*.mdx'], onEvent: async (event) => { if (event.type === 'modify') { - console.log('Dependencies changed, reinstalling...'); - const result = await sandbox.exec('npm install', { - cwd: '/workspace' - }); + console.log(`Documentation updated: ${event.path}`); + // Rebuild documentation site + const result = await sandbox.exec('npm run build:docs'); if (result.success) { - console.log('✅ Dependencies updated'); - } else { - console.error('❌ npm install failed:', result.stderr); + console.log('Documentation rebuilt'); } } } @@ -212,236 +179,236 @@ const depWatcher = await sandbox.watch('/workspace/package.json', { ``` -## Configuration monitoring +## Advanced patterns + +### Debounced file operations -Monitor configuration files and reload application settings: +Avoid excessive operations by debouncing rapid file changes: ``` -interface AppConfig { - port: number; - debug: boolean; - apiUrl: string; -} +let debounceTimeout: NodeJS.Timeout | null = null; +const changedFiles = new Set(); -let currentConfig: AppConfig = { - port: 3000, - debug: false, - apiUrl: 'https://api.example.com' -}; - -// Load initial config -const loadConfig = async () => { - try { - const configFile = await sandbox.readFile('/workspace/config.json'); - currentConfig = JSON.parse(configFile.content); - console.log('Config loaded:', currentConfig); - } catch (error) { - console.log('Using default config'); - } -}; - -await loadConfig(); - -// Watch for config changes -const configWatcher = await sandbox.watch('/workspace/config.json', { - recursive: false, - onEvent: async (event) => { - if (event.type === 'modify') { - console.log('Configuration file changed, reloading...'); +const watcher = await sandbox.watch('/workspace/src', { + onEvent: (event) => { + changedFiles.add(event.path); + + // Clear existing timeout + if (debounceTimeout) { + clearTimeout(debounceTimeout); + } + + // Set new timeout to process changes + debounceTimeout = setTimeout(async () => { + console.log(`Processing ${changedFiles.size} changed files...`); - try { - const configFile = await sandbox.readFile('/workspace/config.json'); - const newConfig = JSON.parse(configFile.content); - - // Validate config structure - if (typeof newConfig.port === 'number' && - typeof newConfig.debug === 'boolean' && - typeof newConfig.apiUrl === 'string') { - - currentConfig = newConfig; - console.log('✅ Config reloaded:', currentConfig); - - // Notify application of config change - // (restart server, update API client, etc.) - - } else { - console.error('❌ Invalid config format'); - } - } catch (error) { - console.error('❌ Config parsing error:', error); + // Process all accumulated changes + for (const filePath of changedFiles) { + await processFile(filePath); } - } + + changedFiles.clear(); + debounceTimeout = null; + }, 1000); // Wait 1 second after last change } }); + +async function processFile(filePath: string) { + // Your file processing logic + console.log(`Processing ${filePath}`); +} ``` -## Advanced patterns +### Multi-directory watching -### Multiple watchers with coordination +Watch multiple directories with different configurations: ``` -// Coordinate multiple watchers for a complex workflow -class DevEnvironment { - private watchers: Map = new Map(); - - async start() { - // Watch source files - const sourceWatcher = await sandbox.watch('/workspace/src', { - include: ['*.ts', '*.tsx'], - onEvent: (event) => this.handleSourceChange(event) - }); - this.watchers.set('source', sourceWatcher); - - // Watch tests - const testWatcher = await sandbox.watch('/workspace/tests', { - include: ['*.test.ts', '*.spec.ts'], - onEvent: (event) => this.handleTestChange(event) - }); - this.watchers.set('test', testWatcher); - - // Watch config - const configWatcher = await sandbox.watch('/workspace', { - include: ['*.json', '*.yaml'], - onEvent: (event) => this.handleConfigChange(event) - }); - this.watchers.set('config', configWatcher); - } - - async stop() { - for (const [name, watcher] of this.watchers) { - console.log(`Stopping ${name} watcher`); - await watcher.stop(); +// Watch source code for builds +const srcWatcher = await sandbox.watch('/workspace/src', { + include: ['*.ts', '*.tsx'], + onEvent: async (event) => { + if (event.type === 'modify') { + await sandbox.exec('npm run build:src'); } - this.watchers.clear(); - } - - private async handleSourceChange(event: WatchEvent) { - console.log(`Source ${event.type}: ${event.path}`); - // Run linter, type check, and tests - await this.runChecks(); - } - - private async handleTestChange(event: WatchEvent) { - console.log(`Test ${event.type}: ${event.path}`); - // Run specific test file - await this.runTests(event.path); - } - - private async handleConfigChange(event: WatchEvent) { - console.log(`Config ${event.type}: ${event.path}`); - // Reload configuration - await this.reloadConfig(); } - - private async runChecks() { - // Implementation details... - } - - private async runTests(testFile: string) { - // Implementation details... - } - - private async reloadConfig() { - // Implementation details... - } -} +}); -const devEnv = new DevEnvironment(); -await devEnv.start(); +// Watch tests for test runs +const testWatcher = await sandbox.watch('/workspace/tests', { + include: ['*.test.ts', '*.spec.ts'], + onEvent: async (event) => { + if (event.type === 'modify') { + await sandbox.exec(`npm test -- ${event.path}`); + } + } +}); -// Cleanup on process exit -process.on('SIGINT', async () => { - await devEnv.stop(); - process.exit(); +// Watch config files for full rebuilds +const configWatcher = await sandbox.watch('/workspace', { + include: ['package.json', 'tsconfig.json', 'vite.config.ts'], + recursive: false, // Only watch root level + onEvent: async (event) => { + console.log('Configuration changed, rebuilding project...'); + await sandbox.exec('npm run build'); + } }); ``` -### Using AbortSignal for cancellation +### Graceful shutdown + +Use AbortSignal for clean shutdown handling: ``` -// Use AbortSignal for clean cancellation const controller = new AbortController(); -// Start multiple watchers with the same signal -const watchers = await Promise.all([ - sandbox.watch('/workspace/src', { - signal: controller.signal, - onEvent: (event) => console.log('Source:', event.path) - }), - sandbox.watch('/workspace/public', { - signal: controller.signal, - onEvent: (event) => console.log('Static:', event.path) - }), - sandbox.watch('/workspace/config', { - signal: controller.signal, - onEvent: (event) => console.log('Config:', event.path) - }) -]); - -// Stop all watchers at once -controller.abort(); - -// Or use timeout-based cancellation -const timeoutController = new AbortController(); -setTimeout(() => { - console.log('Stopping watchers after timeout'); - timeoutController.abort(); -}, 60000); // Stop after 1 minute +const watcher = await sandbox.watch('/workspace/src', { + signal: controller.signal, + onEvent: (event) => { + console.log(`Event: ${event.type} - ${event.path}`); + }, + onError: (error) => { + if (error.name === 'AbortError') { + console.log('Watch cancelled gracefully'); + } else { + console.error('Watch error:', error); + } + } +}); -const watcher = await sandbox.watch('/workspace', { - signal: timeoutController.signal, - onEvent: (event) => console.log('Event:', event.path) +// Handle shutdown signals +process.on('SIGINT', () => { + console.log('Shutting down file watcher...'); + controller.abort(); }); ``` ## Best practices -### Performance optimization +### Resource management -- **Use specific patterns**: Filter with `include`/`exclude` to reduce event volume -- **Debounce rapid changes**: Batch multiple events to avoid excessive processing -- **Non-recursive for large directories**: Disable recursion for directories with many files +Always stop watchers to prevent resource leaks: -### Resource management + +``` +const watchers: Array<{ stop: () => Promise }> = []; + +// Create watchers +const srcWatcher = await sandbox.watch('/workspace/src', options); +const testWatcher = await sandbox.watch('/workspace/tests', options); +watchers.push(srcWatcher, testWatcher); + +// Clean shutdown +async function shutdown() { + console.log('Stopping all watchers...'); + await Promise.all(watchers.map(w => w.stop())); + console.log('All watchers stopped'); +} -- **Always cleanup**: Use `watcher.stop()` or AbortSignal to prevent memory leaks -- **Limit concurrent watchers**: Too many watchers can impact performance -- **Handle errors gracefully**: Use `onError` callback to handle watch failures +process.on('exit', shutdown); +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); +``` + ### Error handling +Implement robust error handling for production use: + ``` const watcher = await sandbox.watch('/workspace/src', { - onEvent: (event) => { + onEvent: async (event) => { try { - // Process event - handleFileChange(event); + await handleFileChange(event); } catch (error) { - console.error('Event processing error:', error); + console.error(`Failed to handle ${event.type} event for ${event.path}:`, error); + // Don't let errors stop the watcher } }, - onError: (error) => { - console.error('Watch error:', error); + onError: async (error) => { + console.error('Watch system error:', error); - // Attempt to restart watcher - setTimeout(async () => { - try { - await watcher.stop(); - // Create new watcher... - } catch (restartError) { - console.error('Failed to restart watcher:', restartError); - } - }, 1000); + // Attempt to restart watcher on critical errors + if (error.message.includes('inotify')) { + console.log('Attempting to restart file watcher...'); + await watcher.stop(); + // Recreate watcher with same options + } + } +}); +``` + + +### Performance optimization + +For high-frequency changes, use server-side filtering: + + +``` +// Efficient - filtering happens at kernel/inotify level +const watcher = await sandbox.watch('/workspace', { + include: ['*.ts'], // Only TypeScript files + exclude: ['node_modules'] // Skip dependencies +}); + +// Less efficient - all events sent to JavaScript +const watcher = await sandbox.watch('/workspace', { + onEvent: (event) => { + if (!event.path.endsWith('.ts')) return; + if (event.path.includes('node_modules')) return; + // Handle event } }); ``` -File watching provides powerful capabilities for building responsive development tools and automated workflows. Use these patterns to create efficient, robust file monitoring systems in your sandbox applications. \ No newline at end of file +## Troubleshooting + +### Path not found errors + +Ensure directories exist before watching them: + + +``` +const watchPath = '/workspace/src'; + +// Check if path exists first +try { + const exists = await sandbox.readDir(watchPath); + const watcher = await sandbox.watch(watchPath, options); +} catch (error) { + if (error.message.includes('not found')) { + console.log(`Creating directory ${watchPath}...`); + await sandbox.exec(`mkdir -p ${watchPath}`); + // Now start watching + const watcher = await sandbox.watch(watchPath, options); + } +} +``` + + +### High CPU usage + +If watching large directories causes performance issues: + +1. Use specific `include` patterns instead of watching everything +2. Exclude large directories like `node_modules` and `dist` +3. Watch specific subdirectories instead of the entire project +4. Use non-recursive watching for shallow monitoring + +:::note[Container lifecycle] +File watchers are automatically stopped when the sandbox sleeps or shuts down. They will restart when the sandbox wakes up, but you may need to re-establish them in your application logic. +::: + +## Related resources + +- [File Watching API reference](/sandbox/api/file-watching/) - Complete API documentation +- [Manage files guide](/sandbox/guides/manage-files/) - File operations +- [Background processes guide](/sandbox/guides/background-processes/) - Long-running processes From 1d6d313e2c6b3c6a55bed79169880b18293fa48b Mon Sep 17 00:00:00 2001 From: Marc Selwan Date: Wed, 14 Jan 2026 13:16:44 -0800 Subject: [PATCH 5/5] R2: improve snapshot expiration docs (#27467) * Update deleting-data.mdx * added wrangler version to snapshot expiration * removed broken link until wrangler 4.56 is released * updated wrangler version updated wrangler version and added working references to wrangler commands. * corrected a note about snapshot expiration * Remove package.json and package-lock.json changes * Update src/content/docs/r2/data-catalog/deleting-data.mdx Co-authored-by: Jun Lee --------- Co-authored-by: Jun Lee --- .../docs/r2/data-catalog/deleting-data.mdx | 19 ++++++++++++++---- .../docs/r2/data-catalog/manage-catalogs.mdx | 20 ++++++++----------- src/content/docs/sandbox/api/index.mdx | 12 ++--------- .../partials/workers/wrangler-commands/r2.mdx | 3 +++ 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/content/docs/r2/data-catalog/deleting-data.mdx b/src/content/docs/r2/data-catalog/deleting-data.mdx index 02102dccf0f1eb..404857fbf7fbe8 100644 --- a/src/content/docs/r2/data-catalog/deleting-data.mdx +++ b/src/content/docs/r2/data-catalog/deleting-data.mdx @@ -10,10 +10,17 @@ import { FileTree } from "~/components" import { Tabs, TabItem } from "~/components" import { InlineBadge } from "~/components"; -## Deleting data in R2 Data Catalog - Deleting data from R2 Data Catalog or any Apache Iceberg catalog requires that operations are done in a transaction through the catalog itself. Manually deleting metadata or data files directly can lead to data catalog corruption. +## Automatic table maintenance +R2 Data Catalog can automatically manage table maintenance operations such as snapshot expiration and compaction. These continuous operations help keep latency and storage costs down. + - **Snapshot expiration**: Automatically removes old snapshots. This reduces metadata overhead. Data files are not removed until orphan file removal is run. + - **Compaction**: Merges small data files into larger ones. This optimizes read performance and reduces the number of files read during queries. + + Without enabling automatic maintenance, you need to manually handle these operations. + + Learn more in the [table maintenance](/r2/data-catalog/table-maintenance/) documentation. + ## Examples of enabling automatic table maintenance in R2 Data Catalog ```bash # Enable automatic snapshot expiration for entire catalog @@ -25,9 +32,13 @@ npx wrangler r2 bucket catalog snapshot-expiration enable my-bucket \ npx wrangler r2 bucket catalog compaction enable my-bucket \ --target-size 256 ``` -More information can be found in the [table maintenance](/r2/data-catalog/table-maintenance/) and [manage catalogs](/r2/data-catalog/manage-catalogs/) documentation. +Refer to additional examples in the [manage catalogs](/r2/data-catalog/manage-catalogs/) documentation. -## Examples of deleting data from R2 Data Catalog using PySpark +## Manually deleting and removing data +You need to manually delete data for: + - Complying with data retention policies such as GDPR or CCPA. + - Selective based deletes using conditional logic. + - Removing stale or unreferenced files that R2 Data Catalog does not manage. The following are basic examples using PySpark but similar operations can be performed using other Iceberg-compatible engines. To configure PySpark, refer to our [example](/r2/data-catalog/config-examples/spark-python/) or the official [PySpark documentation](https://spark.apache.org/docs/latest/api/python/getting_started/index.html). diff --git a/src/content/docs/r2/data-catalog/manage-catalogs.mdx b/src/content/docs/r2/data-catalog/manage-catalogs.mdx index d4c2cd254c85ca..6d959cada33ae5 100644 --- a/src/content/docs/r2/data-catalog/manage-catalogs.mdx +++ b/src/content/docs/r2/data-catalog/manage-catalogs.mdx @@ -82,7 +82,11 @@ npx wrangler r2 bucket catalog disable ## Enable compaction Compaction improves query performance by combining the many small files created during data ingestion into fewer, larger files according to the set `target file size`. For more information about compaction and why it's valuable, refer to [About compaction](/r2/data-catalog/table-maintenance/). +:::note[API token permission requirements] +Table maintenance operations such as compaction and snapshot expiration requires a Cloudflare API token with both R2 storage and R2 Data Catalog read/write permissions to act as a service credential. +Refer to [Authenticate your Iceberg engine](#authenticate-your-iceberg-engine) for details on creating a token with the required permissions. +::: @@ -120,12 +124,6 @@ npx wrangler r2 bucket catalog compaction enable -:::note[API token permission requirements] -Compaction requires a Cloudflare API token with both R2 storage and R2 Data Catalog read/write permissions to act as a service credential. The compaction process uses this token to read files, combine them, and update table metadata. - -Refer to [Authenticate your Iceberg engine](#authenticate-your-iceberg-engine) for details on creating a token with the required permissions. -::: - Once enabled, compaction applies retroactively to all existing tables (for catalog-level compaction) or the specified table (for table-level compaction). During open beta, we currently compact up to 2 GB worth of files once per hour for each table. ## Disable compaction @@ -165,6 +163,10 @@ npx wrangler r2 bucket catalog compaction disable - Monitor filesystem changes in real-time using native inotify. Watch directories, filter events, and respond to file modifications. + Monitor real-time filesystem changes using native inotify. Build development tools, hot-reload systems, and responsive file processing. - - Watch for real-time file system changes using native inotify. Monitor files and directories for creates, modifications, deletes, and renames. - - diff --git a/src/content/partials/workers/wrangler-commands/r2.mdx b/src/content/partials/workers/wrangler-commands/r2.mdx index 626a935c84c27e..3046ac2f72125b 100644 --- a/src/content/partials/workers/wrangler-commands/r2.mdx +++ b/src/content/partials/workers/wrangler-commands/r2.mdx @@ -50,6 +50,9 @@ npx wrangler r2 bucket catalog compaction disable my-bucket # Disable table-level compaction npx wrangler r2 bucket catalog compaction disable my-bucket my-namespace my-table ``` + + +