@cloudflare/sandbox 0.4.12 → 0.4.14
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/.turbo/turbo-build.log +13 -47
- package/CHANGELOG.md +38 -16
- package/Dockerfile +15 -9
- package/README.md +0 -1
- package/dist/index.d.ts +1889 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3144 -65
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +31 -26
- package/src/clients/git-client.ts +3 -4
- package/src/clients/index.ts +12 -16
- package/src/clients/interpreter-client.ts +51 -47
- package/src/clients/port-client.ts +10 -10
- package/src/clients/process-client.ts +11 -8
- package/src/clients/sandbox-client.ts +2 -4
- package/src/clients/types.ts +6 -2
- package/src/clients/utility-client.ts +10 -6
- package/src/errors/adapter.ts +90 -32
- package/src/errors/classes.ts +189 -64
- package/src/errors/index.ts +9 -5
- package/src/file-stream.ts +11 -6
- package/src/index.ts +22 -15
- package/src/interpreter.ts +50 -41
- package/src/request-handler.ts +24 -21
- package/src/sandbox.ts +339 -149
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +1 -1
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +309 -197
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +10 -10
- package/tests/git-client.test.ts +188 -101
- package/tests/port-client.test.ts +100 -108
- package/tests/process-client.test.ts +204 -179
- package/tests/request-handler.test.ts +117 -65
- package/tests/sandbox.test.ts +219 -67
- package/tests/sse-parser.test.ts +17 -16
- package/tests/utility-client.test.ts +79 -72
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +6 -6
- package/dist/chunk-BFVUNTP4.js +0 -104
- package/dist/chunk-BFVUNTP4.js.map +0 -1
- package/dist/chunk-EKSWCBCA.js +0 -86
- package/dist/chunk-EKSWCBCA.js.map +0 -1
- package/dist/chunk-JXZMAU2C.js +0 -559
- package/dist/chunk-JXZMAU2C.js.map +0 -1
- package/dist/chunk-UJ3TV4M6.js +0 -7
- package/dist/chunk-UJ3TV4M6.js.map +0 -1
- package/dist/chunk-YE265ASX.js +0 -2484
- package/dist/chunk-YE265ASX.js.map +0 -1
- package/dist/chunk-Z532A7QC.js +0 -78
- package/dist/chunk-Z532A7QC.js.map +0 -1
- package/dist/file-stream.d.ts +0 -43
- package/dist/file-stream.js +0 -9
- package/dist/file-stream.js.map +0 -1
- package/dist/interpreter.d.ts +0 -33
- package/dist/interpreter.js +0 -8
- package/dist/interpreter.js.map +0 -1
- package/dist/request-handler.d.ts +0 -18
- package/dist/request-handler.js +0 -13
- package/dist/request-handler.js.map +0 -1
- package/dist/sandbox-CLZWpfGc.d.ts +0 -613
- package/dist/sandbox.d.ts +0 -4
- package/dist/sandbox.js +0 -13
- package/dist/sandbox.js.map +0 -1
- package/dist/security.d.ts +0 -31
- package/dist/security.js +0 -13
- package/dist/security.js.map +0 -1
- package/dist/sse-parser.d.ts +0 -28
- package/dist/sse-parser.js +0 -11
- package/dist/sse-parser.js.map +0 -1
- package/dist/version.d.ts +0 -8
- package/dist/version.js +0 -7
- package/dist/version.js.map +0 -1
|
@@ -5,12 +5,12 @@ import type {
|
|
|
5
5
|
VersionResponse
|
|
6
6
|
} from '../src/clients';
|
|
7
7
|
import { UtilityClient } from '../src/clients/utility-client';
|
|
8
|
-
import {
|
|
9
|
-
SandboxError
|
|
10
|
-
} from '../src/errors';
|
|
8
|
+
import { SandboxError } from '../src/errors';
|
|
11
9
|
|
|
12
10
|
// Mock data factory for creating test responses
|
|
13
|
-
const mockPingResponse = (
|
|
11
|
+
const mockPingResponse = (
|
|
12
|
+
overrides: Partial<PingResponse> = {}
|
|
13
|
+
): PingResponse => ({
|
|
14
14
|
success: true,
|
|
15
15
|
message: 'pong',
|
|
16
16
|
uptime: 12345,
|
|
@@ -18,7 +18,10 @@ const mockPingResponse = (overrides: Partial<PingResponse> = {}): PingResponse =
|
|
|
18
18
|
...overrides
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
const mockCommandsResponse = (
|
|
21
|
+
const mockCommandsResponse = (
|
|
22
|
+
commands: string[],
|
|
23
|
+
overrides: Partial<CommandsResponse> = {}
|
|
24
|
+
): CommandsResponse => ({
|
|
22
25
|
success: true,
|
|
23
26
|
availableCommands: commands,
|
|
24
27
|
count: commands.length,
|
|
@@ -26,7 +29,10 @@ const mockCommandsResponse = (commands: string[], overrides: Partial<CommandsRes
|
|
|
26
29
|
...overrides
|
|
27
30
|
});
|
|
28
31
|
|
|
29
|
-
const mockVersionResponse = (
|
|
32
|
+
const mockVersionResponse = (
|
|
33
|
+
version: string = '0.4.5',
|
|
34
|
+
overrides: Partial<VersionResponse> = {}
|
|
35
|
+
): VersionResponse => ({
|
|
30
36
|
success: true,
|
|
31
37
|
version,
|
|
32
38
|
timestamp: '2023-01-01T00:00:00Z',
|
|
@@ -45,7 +51,7 @@ describe('UtilityClient', () => {
|
|
|
45
51
|
|
|
46
52
|
client = new UtilityClient({
|
|
47
53
|
baseUrl: 'http://test.com',
|
|
48
|
-
port: 3000
|
|
54
|
+
port: 3000
|
|
49
55
|
});
|
|
50
56
|
});
|
|
51
57
|
|
|
@@ -55,10 +61,9 @@ describe('UtilityClient', () => {
|
|
|
55
61
|
|
|
56
62
|
describe('health checking', () => {
|
|
57
63
|
it('should check sandbox health successfully', async () => {
|
|
58
|
-
mockFetch.mockResolvedValue(
|
|
59
|
-
JSON.stringify(mockPingResponse()),
|
|
60
|
-
|
|
61
|
-
));
|
|
64
|
+
mockFetch.mockResolvedValue(
|
|
65
|
+
new Response(JSON.stringify(mockPingResponse()), { status: 200 })
|
|
66
|
+
);
|
|
62
67
|
|
|
63
68
|
const result = await client.ping();
|
|
64
69
|
|
|
@@ -69,10 +74,11 @@ describe('UtilityClient', () => {
|
|
|
69
74
|
const messages = ['pong', 'alive', 'ok'];
|
|
70
75
|
|
|
71
76
|
for (const message of messages) {
|
|
72
|
-
mockFetch.mockResolvedValueOnce(
|
|
73
|
-
JSON.stringify(mockPingResponse({ message })),
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
mockFetch.mockResolvedValueOnce(
|
|
78
|
+
new Response(JSON.stringify(mockPingResponse({ message })), {
|
|
79
|
+
status: 200
|
|
80
|
+
})
|
|
81
|
+
);
|
|
76
82
|
|
|
77
83
|
const result = await client.ping();
|
|
78
84
|
expect(result).toBe(message);
|
|
@@ -87,11 +93,11 @@ describe('UtilityClient', () => {
|
|
|
87
93
|
const healthChecks = await Promise.all([
|
|
88
94
|
client.ping(),
|
|
89
95
|
client.ping(),
|
|
90
|
-
client.ping()
|
|
96
|
+
client.ping()
|
|
91
97
|
]);
|
|
92
98
|
|
|
93
99
|
expect(healthChecks).toHaveLength(3);
|
|
94
|
-
healthChecks.forEach(result => {
|
|
100
|
+
healthChecks.forEach((result) => {
|
|
95
101
|
expect(result).toBe('pong');
|
|
96
102
|
});
|
|
97
103
|
|
|
@@ -104,10 +110,9 @@ describe('UtilityClient', () => {
|
|
|
104
110
|
code: 'HEALTH_CHECK_FAILED'
|
|
105
111
|
};
|
|
106
112
|
|
|
107
|
-
mockFetch.mockResolvedValue(
|
|
108
|
-
JSON.stringify(errorResponse),
|
|
109
|
-
|
|
110
|
-
));
|
|
113
|
+
mockFetch.mockResolvedValue(
|
|
114
|
+
new Response(JSON.stringify(errorResponse), { status: 503 })
|
|
115
|
+
);
|
|
111
116
|
|
|
112
117
|
await expect(client.ping()).rejects.toThrow();
|
|
113
118
|
});
|
|
@@ -123,10 +128,11 @@ describe('UtilityClient', () => {
|
|
|
123
128
|
it('should discover available system commands', async () => {
|
|
124
129
|
const systemCommands = ['ls', 'cat', 'echo', 'grep', 'find'];
|
|
125
130
|
|
|
126
|
-
mockFetch.mockResolvedValue(
|
|
127
|
-
JSON.stringify(mockCommandsResponse(systemCommands)),
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
mockFetch.mockResolvedValue(
|
|
132
|
+
new Response(JSON.stringify(mockCommandsResponse(systemCommands)), {
|
|
133
|
+
status: 200
|
|
134
|
+
})
|
|
135
|
+
);
|
|
130
136
|
|
|
131
137
|
const result = await client.getCommands();
|
|
132
138
|
|
|
@@ -139,10 +145,11 @@ describe('UtilityClient', () => {
|
|
|
139
145
|
it('should handle minimal command environments', async () => {
|
|
140
146
|
const minimalCommands = ['sh', 'echo', 'cat'];
|
|
141
147
|
|
|
142
|
-
mockFetch.mockResolvedValue(
|
|
143
|
-
JSON.stringify(mockCommandsResponse(minimalCommands)),
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
mockFetch.mockResolvedValue(
|
|
149
|
+
new Response(JSON.stringify(mockCommandsResponse(minimalCommands)), {
|
|
150
|
+
status: 200
|
|
151
|
+
})
|
|
152
|
+
);
|
|
146
153
|
|
|
147
154
|
const result = await client.getCommands();
|
|
148
155
|
|
|
@@ -153,10 +160,11 @@ describe('UtilityClient', () => {
|
|
|
153
160
|
it('should handle large command environments', async () => {
|
|
154
161
|
const richCommands = Array.from({ length: 150 }, (_, i) => `cmd_${i}`);
|
|
155
162
|
|
|
156
|
-
mockFetch.mockResolvedValue(
|
|
157
|
-
JSON.stringify(mockCommandsResponse(richCommands)),
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
mockFetch.mockResolvedValue(
|
|
164
|
+
new Response(JSON.stringify(mockCommandsResponse(richCommands)), {
|
|
165
|
+
status: 200
|
|
166
|
+
})
|
|
167
|
+
);
|
|
160
168
|
|
|
161
169
|
const result = await client.getCommands();
|
|
162
170
|
|
|
@@ -165,10 +173,9 @@ describe('UtilityClient', () => {
|
|
|
165
173
|
});
|
|
166
174
|
|
|
167
175
|
it('should handle empty command environments', async () => {
|
|
168
|
-
mockFetch.mockResolvedValue(
|
|
169
|
-
JSON.stringify(mockCommandsResponse([])),
|
|
170
|
-
|
|
171
|
-
));
|
|
176
|
+
mockFetch.mockResolvedValue(
|
|
177
|
+
new Response(JSON.stringify(mockCommandsResponse([])), { status: 200 })
|
|
178
|
+
);
|
|
172
179
|
|
|
173
180
|
const result = await client.getCommands();
|
|
174
181
|
|
|
@@ -182,10 +189,9 @@ describe('UtilityClient', () => {
|
|
|
182
189
|
code: 'PERMISSION_DENIED'
|
|
183
190
|
};
|
|
184
191
|
|
|
185
|
-
mockFetch.mockResolvedValue(
|
|
186
|
-
JSON.stringify(errorResponse),
|
|
187
|
-
|
|
188
|
-
));
|
|
192
|
+
mockFetch.mockResolvedValue(
|
|
193
|
+
new Response(JSON.stringify(errorResponse), { status: 403 })
|
|
194
|
+
);
|
|
189
195
|
|
|
190
196
|
await expect(client.getCommands()).rejects.toThrow();
|
|
191
197
|
});
|
|
@@ -193,10 +199,9 @@ describe('UtilityClient', () => {
|
|
|
193
199
|
|
|
194
200
|
describe('error handling and resilience', () => {
|
|
195
201
|
it('should handle malformed server responses gracefully', async () => {
|
|
196
|
-
mockFetch.mockResolvedValue(
|
|
197
|
-
'invalid json {',
|
|
198
|
-
|
|
199
|
-
));
|
|
202
|
+
mockFetch.mockResolvedValue(
|
|
203
|
+
new Response('invalid json {', { status: 200 })
|
|
204
|
+
);
|
|
200
205
|
|
|
201
206
|
await expect(client.ping()).rejects.toThrow(SandboxError);
|
|
202
207
|
});
|
|
@@ -210,10 +215,9 @@ describe('UtilityClient', () => {
|
|
|
210
215
|
|
|
211
216
|
it('should handle partial service failures', async () => {
|
|
212
217
|
// First call (ping) succeeds
|
|
213
|
-
mockFetch.mockResolvedValueOnce(
|
|
214
|
-
JSON.stringify(mockPingResponse()),
|
|
215
|
-
|
|
216
|
-
));
|
|
218
|
+
mockFetch.mockResolvedValueOnce(
|
|
219
|
+
new Response(JSON.stringify(mockPingResponse()), { status: 200 })
|
|
220
|
+
);
|
|
217
221
|
|
|
218
222
|
// Second call (getCommands) fails
|
|
219
223
|
const errorResponse = {
|
|
@@ -221,10 +225,9 @@ describe('UtilityClient', () => {
|
|
|
221
225
|
code: 'SERVICE_UNAVAILABLE'
|
|
222
226
|
};
|
|
223
227
|
|
|
224
|
-
mockFetch.mockResolvedValueOnce(
|
|
225
|
-
JSON.stringify(errorResponse),
|
|
226
|
-
|
|
227
|
-
));
|
|
228
|
+
mockFetch.mockResolvedValueOnce(
|
|
229
|
+
new Response(JSON.stringify(errorResponse), { status: 503 })
|
|
230
|
+
);
|
|
228
231
|
|
|
229
232
|
const pingResult = await client.ping();
|
|
230
233
|
expect(pingResult).toBe('pong');
|
|
@@ -239,7 +242,9 @@ describe('UtilityClient', () => {
|
|
|
239
242
|
if (callCount % 2 === 0) {
|
|
240
243
|
return Promise.reject(new Error('Intermittent failure'));
|
|
241
244
|
} else {
|
|
242
|
-
return Promise.resolve(
|
|
245
|
+
return Promise.resolve(
|
|
246
|
+
new Response(JSON.stringify(mockPingResponse()))
|
|
247
|
+
);
|
|
243
248
|
}
|
|
244
249
|
});
|
|
245
250
|
|
|
@@ -247,7 +252,7 @@ describe('UtilityClient', () => {
|
|
|
247
252
|
client.ping(), // Should succeed (call 1)
|
|
248
253
|
client.ping(), // Should fail (call 2)
|
|
249
254
|
client.ping(), // Should succeed (call 3)
|
|
250
|
-
client.ping()
|
|
255
|
+
client.ping() // Should fail (call 4)
|
|
251
256
|
]);
|
|
252
257
|
|
|
253
258
|
expect(results[0].status).toBe('fulfilled');
|
|
@@ -259,10 +264,11 @@ describe('UtilityClient', () => {
|
|
|
259
264
|
|
|
260
265
|
describe('version checking', () => {
|
|
261
266
|
it('should get container version successfully', async () => {
|
|
262
|
-
mockFetch.mockResolvedValue(
|
|
263
|
-
JSON.stringify(mockVersionResponse('0.4.5')),
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
mockFetch.mockResolvedValue(
|
|
268
|
+
new Response(JSON.stringify(mockVersionResponse('0.4.5')), {
|
|
269
|
+
status: 200
|
|
270
|
+
})
|
|
271
|
+
);
|
|
266
272
|
|
|
267
273
|
const result = await client.getVersion();
|
|
268
274
|
|
|
@@ -273,10 +279,11 @@ describe('UtilityClient', () => {
|
|
|
273
279
|
const versions = ['1.0.0', '2.5.3-beta', '0.0.1', '10.20.30'];
|
|
274
280
|
|
|
275
281
|
for (const version of versions) {
|
|
276
|
-
mockFetch.mockResolvedValueOnce(
|
|
277
|
-
JSON.stringify(mockVersionResponse(version)),
|
|
278
|
-
|
|
279
|
-
|
|
282
|
+
mockFetch.mockResolvedValueOnce(
|
|
283
|
+
new Response(JSON.stringify(mockVersionResponse(version)), {
|
|
284
|
+
status: 200
|
|
285
|
+
})
|
|
286
|
+
);
|
|
280
287
|
|
|
281
288
|
const result = await client.getVersion();
|
|
282
289
|
expect(result).toBe(version);
|
|
@@ -285,10 +292,9 @@ describe('UtilityClient', () => {
|
|
|
285
292
|
|
|
286
293
|
it('should return "unknown" when version endpoint does not exist (backward compatibility)', async () => {
|
|
287
294
|
// Simulate 404 or other error for old containers
|
|
288
|
-
mockFetch.mockResolvedValue(
|
|
289
|
-
JSON.stringify({ error: 'Not Found' }),
|
|
290
|
-
|
|
291
|
-
));
|
|
295
|
+
mockFetch.mockResolvedValue(
|
|
296
|
+
new Response(JSON.stringify({ error: 'Not Found' }), { status: 404 })
|
|
297
|
+
);
|
|
292
298
|
|
|
293
299
|
const result = await client.getVersion();
|
|
294
300
|
|
|
@@ -304,10 +310,11 @@ describe('UtilityClient', () => {
|
|
|
304
310
|
});
|
|
305
311
|
|
|
306
312
|
it('should handle version response with unknown value', async () => {
|
|
307
|
-
mockFetch.mockResolvedValue(
|
|
308
|
-
JSON.stringify(mockVersionResponse('unknown')),
|
|
309
|
-
|
|
310
|
-
|
|
313
|
+
mockFetch.mockResolvedValue(
|
|
314
|
+
new Response(JSON.stringify(mockVersionResponse('unknown')), {
|
|
315
|
+
status: 200
|
|
316
|
+
})
|
|
317
|
+
);
|
|
311
318
|
|
|
312
319
|
const result = await client.getVersion();
|
|
313
320
|
|
|
@@ -324,7 +331,7 @@ describe('UtilityClient', () => {
|
|
|
324
331
|
it('should initialize with full options', () => {
|
|
325
332
|
const fullOptionsClient = new UtilityClient({
|
|
326
333
|
baseUrl: 'http://custom.com',
|
|
327
|
-
port: 8080
|
|
334
|
+
port: 8080
|
|
328
335
|
});
|
|
329
336
|
expect(fullOptionsClient).toBeInstanceOf(UtilityClient);
|
|
330
337
|
});
|
package/tsdown.config.ts
ADDED
package/vitest.config.ts
CHANGED
|
@@ -18,14 +18,14 @@ export default defineWorkersConfig({
|
|
|
18
18
|
poolOptions: {
|
|
19
19
|
workers: {
|
|
20
20
|
wrangler: {
|
|
21
|
-
configPath: './tests/wrangler.jsonc'
|
|
21
|
+
configPath: './tests/wrangler.jsonc'
|
|
22
22
|
},
|
|
23
23
|
singleWorker: true,
|
|
24
|
-
isolatedStorage: false
|
|
25
|
-
}
|
|
26
|
-
}
|
|
24
|
+
isolatedStorage: false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
27
|
},
|
|
28
28
|
esbuild: {
|
|
29
|
-
target: 'esnext'
|
|
30
|
-
}
|
|
29
|
+
target: 'esnext'
|
|
30
|
+
}
|
|
31
31
|
});
|
package/dist/chunk-BFVUNTP4.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// src/file-stream.ts
|
|
2
|
-
async function* parseSSE(stream) {
|
|
3
|
-
const reader = stream.getReader();
|
|
4
|
-
const decoder = new TextDecoder();
|
|
5
|
-
let buffer = "";
|
|
6
|
-
try {
|
|
7
|
-
while (true) {
|
|
8
|
-
const { done, value } = await reader.read();
|
|
9
|
-
if (done) {
|
|
10
|
-
break;
|
|
11
|
-
}
|
|
12
|
-
buffer += decoder.decode(value, { stream: true });
|
|
13
|
-
const lines = buffer.split("\n");
|
|
14
|
-
buffer = lines.pop() || "";
|
|
15
|
-
for (const line of lines) {
|
|
16
|
-
if (line.startsWith("data: ")) {
|
|
17
|
-
const data = line.slice(6);
|
|
18
|
-
try {
|
|
19
|
-
const event = JSON.parse(data);
|
|
20
|
-
yield event;
|
|
21
|
-
} catch {
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
} finally {
|
|
27
|
-
reader.releaseLock();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async function* streamFile(stream) {
|
|
31
|
-
let metadata = null;
|
|
32
|
-
for await (const event of parseSSE(stream)) {
|
|
33
|
-
switch (event.type) {
|
|
34
|
-
case "metadata":
|
|
35
|
-
metadata = {
|
|
36
|
-
mimeType: event.mimeType,
|
|
37
|
-
size: event.size,
|
|
38
|
-
isBinary: event.isBinary,
|
|
39
|
-
encoding: event.encoding
|
|
40
|
-
};
|
|
41
|
-
break;
|
|
42
|
-
case "chunk":
|
|
43
|
-
if (!metadata) {
|
|
44
|
-
throw new Error("Received chunk before metadata");
|
|
45
|
-
}
|
|
46
|
-
if (metadata.isBinary && metadata.encoding === "base64") {
|
|
47
|
-
const binaryString = atob(event.data);
|
|
48
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
49
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
50
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
51
|
-
}
|
|
52
|
-
yield bytes;
|
|
53
|
-
} else {
|
|
54
|
-
yield event.data;
|
|
55
|
-
}
|
|
56
|
-
break;
|
|
57
|
-
case "complete":
|
|
58
|
-
if (!metadata) {
|
|
59
|
-
throw new Error("Stream completed without metadata");
|
|
60
|
-
}
|
|
61
|
-
return metadata;
|
|
62
|
-
case "error":
|
|
63
|
-
throw new Error(`File streaming error: ${event.error}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
throw new Error("Stream ended unexpectedly");
|
|
67
|
-
}
|
|
68
|
-
async function collectFile(stream) {
|
|
69
|
-
const chunks = [];
|
|
70
|
-
const generator = streamFile(stream);
|
|
71
|
-
let result = await generator.next();
|
|
72
|
-
while (!result.done) {
|
|
73
|
-
chunks.push(result.value);
|
|
74
|
-
result = await generator.next();
|
|
75
|
-
}
|
|
76
|
-
const metadata = result.value;
|
|
77
|
-
if (!metadata) {
|
|
78
|
-
throw new Error("Failed to get file metadata");
|
|
79
|
-
}
|
|
80
|
-
if (metadata.isBinary) {
|
|
81
|
-
const totalLength = chunks.reduce(
|
|
82
|
-
(sum, chunk) => sum + (chunk instanceof Uint8Array ? chunk.length : 0),
|
|
83
|
-
0
|
|
84
|
-
);
|
|
85
|
-
const combined = new Uint8Array(totalLength);
|
|
86
|
-
let offset = 0;
|
|
87
|
-
for (const chunk of chunks) {
|
|
88
|
-
if (chunk instanceof Uint8Array) {
|
|
89
|
-
combined.set(chunk, offset);
|
|
90
|
-
offset += chunk.length;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { content: combined, metadata };
|
|
94
|
-
} else {
|
|
95
|
-
const combined = chunks.filter((c) => typeof c === "string").join("");
|
|
96
|
-
return { content: combined, metadata };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export {
|
|
101
|
-
streamFile,
|
|
102
|
-
collectFile
|
|
103
|
-
};
|
|
104
|
-
//# sourceMappingURL=chunk-BFVUNTP4.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/file-stream.ts"],"sourcesContent":["import type { FileChunk, FileMetadata, FileStreamEvent } from '@repo/shared';\n\n/**\n * Parse SSE (Server-Sent Events) lines from a stream\n */\nasync function* parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<FileStreamEvent> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6); // Remove 'data: ' prefix\n try {\n const event = JSON.parse(data) as FileStreamEvent;\n yield event;\n } catch {\n // Skip invalid JSON events and continue processing\n }\n }\n }\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Stream a file from the sandbox with automatic base64 decoding for binary files\n *\n * @param stream - The ReadableStream from readFileStream()\n * @returns AsyncGenerator that yields FileChunk (string for text, Uint8Array for binary)\n *\n * @example\n * ```ts\n * const stream = await sandbox.readFileStream('/path/to/file.png');\n * for await (const chunk of streamFile(stream)) {\n * if (chunk instanceof Uint8Array) {\n * // Binary chunk\n * console.log('Binary chunk:', chunk.length, 'bytes');\n * } else {\n * // Text chunk\n * console.log('Text chunk:', chunk);\n * }\n * }\n * ```\n */\nexport async function* streamFile(stream: ReadableStream<Uint8Array>): AsyncGenerator<FileChunk, FileMetadata> {\n let metadata: FileMetadata | null = null;\n\n for await (const event of parseSSE(stream)) {\n switch (event.type) {\n case 'metadata':\n metadata = {\n mimeType: event.mimeType,\n size: event.size,\n isBinary: event.isBinary,\n encoding: event.encoding,\n };\n break;\n\n case 'chunk':\n if (!metadata) {\n throw new Error('Received chunk before metadata');\n }\n\n if (metadata.isBinary && metadata.encoding === 'base64') {\n // Decode base64 to Uint8Array for binary files\n const binaryString = atob(event.data);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n yield bytes;\n } else {\n // Text files - yield as-is\n yield event.data;\n }\n break;\n\n case 'complete':\n if (!metadata) {\n throw new Error('Stream completed without metadata');\n }\n return metadata;\n\n case 'error':\n throw new Error(`File streaming error: ${event.error}`);\n }\n }\n\n throw new Error('Stream ended unexpectedly');\n}\n\n/**\n * Collect an entire file into memory from a stream\n *\n * @param stream - The ReadableStream from readFileStream()\n * @returns Object containing the file content and metadata\n *\n * @example\n * ```ts\n * const stream = await sandbox.readFileStream('/path/to/file.txt');\n * const { content, metadata } = await collectFile(stream);\n * console.log('Content:', content);\n * console.log('MIME type:', metadata.mimeType);\n * ```\n */\nexport async function collectFile(stream: ReadableStream<Uint8Array>): Promise<{\n content: string | Uint8Array;\n metadata: FileMetadata;\n}> {\n const chunks: Array<string | Uint8Array> = [];\n\n // Iterate through the generator and get the return value (metadata)\n const generator = streamFile(stream);\n let result = await generator.next();\n\n while (!result.done) {\n chunks.push(result.value);\n result = await generator.next();\n }\n\n const metadata = result.value;\n\n if (!metadata) {\n throw new Error('Failed to get file metadata');\n }\n\n // Combine chunks based on type\n if (metadata.isBinary) {\n // Binary file - combine Uint8Arrays\n const totalLength = chunks.reduce((sum, chunk) =>\n sum + (chunk instanceof Uint8Array ? chunk.length : 0), 0\n );\n const combined = new Uint8Array(totalLength);\n let offset = 0;\n for (const chunk of chunks) {\n if (chunk instanceof Uint8Array) {\n combined.set(chunk, offset);\n offset += chunk.length;\n }\n }\n return { content: combined, metadata };\n } else {\n // Text file - combine strings\n const combined = chunks.filter(c => typeof c === 'string').join('');\n return { content: combined, metadata };\n }\n}\n"],"mappings":";AAKA,gBAAgB,SAAS,QAAqE;AAC5F,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AAEb,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAE1C,UAAI,MAAM;AACR;AAAA,MACF;AAEA,gBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAChD,YAAM,QAAQ,OAAO,MAAM,IAAI;AAG/B,eAAS,MAAM,IAAI,KAAK;AAExB,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,WAAW,QAAQ,GAAG;AAC7B,gBAAM,OAAO,KAAK,MAAM,CAAC;AACzB,cAAI;AACF,kBAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,kBAAM;AAAA,UACR,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AACF;AAsBA,gBAAuB,WAAW,QAA6E;AAC7G,MAAI,WAAgC;AAEpC,mBAAiB,SAAS,SAAS,MAAM,GAAG;AAC1C,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK;AACH,mBAAW;AAAA,UACT,UAAU,MAAM;AAAA,UAChB,MAAM,MAAM;AAAA,UACZ,UAAU,MAAM;AAAA,UAChB,UAAU,MAAM;AAAA,QAClB;AACA;AAAA,MAEF,KAAK;AACH,YAAI,CAAC,UAAU;AACb,gBAAM,IAAI,MAAM,gCAAgC;AAAA,QAClD;AAEA,YAAI,SAAS,YAAY,SAAS,aAAa,UAAU;AAEvD,gBAAM,eAAe,KAAK,MAAM,IAAI;AACpC,gBAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;AAChD,mBAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,kBAAM,CAAC,IAAI,aAAa,WAAW,CAAC;AAAA,UACtC;AACA,gBAAM;AAAA,QACR,OAAO;AAEL,gBAAM,MAAM;AAAA,QACd;AACA;AAAA,MAEF,KAAK;AACH,YAAI,CAAC,UAAU;AACb,gBAAM,IAAI,MAAM,mCAAmC;AAAA,QACrD;AACA,eAAO;AAAA,MAET,KAAK;AACH,cAAM,IAAI,MAAM,yBAAyB,MAAM,KAAK,EAAE;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,IAAI,MAAM,2BAA2B;AAC7C;AAgBA,eAAsB,YAAY,QAG/B;AACD,QAAM,SAAqC,CAAC;AAG5C,QAAM,YAAY,WAAW,MAAM;AACnC,MAAI,SAAS,MAAM,UAAU,KAAK;AAElC,SAAO,CAAC,OAAO,MAAM;AACnB,WAAO,KAAK,OAAO,KAAK;AACxB,aAAS,MAAM,UAAU,KAAK;AAAA,EAChC;AAEA,QAAM,WAAW,OAAO;AAExB,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAGA,MAAI,SAAS,UAAU;AAErB,UAAM,cAAc,OAAO;AAAA,MAAO,CAAC,KAAK,UACtC,OAAO,iBAAiB,aAAa,MAAM,SAAS;AAAA,MAAI;AAAA,IAC1D;AACA,UAAM,WAAW,IAAI,WAAW,WAAW;AAC3C,QAAI,SAAS;AACb,eAAW,SAAS,QAAQ;AAC1B,UAAI,iBAAiB,YAAY;AAC/B,iBAAS,IAAI,OAAO,MAAM;AAC1B,kBAAU,MAAM;AAAA,MAClB;AAAA,IACF;AACA,WAAO,EAAE,SAAS,UAAU,SAAS;AAAA,EACvC,OAAO;AAEL,UAAM,WAAW,OAAO,OAAO,OAAK,OAAO,MAAM,QAAQ,EAAE,KAAK,EAAE;AAClE,WAAO,EAAE,SAAS,UAAU,SAAS;AAAA,EACvC;AACF;","names":[]}
|
package/dist/chunk-EKSWCBCA.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// src/sse-parser.ts
|
|
2
|
-
async function* parseSSEStream(stream, signal) {
|
|
3
|
-
const reader = stream.getReader();
|
|
4
|
-
const decoder = new TextDecoder();
|
|
5
|
-
let buffer = "";
|
|
6
|
-
try {
|
|
7
|
-
while (true) {
|
|
8
|
-
if (signal?.aborted) {
|
|
9
|
-
throw new Error("Operation was aborted");
|
|
10
|
-
}
|
|
11
|
-
const { done, value } = await reader.read();
|
|
12
|
-
if (done) break;
|
|
13
|
-
buffer += decoder.decode(value, { stream: true });
|
|
14
|
-
const lines = buffer.split("\n");
|
|
15
|
-
buffer = lines.pop() || "";
|
|
16
|
-
for (const line of lines) {
|
|
17
|
-
if (line.trim() === "") continue;
|
|
18
|
-
if (line.startsWith("data: ")) {
|
|
19
|
-
const data = line.substring(6);
|
|
20
|
-
if (data === "[DONE]" || data.trim() === "") continue;
|
|
21
|
-
try {
|
|
22
|
-
const event = JSON.parse(data);
|
|
23
|
-
yield event;
|
|
24
|
-
} catch {
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (buffer.trim() && buffer.startsWith("data: ")) {
|
|
30
|
-
const data = buffer.substring(6);
|
|
31
|
-
if (data !== "[DONE]" && data.trim()) {
|
|
32
|
-
try {
|
|
33
|
-
const event = JSON.parse(data);
|
|
34
|
-
yield event;
|
|
35
|
-
} catch {
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
} finally {
|
|
40
|
-
reader.releaseLock();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function* responseToAsyncIterable(response, signal) {
|
|
44
|
-
if (!response.ok) {
|
|
45
|
-
throw new Error(`Response not ok: ${response.status} ${response.statusText}`);
|
|
46
|
-
}
|
|
47
|
-
if (!response.body) {
|
|
48
|
-
throw new Error("No response body");
|
|
49
|
-
}
|
|
50
|
-
yield* parseSSEStream(response.body, signal);
|
|
51
|
-
}
|
|
52
|
-
function asyncIterableToSSEStream(events, options) {
|
|
53
|
-
const encoder = new TextEncoder();
|
|
54
|
-
const serialize = options?.serialize || JSON.stringify;
|
|
55
|
-
return new ReadableStream({
|
|
56
|
-
async start(controller) {
|
|
57
|
-
try {
|
|
58
|
-
for await (const event of events) {
|
|
59
|
-
if (options?.signal?.aborted) {
|
|
60
|
-
controller.error(new Error("Operation was aborted"));
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
const data = serialize(event);
|
|
64
|
-
const sseEvent = `data: ${data}
|
|
65
|
-
|
|
66
|
-
`;
|
|
67
|
-
controller.enqueue(encoder.encode(sseEvent));
|
|
68
|
-
}
|
|
69
|
-
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
70
|
-
} catch (error) {
|
|
71
|
-
controller.error(error);
|
|
72
|
-
} finally {
|
|
73
|
-
controller.close();
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
cancel() {
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export {
|
|
82
|
-
parseSSEStream,
|
|
83
|
-
responseToAsyncIterable,
|
|
84
|
-
asyncIterableToSSEStream
|
|
85
|
-
};
|
|
86
|
-
//# sourceMappingURL=chunk-EKSWCBCA.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sse-parser.ts"],"sourcesContent":["/**\n * Server-Sent Events (SSE) parser for streaming responses\n * Converts ReadableStream<Uint8Array> to typed AsyncIterable<T>\n */\n\n/**\n * Parse a ReadableStream of SSE events into typed AsyncIterable\n * @param stream - The ReadableStream from fetch response\n * @param signal - Optional AbortSignal for cancellation\n */\nexport async function* parseSSEStream<T>(\n stream: ReadableStream<Uint8Array>,\n signal?: AbortSignal\n): AsyncIterable<T> {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n // Check for cancellation\n if (signal?.aborted) {\n throw new Error('Operation was aborted');\n }\n\n const { done, value } = await reader.read();\n if (done) break;\n\n // Decode chunk and add to buffer\n buffer += decoder.decode(value, { stream: true });\n\n // Process complete SSE events in buffer\n const lines = buffer.split('\\n');\n\n // Keep the last incomplete line in buffer\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n // Skip empty lines\n if (line.trim() === '') continue;\n\n // Process SSE data lines\n if (line.startsWith('data: ')) {\n const data = line.substring(6);\n\n // Skip [DONE] markers or empty data\n if (data === '[DONE]' || data.trim() === '') continue;\n\n try {\n const event = JSON.parse(data) as T;\n yield event;\n } catch {\n // Skip invalid JSON events and continue processing\n }\n }\n // Handle other SSE fields if needed (event:, id:, retry:)\n // For now, we only care about data: lines\n }\n }\n\n // Process any remaining data in buffer\n if (buffer.trim() && buffer.startsWith('data: ')) {\n const data = buffer.substring(6);\n if (data !== '[DONE]' && data.trim()) {\n try {\n const event = JSON.parse(data) as T;\n yield event;\n } catch {\n // Skip invalid JSON in final event\n }\n }\n }\n } finally {\n // Clean up resources\n reader.releaseLock();\n }\n}\n\n\n/**\n * Helper to convert a Response with SSE stream directly to AsyncIterable\n * @param response - Response object with SSE stream\n * @param signal - Optional AbortSignal for cancellation\n */\nexport async function* responseToAsyncIterable<T>(\n response: Response,\n signal?: AbortSignal\n): AsyncIterable<T> {\n if (!response.ok) {\n throw new Error(`Response not ok: ${response.status} ${response.statusText}`);\n }\n\n if (!response.body) {\n throw new Error('No response body');\n }\n\n yield* parseSSEStream<T>(response.body, signal);\n}\n\n/**\n * Create an SSE-formatted ReadableStream from an AsyncIterable\n * (Useful for Worker endpoints that need to forward AsyncIterable as SSE)\n * @param events - AsyncIterable of events\n * @param options - Stream options\n */\nexport function asyncIterableToSSEStream<T>(\n events: AsyncIterable<T>,\n options?: {\n signal?: AbortSignal;\n serialize?: (event: T) => string;\n }\n): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n const serialize = options?.serialize || JSON.stringify;\n\n return new ReadableStream({\n async start(controller) {\n try {\n for await (const event of events) {\n if (options?.signal?.aborted) {\n controller.error(new Error('Operation was aborted'));\n break;\n }\n\n const data = serialize(event);\n const sseEvent = `data: ${data}\\n\\n`;\n controller.enqueue(encoder.encode(sseEvent));\n }\n\n // Send completion marker\n controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));\n } catch (error) {\n controller.error(error);\n } finally {\n controller.close();\n }\n },\n\n cancel() {\n // Handle stream cancellation\n }\n });\n}"],"mappings":";AAUA,gBAAuB,eACrB,QACA,QACkB;AAClB,QAAM,SAAS,OAAO,UAAU;AAChC,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AAEb,MAAI;AACF,WAAO,MAAM;AAEX,UAAI,QAAQ,SAAS;AACnB,cAAM,IAAI,MAAM,uBAAuB;AAAA,MACzC;AAEA,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AAGV,gBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAGhD,YAAM,QAAQ,OAAO,MAAM,IAAI;AAG/B,eAAS,MAAM,IAAI,KAAK;AAExB,iBAAW,QAAQ,OAAO;AAExB,YAAI,KAAK,KAAK,MAAM,GAAI;AAGxB,YAAI,KAAK,WAAW,QAAQ,GAAG;AAC7B,gBAAM,OAAO,KAAK,UAAU,CAAC;AAG7B,cAAI,SAAS,YAAY,KAAK,KAAK,MAAM,GAAI;AAE7C,cAAI;AACF,kBAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,kBAAM;AAAA,UACR,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MAGF;AAAA,IACF;AAGA,QAAI,OAAO,KAAK,KAAK,OAAO,WAAW,QAAQ,GAAG;AAChD,YAAM,OAAO,OAAO,UAAU,CAAC;AAC/B,UAAI,SAAS,YAAY,KAAK,KAAK,GAAG;AACpC,YAAI;AACF,gBAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,gBAAM;AAAA,QACR,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF,UAAE;AAEA,WAAO,YAAY;AAAA,EACrB;AACF;AAQA,gBAAuB,wBACrB,UACA,QACkB;AAClB,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,oBAAoB,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAAA,EAC9E;AAEA,MAAI,CAAC,SAAS,MAAM;AAClB,UAAM,IAAI,MAAM,kBAAkB;AAAA,EACpC;AAEA,SAAO,eAAkB,SAAS,MAAM,MAAM;AAChD;AAQO,SAAS,yBACd,QACA,SAI4B;AAC5B,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,YAAY,SAAS,aAAa,KAAK;AAE7C,SAAO,IAAI,eAAe;AAAA,IACxB,MAAM,MAAM,YAAY;AACtB,UAAI;AACF,yBAAiB,SAAS,QAAQ;AAChC,cAAI,SAAS,QAAQ,SAAS;AAC5B,uBAAW,MAAM,IAAI,MAAM,uBAAuB,CAAC;AACnD;AAAA,UACF;AAEA,gBAAM,OAAO,UAAU,KAAK;AAC5B,gBAAM,WAAW,SAAS,IAAI;AAAA;AAAA;AAC9B,qBAAW,QAAQ,QAAQ,OAAO,QAAQ,CAAC;AAAA,QAC7C;AAGA,mBAAW,QAAQ,QAAQ,OAAO,kBAAkB,CAAC;AAAA,MACvD,SAAS,OAAO;AACd,mBAAW,MAAM,KAAK;AAAA,MACxB,UAAE;AACA,mBAAW,MAAM;AAAA,MACnB;AAAA,IACF;AAAA,IAEA,SAAS;AAAA,IAET;AAAA,EACF,CAAC;AACH;","names":[]}
|