@cloudflare/sandbox 0.0.0-cbb7fcd → 0.0.0-cecde0a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/Dockerfile +73 -9
- package/README.md +312 -12
- package/container_src/handler/exec.ts +337 -0
- package/container_src/handler/file.ts +844 -0
- package/container_src/handler/git.ts +182 -0
- package/container_src/handler/ports.ts +314 -0
- package/container_src/handler/process.ts +640 -0
- package/container_src/index.ts +102 -2647
- package/container_src/types.ts +103 -0
- package/package.json +2 -7
- package/src/client.ts +335 -1247
- package/src/index.ts +20 -134
- package/src/request-handler.ts +144 -0
- package/src/sandbox.ts +645 -0
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +386 -0
- package/tests/client.example.ts +0 -308
- package/tests/connection-test.ts +0 -81
- package/tests/simple-test.ts +0 -81
- package/tests/test1.ts +0 -281
- package/tests/test2.ts +0 -929
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @cloudflare/sandbox
|
|
2
2
|
|
|
3
|
+
## 0.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#20](https://github.com/cloudflare/sandbox-sdk/pull/20) [`f106fda`](https://github.com/cloudflare/sandbox-sdk/commit/f106fdac98e7ef35677326290d45cbf3af88982c) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - add preview URLs and dynamic port forwarding
|
|
8
|
+
|
|
9
|
+
## 0.0.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`60af265`](https://github.com/cloudflare/sandbox-sdk/commit/60af265d834e83fd30a921a3e1be232f13fe24da) Thanks [@threepointone](https://github.com/threepointone)! - update dependencies
|
|
14
|
+
|
|
15
|
+
## 0.0.7
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [`d1c7c99`](https://github.com/cloudflare/sandbox-sdk/commit/d1c7c99df6555eff71bcd59852e4b8eed2ad8cb6) Thanks [@threepointone](https://github.com/threepointone)! - fix file operations
|
|
20
|
+
|
|
21
|
+
## 0.0.6
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- [#9](https://github.com/cloudflare/sandbox-sdk/pull/9) [`24f5470`](https://github.com/cloudflare/sandbox-sdk/commit/24f547048d5a26137de4656cea13d83ad2cc0b43) Thanks [@ItsWendell](https://github.com/ItsWendell)! - fix baseUrl for stub and stub forwarding
|
|
26
|
+
|
|
3
27
|
## 0.0.5
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/Dockerfile
CHANGED
|
@@ -1,16 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sandbox base image with development tools, Python, Node.js, and Bun
|
|
2
|
+
FROM ubuntu:22.04
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
# Prevent interactive prompts during package installation
|
|
5
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
6
|
+
|
|
7
|
+
# Install essential system packages and development tools
|
|
8
|
+
RUN apt-get update && apt-get install -y \
|
|
9
|
+
# Basic utilities
|
|
10
|
+
curl \
|
|
11
|
+
wget \
|
|
12
|
+
git \
|
|
13
|
+
unzip \
|
|
14
|
+
zip \
|
|
15
|
+
# Process management
|
|
16
|
+
procps \
|
|
17
|
+
htop \
|
|
18
|
+
# Build tools
|
|
19
|
+
build-essential \
|
|
20
|
+
pkg-config \
|
|
21
|
+
# Network tools
|
|
22
|
+
net-tools \
|
|
23
|
+
iputils-ping \
|
|
24
|
+
dnsutils \
|
|
25
|
+
# Text processing
|
|
26
|
+
jq \
|
|
27
|
+
vim \
|
|
28
|
+
nano \
|
|
29
|
+
# Python dependencies
|
|
30
|
+
python3.11 \
|
|
31
|
+
python3.11-dev \
|
|
32
|
+
python3-pip \
|
|
33
|
+
# Other useful tools
|
|
34
|
+
sudo \
|
|
35
|
+
ca-certificates \
|
|
36
|
+
gnupg \
|
|
37
|
+
lsb-release \
|
|
38
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
39
|
+
|
|
40
|
+
# Set Python 3.11 as default python3
|
|
41
|
+
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
|
|
42
|
+
|
|
43
|
+
# Install Node.js 22 LTS
|
|
44
|
+
# Using the official NodeSource repository setup script
|
|
45
|
+
RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
|
|
46
|
+
&& mkdir -p /etc/apt/keyrings \
|
|
47
|
+
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
|
48
|
+
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
|
|
49
|
+
&& apt-get update \
|
|
50
|
+
&& apt-get install -y nodejs \
|
|
51
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
52
|
+
|
|
53
|
+
# Install Bun using the official installation script
|
|
54
|
+
RUN curl -fsSL https://bun.sh/install | bash \
|
|
55
|
+
&& mv /root/.bun/bin/bun /usr/local/bin/bun \
|
|
56
|
+
&& mv /root/.bun/bin/bunx /usr/local/bin/bunx \
|
|
57
|
+
&& rm -rf /root/.bun
|
|
58
|
+
|
|
59
|
+
# Install global npm packages as root
|
|
60
|
+
RUN npm install -g yarn pnpm
|
|
61
|
+
|
|
62
|
+
# Set up working directory
|
|
5
63
|
WORKDIR /app
|
|
6
64
|
|
|
7
|
-
#
|
|
8
|
-
RUN
|
|
65
|
+
# Verify installations
|
|
66
|
+
RUN python3 --version && \
|
|
67
|
+
node --version && \
|
|
68
|
+
npm --version && \
|
|
69
|
+
bun --version && \
|
|
70
|
+
yarn --version && \
|
|
71
|
+
pnpm --version
|
|
9
72
|
|
|
10
|
-
|
|
11
|
-
|
|
73
|
+
# Copy container source files
|
|
74
|
+
COPY container_src/ ./
|
|
12
75
|
|
|
76
|
+
# Expose the application port
|
|
13
77
|
EXPOSE 3000
|
|
14
|
-
# Run
|
|
15
|
-
CMD ["bun", "index.ts"]
|
|
16
78
|
|
|
79
|
+
# Run the application
|
|
80
|
+
CMD ["bun", "index.ts"]
|
package/README.md
CHANGED
|
@@ -47,19 +47,319 @@ import { getSandbox } from "@cloudflare/sandbox";
|
|
|
47
47
|
export default {
|
|
48
48
|
async fetch(request: Request, env: Env) {
|
|
49
49
|
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
|
|
50
|
-
|
|
50
|
+
const result = await sandbox.exec("ls -la");
|
|
51
|
+
return Response.json(result);
|
|
51
52
|
},
|
|
52
53
|
};
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
### Methods
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
- `
|
|
56
|
+
### Core Methods
|
|
57
|
+
|
|
58
|
+
#### Command Execution
|
|
59
|
+
- `exec(command: string, options?: ExecOptions)`: Execute a command and return the complete result.
|
|
60
|
+
- `execStream(command: string, options?: StreamOptions)`: Execute a command with real-time streaming (returns ReadableStream).
|
|
61
|
+
|
|
62
|
+
#### Process Management
|
|
63
|
+
- `startProcess(command: string, options?: ProcessOptions)`: Start a background process.
|
|
64
|
+
- `listProcesses()`: List all running processes.
|
|
65
|
+
- `getProcess(id: string)`: Get details of a specific process.
|
|
66
|
+
- `killProcess(id: string, signal?: string)`: Kill a specific process.
|
|
67
|
+
- `killAllProcesses()`: Kill all running processes.
|
|
68
|
+
- `streamProcessLogs(processId: string, options?: { signal?: AbortSignal })`: Stream logs from a running process (returns ReadableStream).
|
|
69
|
+
|
|
70
|
+
#### File Operations
|
|
71
|
+
- `gitCheckout(repoUrl: string, options: { branch?: string; targetDir?: string })`: Checkout a git repository.
|
|
72
|
+
- `mkdir(path: string, options?: { recursive?: boolean })`: Create a directory.
|
|
73
|
+
- `writeFile(path: string, content: string, options?: { encoding?: string })`: Write content to a file.
|
|
74
|
+
- `readFile(path: string, options?: { encoding?: string })`: Read content from a file.
|
|
75
|
+
- `deleteFile(path: string)`: Delete a file.
|
|
76
|
+
- `renameFile(oldPath: string, newPath: string)`: Rename a file.
|
|
77
|
+
- `moveFile(sourcePath: string, destinationPath: string)`: Move a file.
|
|
78
|
+
|
|
79
|
+
#### Port Management
|
|
80
|
+
- `exposePort(port: number, options: { name?: string; hostname: string })`: Expose a port for external access.
|
|
81
|
+
- `unexposePort(port: number)`: Unexpose a previously exposed port.
|
|
82
|
+
- `getExposedPorts(hostname: string)`: List all exposed ports with their preview URLs.
|
|
83
|
+
|
|
84
|
+
### Beautiful AsyncIterable Streaming APIs ✨
|
|
85
|
+
|
|
86
|
+
The SDK provides streaming methods that return `ReadableStream` for RPC compatibility, along with a `parseSSEStream` utility to convert them to typed AsyncIterables:
|
|
87
|
+
|
|
88
|
+
#### Stream Command Output
|
|
89
|
+
```typescript
|
|
90
|
+
import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
|
|
91
|
+
|
|
92
|
+
// Get the stream and convert to AsyncIterable
|
|
93
|
+
const stream = await sandbox.execStream('npm run build');
|
|
94
|
+
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
95
|
+
switch (event.type) {
|
|
96
|
+
case 'start':
|
|
97
|
+
console.log(`Build started: ${event.command}`);
|
|
98
|
+
break;
|
|
99
|
+
case 'stdout':
|
|
100
|
+
console.log(`[OUT] ${event.data}`);
|
|
101
|
+
break;
|
|
102
|
+
case 'stderr':
|
|
103
|
+
console.error(`[ERR] ${event.data}`);
|
|
104
|
+
break;
|
|
105
|
+
case 'complete':
|
|
106
|
+
console.log(`Build finished with exit code: ${event.exitCode}`);
|
|
107
|
+
break;
|
|
108
|
+
case 'error':
|
|
109
|
+
console.error(`Build error: ${event.error}`);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Stream Process Logs
|
|
116
|
+
```typescript
|
|
117
|
+
import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';
|
|
118
|
+
|
|
119
|
+
// Monitor background process logs
|
|
120
|
+
const webServer = await sandbox.startProcess('node server.js');
|
|
121
|
+
|
|
122
|
+
const logStream = await sandbox.streamProcessLogs(webServer.id);
|
|
123
|
+
for await (const log of parseSSEStream<LogEvent>(logStream)) {
|
|
124
|
+
if (log.type === 'stdout') {
|
|
125
|
+
console.log(`Server: ${log.data}`);
|
|
126
|
+
} else if (log.type === 'stderr' && log.data.includes('ERROR')) {
|
|
127
|
+
// React to errors
|
|
128
|
+
await handleError(log);
|
|
129
|
+
} else if (log.type === 'exit') {
|
|
130
|
+
console.log(`Server exited with code: ${log.exitCode}`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Why parseSSEStream?
|
|
137
|
+
|
|
138
|
+
The streaming methods return `ReadableStream<Uint8Array>` to ensure compatibility across Durable Object RPC boundaries. The `parseSSEStream` utility converts these streams into typed AsyncIterables, giving you the best of both worlds:
|
|
139
|
+
|
|
140
|
+
- **RPC Compatibility**: ReadableStream can be serialized across process boundaries
|
|
141
|
+
- **Beautiful APIs**: AsyncIterable provides clean `for await` syntax with typed events
|
|
142
|
+
- **Type Safety**: Full TypeScript support with `ExecEvent` and `LogEvent` types
|
|
143
|
+
|
|
144
|
+
#### Advanced Examples
|
|
145
|
+
|
|
146
|
+
##### CI/CD Build System
|
|
147
|
+
```typescript
|
|
148
|
+
import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
|
|
149
|
+
|
|
150
|
+
export async function runBuild(env: Env, buildId: string) {
|
|
151
|
+
const sandbox = getSandbox(env.Sandbox, buildId);
|
|
152
|
+
const buildLog: string[] = [];
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const stream = await sandbox.execStream('npm run build');
|
|
156
|
+
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
157
|
+
buildLog.push(`[${event.type}] ${event.data || ''}`);
|
|
158
|
+
|
|
159
|
+
if (event.type === 'complete') {
|
|
160
|
+
await env.BUILDS.put(buildId, {
|
|
161
|
+
status: event.exitCode === 0 ? 'success' : 'failed',
|
|
162
|
+
exitCode: event.exitCode,
|
|
163
|
+
logs: buildLog.join('\n'),
|
|
164
|
+
duration: Date.now() - new Date(event.timestamp).getTime()
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
await env.BUILDS.put(buildId, {
|
|
170
|
+
status: 'error',
|
|
171
|
+
error: error.message,
|
|
172
|
+
logs: buildLog.join('\n')
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
##### System Monitoring
|
|
179
|
+
```typescript
|
|
180
|
+
import { parseSSEStream, type LogEvent } from '@cloudflare/sandbox';
|
|
181
|
+
|
|
182
|
+
export default {
|
|
183
|
+
async scheduled(controller: ScheduledController, env: Env) {
|
|
184
|
+
const sandbox = getSandbox(env.Sandbox, 'monitor');
|
|
185
|
+
|
|
186
|
+
// Monitor system logs
|
|
187
|
+
const monitor = await sandbox.startProcess('journalctl -f');
|
|
188
|
+
|
|
189
|
+
const logStream = await sandbox.streamProcessLogs(monitor.id);
|
|
190
|
+
for await (const log of parseSSEStream<LogEvent>(logStream)) {
|
|
191
|
+
if (log.type === 'stdout') {
|
|
192
|
+
// Check for critical errors
|
|
193
|
+
if (log.data.includes('CRITICAL')) {
|
|
194
|
+
await env.ALERTS.send({
|
|
195
|
+
severity: 'critical',
|
|
196
|
+
message: log.data,
|
|
197
|
+
timestamp: log.timestamp
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Store logs
|
|
202
|
+
await env.LOGS.put(`${log.timestamp}-${monitor.id}`, log.data);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
##### Streaming to Frontend via SSE
|
|
210
|
+
```typescript
|
|
211
|
+
// Worker endpoint that streams to frontend
|
|
212
|
+
app.get('/api/build/:id/stream', async (req, env) => {
|
|
213
|
+
const sandbox = getSandbox(env.Sandbox, req.params.id);
|
|
214
|
+
const encoder = new TextEncoder();
|
|
215
|
+
|
|
216
|
+
return new Response(
|
|
217
|
+
new ReadableStream({
|
|
218
|
+
async start(controller) {
|
|
219
|
+
try {
|
|
220
|
+
for await (const event of sandbox.execStream('npm run build')) {
|
|
221
|
+
// Forward events to frontend as SSE
|
|
222
|
+
const sseEvent = `data: ${JSON.stringify(event)}\n\n`;
|
|
223
|
+
controller.enqueue(encoder.encode(sseEvent));
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const errorEvent = `data: ${JSON.stringify({
|
|
227
|
+
type: 'error',
|
|
228
|
+
error: error.message
|
|
229
|
+
})}\n\n`;
|
|
230
|
+
controller.enqueue(encoder.encode(errorEvent));
|
|
231
|
+
} finally {
|
|
232
|
+
controller.close();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}),
|
|
236
|
+
{
|
|
237
|
+
headers: {
|
|
238
|
+
'Content-Type': 'text/event-stream',
|
|
239
|
+
'Cache-Control': 'no-cache'
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Streaming Utilities
|
|
247
|
+
|
|
248
|
+
The SDK exports additional utilities for working with SSE streams:
|
|
249
|
+
|
|
250
|
+
#### `parseSSEStream`
|
|
251
|
+
Converts a `ReadableStream<Uint8Array>` (from SSE endpoints) into a typed `AsyncIterable<T>`. This is the primary utility for consuming streams from the SDK.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { parseSSEStream, type ExecEvent } from '@cloudflare/sandbox';
|
|
255
|
+
|
|
256
|
+
const stream = await sandbox.execStream('npm build');
|
|
257
|
+
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
258
|
+
console.log(event);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `responseToAsyncIterable`
|
|
263
|
+
Converts a `Response` object with SSE content directly to `AsyncIterable<T>`. Useful when fetching from external SSE endpoints.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { responseToAsyncIterable, type LogEvent } from '@cloudflare/sandbox';
|
|
267
|
+
|
|
268
|
+
// Fetch from an external SSE endpoint
|
|
269
|
+
const response = await fetch('https://api.example.com/logs/stream', {
|
|
270
|
+
headers: { 'Accept': 'text/event-stream' }
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Convert Response to typed AsyncIterable
|
|
274
|
+
for await (const event of responseToAsyncIterable<LogEvent>(response)) {
|
|
275
|
+
console.log(`[${event.type}] ${event.data}`);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### `asyncIterableToSSEStream`
|
|
280
|
+
Converts an `AsyncIterable<T>` into an SSE-formatted `ReadableStream<Uint8Array>`. Perfect for Worker endpoints that need to transform or filter events before sending to clients.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { getSandbox, parseSSEStream, asyncIterableToSSEStream, type LogEvent } from '@cloudflare/sandbox';
|
|
284
|
+
|
|
285
|
+
export async function handleFilteredLogs(request: Request, env: Env) {
|
|
286
|
+
const sandbox = getSandbox(env.SANDBOX);
|
|
287
|
+
|
|
288
|
+
// Custom async generator that filters logs
|
|
289
|
+
async function* filterLogs() {
|
|
290
|
+
const stream = await sandbox.streamProcessLogs('web-server');
|
|
291
|
+
|
|
292
|
+
for await (const log of parseSSEStream<LogEvent>(stream)) {
|
|
293
|
+
// Only forward error logs to the client
|
|
294
|
+
if (log.type === 'stderr' || log.data.includes('ERROR')) {
|
|
295
|
+
yield log;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Convert filtered AsyncIterable back to SSE stream for the response
|
|
301
|
+
const sseStream = asyncIterableToSSEStream(filterLogs());
|
|
302
|
+
|
|
303
|
+
return new Response(sseStream, {
|
|
304
|
+
headers: {
|
|
305
|
+
'Content-Type': 'text/event-stream',
|
|
306
|
+
'Cache-Control': 'no-cache',
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Advanced Example - Merging Multiple Streams:**
|
|
313
|
+
```typescript
|
|
314
|
+
async function* mergeBuilds(env: Env) {
|
|
315
|
+
const sandbox1 = getSandbox(env.SANDBOX1);
|
|
316
|
+
const sandbox2 = getSandbox(env.SANDBOX2);
|
|
317
|
+
|
|
318
|
+
// Start builds in parallel
|
|
319
|
+
const [stream1, stream2] = await Promise.all([
|
|
320
|
+
sandbox1.execStream('npm run build:frontend'),
|
|
321
|
+
sandbox2.execStream('npm run build:backend')
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
// Parse and merge events
|
|
325
|
+
const frontend = parseSSEStream<ExecEvent>(stream1);
|
|
326
|
+
const backend = parseSSEStream<ExecEvent>(stream2);
|
|
327
|
+
|
|
328
|
+
// Merge with source identification
|
|
329
|
+
for await (const event of frontend) {
|
|
330
|
+
yield { ...event, source: 'frontend' };
|
|
331
|
+
}
|
|
332
|
+
for await (const event of backend) {
|
|
333
|
+
yield { ...event, source: 'backend' };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Convert merged stream to SSE for client
|
|
338
|
+
const mergedSSE = asyncIterableToSSEStream(mergeBuilds(env));
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Cancellation Support
|
|
342
|
+
|
|
343
|
+
Both streaming methods support cancellation via AbortSignal:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
const controller = new AbortController();
|
|
347
|
+
|
|
348
|
+
// Cancel after 30 seconds
|
|
349
|
+
setTimeout(() => controller.abort(), 30000);
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
for await (const event of sandbox.execStream('long-running-task', {
|
|
353
|
+
signal: controller.signal
|
|
354
|
+
})) {
|
|
355
|
+
// Process events
|
|
356
|
+
if (shouldCancel(event)) {
|
|
357
|
+
controller.abort();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error.message.includes('aborted')) {
|
|
362
|
+
console.log('Operation cancelled');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|