@cloudflare/sandbox 0.0.0-cecde0a → 0.0.0-d55b0f4
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 +26 -0
- package/Dockerfile +4 -5
- package/package.json +5 -4
- package/src/sandbox.ts +1 -0
- package/tsconfig.json +1 -1
- package/README.md +0 -365
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @cloudflare/sandbox
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#30](https://github.com/cloudflare/sandbox-sdk/pull/30) [`30e5c25`](https://github.com/cloudflare/sandbox-sdk/commit/30e5c25cf7d4b07f9049724206c531e2d5d29d5c) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Remove actions timeout
|
|
8
|
+
|
|
9
|
+
- [#29](https://github.com/cloudflare/sandbox-sdk/pull/29) [`d78508f`](https://github.com/cloudflare/sandbox-sdk/commit/d78508f7287a59e0423edd2999c2c83e9e34ccfd) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Create multi-platform Docker image and switch to Cloudflare official repo
|
|
10
|
+
|
|
11
|
+
## 0.1.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [`157dde9`](https://github.com/cloudflare/sandbox-sdk/commit/157dde9b1f23e9bb6f3e9c3f0514b639a8813897) Thanks [@threepointone](https://github.com/threepointone)! - update deps
|
|
16
|
+
|
|
17
|
+
- [`a04f6b6`](https://github.com/cloudflare/sandbox-sdk/commit/a04f6b6c0b2ef9e3ce0851b53769f1c10d8c6de6) Thanks [@threepointone](https://github.com/threepointone)! - trigger a build with updated deps
|
|
18
|
+
|
|
19
|
+
## 0.1.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- [#24](https://github.com/cloudflare/sandbox-sdk/pull/24) [`cecde0a`](https://github.com/cloudflare/sandbox-sdk/commit/cecde0a7530a87deffd8562fb8b01d66ee80ee19) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Redesign command execution API
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#22](https://github.com/cloudflare/sandbox-sdk/pull/22) [`f5fcd52`](https://github.com/cloudflare/sandbox-sdk/commit/f5fcd52025d1f7958a374e69d75e3fc590275f3f) Thanks [@ghostwriternr](https://github.com/ghostwriternr)! - Allow setting env variables dynamically and remove command restrictions
|
|
28
|
+
|
|
3
29
|
## 0.0.9
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/Dockerfile
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Sandbox base image with development tools, Python, Node.js, and Bun
|
|
2
|
+
FROM oven/bun:latest AS bun-source
|
|
2
3
|
FROM ubuntu:22.04
|
|
3
4
|
|
|
4
5
|
# Prevent interactive prompts during package installation
|
|
@@ -50,11 +51,9 @@ RUN apt-get update && apt-get install -y ca-certificates curl gnupg \
|
|
|
50
51
|
&& apt-get install -y nodejs \
|
|
51
52
|
&& rm -rf /var/lib/apt/lists/*
|
|
52
53
|
|
|
53
|
-
# Install Bun
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
&& mv /root/.bun/bin/bunx /usr/local/bin/bunx \
|
|
57
|
-
&& rm -rf /root/.bun
|
|
54
|
+
# Install Bun from official image (avoids architecture compatibility issues)
|
|
55
|
+
COPY --from=bun-source /usr/local/bin/bun /usr/local/bin/bun
|
|
56
|
+
COPY --from=bun-source /usr/local/bin/bunx /usr/local/bin/bunx
|
|
58
57
|
|
|
59
58
|
# Install global npm packages as root
|
|
60
59
|
RUN npm install -g yarn pnpm
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudflare/sandbox",
|
|
3
|
-
"version": "0.0.0-
|
|
3
|
+
"version": "0.0.0-d55b0f4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/cloudflare/sandbox-sdk"
|
|
7
7
|
},
|
|
8
8
|
"description": "A sandboxed environment for running commands",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@cloudflare/containers": "^0.0.
|
|
10
|
+
"@cloudflare/containers": "^0.0.25"
|
|
11
11
|
},
|
|
12
12
|
"tags": [
|
|
13
13
|
"sandbox",
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "rm -rf dist && tsup src/*.ts --outDir dist --dts --sourcemap --format esm",
|
|
21
|
-
"docker:
|
|
22
|
-
"docker:publish": "docker
|
|
21
|
+
"docker:local": "docker build . -t cloudflare/sandbox-test:$npm_package_version",
|
|
22
|
+
"docker:publish": "docker buildx build --platform linux/amd64,linux/arm64 -t cloudflare/sandbox:$npm_package_version --push .",
|
|
23
|
+
"docker:publish:beta": "docker buildx build --platform linux/amd64,linux/arm64 -t cloudflare/sandbox:$npm_package_version-beta --push ."
|
|
23
24
|
},
|
|
24
25
|
"exports": {
|
|
25
26
|
".": {
|
package/src/sandbox.ts
CHANGED
|
@@ -31,6 +31,7 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
34
|
+
defaultPort = 3000; // Default port for the container's Bun server
|
|
34
35
|
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
35
36
|
client: HttpClient;
|
|
36
37
|
private sandboxName: string | null = null;
|
package/tsconfig.json
CHANGED
package/README.md
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
## @cloudflare/sandbox
|
|
2
|
-
|
|
3
|
-
> **⚠️ Experimental** - This library is currently experimental and we're actively seeking feedback. Please try it out and let us know what you think!
|
|
4
|
-
|
|
5
|
-
A library to spin up a sandboxed environment.
|
|
6
|
-
|
|
7
|
-
First, setup your wrangler.json to use the sandbox:
|
|
8
|
-
|
|
9
|
-
```jsonc
|
|
10
|
-
{
|
|
11
|
-
// ...
|
|
12
|
-
"containers": [
|
|
13
|
-
{
|
|
14
|
-
"class_name": "Sandbox",
|
|
15
|
-
"image": "./node_modules/@cloudflare/sandbox/Dockerfile",
|
|
16
|
-
"name": "sandbox"
|
|
17
|
-
}
|
|
18
|
-
],
|
|
19
|
-
"durable_objects": {
|
|
20
|
-
"bindings": [
|
|
21
|
-
{
|
|
22
|
-
"class_name": "Sandbox",
|
|
23
|
-
"name": "Sandbox"
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
},
|
|
27
|
-
"migrations": [
|
|
28
|
-
{
|
|
29
|
-
"new_sqlite_classes": ["Sandbox"],
|
|
30
|
-
"tag": "v1"
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Then, export the Sandbox class in your worker:
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
export { Sandbox } from "@cloudflare/sandbox";
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
You can then use the Sandbox class in your worker:
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { getSandbox } from "@cloudflare/sandbox";
|
|
46
|
-
|
|
47
|
-
export default {
|
|
48
|
-
async fetch(request: Request, env: Env) {
|
|
49
|
-
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
|
|
50
|
-
const result = await sandbox.exec("ls -la");
|
|
51
|
-
return Response.json(result);
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
```
|
|
55
|
-
|
|
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
|
-
```
|