@cloudflare/sandbox 0.2.1 → 0.2.2

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.
@@ -0,0 +1,448 @@
1
+ import { existsSync } from "node:fs";
2
+ import { CircuitBreaker } from "./circuit-breaker";
3
+ import { type CreateContextRequest, JupyterServer } from "./jupyter-server";
4
+
5
+ interface PoolStats {
6
+ available: number;
7
+ inUse: number;
8
+ total: number;
9
+ minSize: number;
10
+ maxSize: number;
11
+ warming: boolean;
12
+ }
13
+
14
+ interface CircuitBreakerState {
15
+ state: string;
16
+ failures: number;
17
+ lastFailure: number;
18
+ isOpen: boolean;
19
+ }
20
+
21
+ export interface JupyterHealthStatus {
22
+ ready: boolean;
23
+ initializing: boolean;
24
+ error?: string;
25
+ checks?: {
26
+ httpApi: boolean;
27
+ kernelManager: boolean;
28
+ markerFile: boolean;
29
+ kernelSpawn?: boolean;
30
+ websocket?: boolean;
31
+ };
32
+ timestamp: number;
33
+ performance?: {
34
+ poolStats?: Record<string, PoolStats>;
35
+ activeContexts?: number;
36
+ uptime?: number;
37
+ circuitBreakers?: {
38
+ contextCreation: CircuitBreakerState;
39
+ codeExecution: CircuitBreakerState;
40
+ kernelCommunication: CircuitBreakerState;
41
+ };
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Wrapper service that provides graceful degradation for Jupyter functionality
47
+ */
48
+ export class JupyterService {
49
+ private jupyterServer: JupyterServer;
50
+ private initPromise: Promise<void> | null = null;
51
+ private initialized = false;
52
+ private initError: Error | null = null;
53
+ private startTime = Date.now();
54
+ private circuitBreakers: {
55
+ contextCreation: CircuitBreaker;
56
+ codeExecution: CircuitBreaker;
57
+ kernelCommunication: CircuitBreaker;
58
+ };
59
+
60
+ constructor() {
61
+ this.jupyterServer = new JupyterServer();
62
+
63
+ // Initialize circuit breakers for different operations
64
+ this.circuitBreakers = {
65
+ contextCreation: new CircuitBreaker({
66
+ name: "context-creation",
67
+ threshold: 3,
68
+ timeout: 60000, // 1 minute
69
+ }),
70
+ codeExecution: new CircuitBreaker({
71
+ name: "code-execution",
72
+ threshold: 5,
73
+ timeout: 30000, // 30 seconds
74
+ }),
75
+ kernelCommunication: new CircuitBreaker({
76
+ name: "kernel-communication",
77
+ threshold: 10,
78
+ timeout: 20000, // 20 seconds
79
+ }),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Initialize Jupyter server with retry logic
85
+ */
86
+ async initialize(): Promise<void> {
87
+ if (this.initialized) return;
88
+
89
+ if (!this.initPromise) {
90
+ this.initPromise = this.doInitialize()
91
+ .then(() => {
92
+ this.initialized = true;
93
+ console.log("[JupyterService] Initialization complete");
94
+ })
95
+ .catch((err) => {
96
+ this.initError = err;
97
+ // Don't null out initPromise on error - keep it so we can return the same error
98
+ console.error("[JupyterService] Initialization failed:", err);
99
+ throw err;
100
+ });
101
+ }
102
+
103
+ return this.initPromise;
104
+ }
105
+
106
+ private async doInitialize(): Promise<void> {
107
+ // Wait for Jupyter marker file or timeout
108
+ const markerCheckPromise = this.waitForMarkerFile();
109
+ const timeoutPromise = new Promise<void>((_, reject) => {
110
+ setTimeout(
111
+ () => reject(new Error("Jupyter initialization timeout")),
112
+ 60000
113
+ );
114
+ });
115
+
116
+ try {
117
+ await Promise.race([markerCheckPromise, timeoutPromise]);
118
+ console.log("[JupyterService] Jupyter process detected via marker file");
119
+ } catch (error) {
120
+ console.log(
121
+ "[JupyterService] Marker file not found yet - proceeding with initialization"
122
+ );
123
+ }
124
+
125
+ // Initialize Jupyter server
126
+ await this.jupyterServer.initialize();
127
+
128
+ // Pre-warm context pools in background after Jupyter is ready
129
+ this.warmContextPools();
130
+ }
131
+
132
+ /**
133
+ * Pre-warm context pools for better performance
134
+ */
135
+ private async warmContextPools() {
136
+ try {
137
+ console.log(
138
+ "[JupyterService] Pre-warming context pools for better performance"
139
+ );
140
+
141
+ // Enable pool warming with min sizes
142
+ await this.jupyterServer.enablePoolWarming("python", 1);
143
+ await this.jupyterServer.enablePoolWarming("javascript", 1);
144
+ } catch (error) {
145
+ console.error("[JupyterService] Error pre-warming context pools:", error);
146
+ }
147
+ }
148
+
149
+ private async waitForMarkerFile(): Promise<void> {
150
+ const markerPath = "/tmp/jupyter-ready";
151
+ let attempts = 0;
152
+ const maxAttempts = 120; // 2 minutes with 1s intervals
153
+
154
+ while (attempts < maxAttempts) {
155
+ if (existsSync(markerPath)) {
156
+ return;
157
+ }
158
+ attempts++;
159
+ await new Promise((resolve) => setTimeout(resolve, 1000));
160
+ }
161
+
162
+ throw new Error("Marker file not found within timeout");
163
+ }
164
+
165
+ /**
166
+ * Get current health status
167
+ */
168
+ async getHealthStatus(): Promise<JupyterHealthStatus> {
169
+ const status: JupyterHealthStatus = {
170
+ ready: this.initialized,
171
+ initializing: this.initPromise !== null && !this.initialized,
172
+ timestamp: Date.now(),
173
+ };
174
+
175
+ if (this.initError) {
176
+ status.error = this.initError.message;
177
+ }
178
+
179
+ // Detailed health checks
180
+ if (this.initialized || this.initPromise) {
181
+ status.checks = {
182
+ httpApi: await this.checkHttpApi(),
183
+ kernelManager: this.initialized,
184
+ markerFile: existsSync("/tmp/jupyter-ready"),
185
+ };
186
+
187
+ // Advanced health checks only if fully initialized
188
+ if (this.initialized) {
189
+ const advancedChecks = await this.performAdvancedHealthChecks();
190
+ status.checks.kernelSpawn = advancedChecks.kernelSpawn;
191
+ status.checks.websocket = advancedChecks.websocket;
192
+
193
+ // Performance metrics
194
+ status.performance = {
195
+ poolStats: await this.jupyterServer.getPoolStats(),
196
+ activeContexts: (await this.jupyterServer.listContexts()).length,
197
+ uptime: Date.now() - this.startTime,
198
+ };
199
+ }
200
+ }
201
+
202
+ // Add circuit breaker status
203
+ if (status.performance) {
204
+ status.performance.circuitBreakers = {
205
+ contextCreation: this.circuitBreakers.contextCreation.getState(),
206
+ codeExecution: this.circuitBreakers.codeExecution.getState(),
207
+ kernelCommunication:
208
+ this.circuitBreakers.kernelCommunication.getState(),
209
+ };
210
+ }
211
+
212
+ return status;
213
+ }
214
+
215
+ private async checkHttpApi(): Promise<boolean> {
216
+ try {
217
+ const response = await fetch("http://localhost:8888/api", {
218
+ signal: AbortSignal.timeout(2000),
219
+ });
220
+ return response.ok;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Perform advanced health checks including kernel spawn and WebSocket
228
+ */
229
+ private async performAdvancedHealthChecks(): Promise<{
230
+ kernelSpawn: boolean;
231
+ websocket: boolean;
232
+ }> {
233
+ const checks = {
234
+ kernelSpawn: false,
235
+ websocket: false,
236
+ };
237
+
238
+ try {
239
+ // Test kernel spawn with timeout
240
+ const testContext = await Promise.race([
241
+ this.jupyterServer.createContext({ language: "python" }),
242
+ new Promise<null>((_, reject) =>
243
+ setTimeout(() => reject(new Error("Kernel spawn timeout")), 5000)
244
+ ),
245
+ ]);
246
+
247
+ if (testContext) {
248
+ checks.kernelSpawn = true;
249
+
250
+ // Test WebSocket by executing simple code
251
+ try {
252
+ const result = await Promise.race([
253
+ this.jupyterServer.executeCode(
254
+ testContext.id,
255
+ "print('health_check')"
256
+ ),
257
+ new Promise<null>((_, reject) =>
258
+ setTimeout(() => reject(new Error("WebSocket timeout")), 3000)
259
+ ),
260
+ ]);
261
+
262
+ checks.websocket = result !== null;
263
+ } catch {
264
+ // WebSocket test failed
265
+ }
266
+
267
+ // Clean up test context
268
+ try {
269
+ await this.jupyterServer.deleteContext(testContext.id);
270
+ } catch {
271
+ // Ignore cleanup errors
272
+ }
273
+ }
274
+ } catch (error) {
275
+ console.log("[JupyterService] Advanced health check failed:", error);
276
+ }
277
+
278
+ return checks;
279
+ }
280
+
281
+ /**
282
+ * Ensure Jupyter is initialized before proceeding
283
+ * This will wait for initialization to complete or fail
284
+ */
285
+ private async ensureInitialized(timeoutMs: number = 30000): Promise<void> {
286
+ if (this.initialized) return;
287
+
288
+ // Start initialization if not already started
289
+ if (!this.initPromise) {
290
+ this.initPromise = this.initialize();
291
+ }
292
+
293
+ // Wait for initialization with timeout
294
+ try {
295
+ await Promise.race([
296
+ this.initPromise,
297
+ new Promise<never>((_, reject) =>
298
+ setTimeout(
299
+ () =>
300
+ reject(new Error("Timeout waiting for Jupyter initialization")),
301
+ timeoutMs
302
+ )
303
+ ),
304
+ ]);
305
+ } catch (error) {
306
+ // If it's a timeout and Jupyter is still initializing, throw a retryable error
307
+ if (
308
+ error instanceof Error &&
309
+ error.message.includes("Timeout") &&
310
+ !this.initError
311
+ ) {
312
+ throw new JupyterNotReadyError(
313
+ "Jupyter is taking longer than expected to initialize. Please try again.",
314
+ {
315
+ retryAfter: 10,
316
+ progress: this.getInitializationProgress(),
317
+ }
318
+ );
319
+ }
320
+ // If initialization actually failed, throw the real error
321
+ throw new Error(
322
+ `Jupyter initialization failed: ${
323
+ error instanceof Error ? error.message : "Unknown error"
324
+ }`
325
+ );
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Create context - will wait for Jupyter if still initializing
331
+ */
332
+ async createContext(req: CreateContextRequest): Promise<any> {
333
+ if (!this.initialized) {
334
+ console.log(
335
+ "[JupyterService] Context creation requested while Jupyter is initializing - waiting..."
336
+ );
337
+ const startWait = Date.now();
338
+ await this.ensureInitialized();
339
+ const waitTime = Date.now() - startWait;
340
+ console.log(
341
+ `[JupyterService] Jupyter ready after ${waitTime}ms wait - proceeding with context creation`
342
+ );
343
+ }
344
+
345
+ // Use circuit breaker for context creation
346
+ return await this.circuitBreakers.contextCreation.execute(async () => {
347
+ return await this.jupyterServer.createContext(req);
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Execute code - will wait for Jupyter if still initializing
353
+ */
354
+ async executeCode(
355
+ contextId: string | undefined,
356
+ code: string,
357
+ language?: string
358
+ ): Promise<Response> {
359
+ if (!this.initialized) {
360
+ console.log(
361
+ "[JupyterService] Code execution requested while Jupyter is initializing - waiting..."
362
+ );
363
+ const startWait = Date.now();
364
+ await this.ensureInitialized();
365
+ const waitTime = Date.now() - startWait;
366
+ console.log(
367
+ `[JupyterService] Jupyter ready after ${waitTime}ms wait - proceeding with code execution`
368
+ );
369
+ }
370
+
371
+ // Use circuit breaker for code execution
372
+ return await this.circuitBreakers.codeExecution.execute(async () => {
373
+ return await this.jupyterServer.executeCode(contextId, code, language);
374
+ });
375
+ }
376
+
377
+ /**
378
+ * List contexts with graceful degradation
379
+ */
380
+ async listContexts(): Promise<any[]> {
381
+ if (!this.initialized) {
382
+ return [];
383
+ }
384
+ return await this.jupyterServer.listContexts();
385
+ }
386
+
387
+ /**
388
+ * Delete context - will wait for Jupyter if still initializing
389
+ */
390
+ async deleteContext(contextId: string): Promise<void> {
391
+ if (!this.initialized) {
392
+ console.log(
393
+ "[JupyterService] Context deletion requested while Jupyter is initializing - waiting..."
394
+ );
395
+ const startWait = Date.now();
396
+ await this.ensureInitialized();
397
+ const waitTime = Date.now() - startWait;
398
+ console.log(
399
+ `[JupyterService] Jupyter ready after ${waitTime}ms wait - proceeding with context deletion`
400
+ );
401
+ }
402
+
403
+ // Use circuit breaker for kernel communication
404
+ return await this.circuitBreakers.kernelCommunication.execute(async () => {
405
+ return await this.jupyterServer.deleteContext(contextId);
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Shutdown the service
411
+ */
412
+ async shutdown(): Promise<void> {
413
+ if (this.initialized) {
414
+ await this.jupyterServer.shutdown();
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Get initialization progress
420
+ */
421
+ private getInitializationProgress(): number {
422
+ if (this.initialized) return 100;
423
+ if (!this.initPromise) return 0;
424
+
425
+ const elapsed = Date.now() - this.startTime;
426
+ const estimatedTotal = 20000; // 20 seconds estimated
427
+ return Math.min(95, Math.round((elapsed / estimatedTotal) * 100));
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Error thrown when Jupyter is not ready yet
433
+ * This matches the interface of the SDK's JupyterNotReadyError
434
+ */
435
+ export class JupyterNotReadyError extends Error {
436
+ public readonly retryAfter: number;
437
+ public readonly progress?: number;
438
+
439
+ constructor(
440
+ message: string,
441
+ options?: { retryAfter?: number; progress?: number }
442
+ ) {
443
+ super(message);
444
+ this.name = "JupyterNotReadyError";
445
+ this.retryAfter = options?.retryAfter || 5;
446
+ this.progress = options?.progress;
447
+ }
448
+ }
@@ -1,5 +1,17 @@
1
1
  #!/bin/bash
2
2
 
3
+ # Function to check if Jupyter is ready
4
+ check_jupyter_ready() {
5
+ curl -s http://localhost:8888/api > /dev/null 2>&1
6
+ }
7
+
8
+ # Function to notify Bun server that Jupyter is ready
9
+ notify_jupyter_ready() {
10
+ # Create a marker file that the Bun server can check
11
+ touch /tmp/jupyter-ready
12
+ echo "[Startup] Jupyter is ready, notified Bun server"
13
+ }
14
+
3
15
  # Start Jupyter notebook server in background
4
16
  echo "[Startup] Starting Jupyter server..."
5
17
  jupyter notebook \
@@ -17,36 +29,55 @@ jupyter notebook \
17
29
 
18
30
  JUPYTER_PID=$!
19
31
 
20
- # Wait for Jupyter to be ready
21
- echo "[Startup] Waiting for Jupyter to become ready..."
22
- MAX_ATTEMPTS=30
23
- ATTEMPT=0
32
+ # Start Bun server immediately (parallel startup)
33
+ echo "[Startup] Starting Bun server..."
34
+ bun index.ts &
35
+ BUN_PID=$!
24
36
 
25
- while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
26
- if curl -s http://localhost:8888/api > /dev/null 2>&1; then
27
- echo "[Startup] Jupyter server is ready!"
28
- break
29
- fi
37
+ # Monitor Jupyter readiness in background
38
+ (
39
+ echo "[Startup] Monitoring Jupyter readiness in background..."
40
+ MAX_ATTEMPTS=30
41
+ ATTEMPT=0
42
+ DELAY=0.5
43
+ MAX_DELAY=5
30
44
 
31
- # Check if Jupyter process is still running
32
- if ! kill -0 $JUPYTER_PID 2>/dev/null; then
33
- echo "[Startup] ERROR: Jupyter process died. Check /tmp/jupyter.log for details"
45
+ while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
46
+ if check_jupyter_ready; then
47
+ notify_jupyter_ready
48
+ echo "[Startup] Jupyter server is ready after $ATTEMPT attempts"
49
+ break
50
+ fi
51
+
52
+ # Check if Jupyter process is still running
53
+ if ! kill -0 $JUPYTER_PID 2>/dev/null; then
54
+ echo "[Startup] WARNING: Jupyter process died. Check /tmp/jupyter.log for details"
55
+ cat /tmp/jupyter.log
56
+ # Don't exit - let Bun server continue running in degraded mode
57
+ break
58
+ fi
59
+
60
+ ATTEMPT=$((ATTEMPT + 1))
61
+ echo "[Startup] Jupyter not ready yet (attempt $ATTEMPT/$MAX_ATTEMPTS, delay ${DELAY}s)"
62
+
63
+ # Sleep with exponential backoff
64
+ sleep $DELAY
65
+
66
+ # Increase delay exponentially with jitter, cap at MAX_DELAY
67
+ DELAY=$(awk "BEGIN {printf \"%.2f\", $DELAY * 1.5 + (rand() * 0.5)}")
68
+ # Use awk for comparison since bc might not be available
69
+ if [ $(awk "BEGIN {print ($DELAY > $MAX_DELAY)}") -eq 1 ]; then
70
+ DELAY=$MAX_DELAY
71
+ fi
72
+ done
73
+
74
+ if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
75
+ echo "[Startup] WARNING: Jupyter failed to become ready within attempts"
76
+ echo "[Startup] Jupyter logs:"
34
77
  cat /tmp/jupyter.log
35
- exit 1
78
+ # Don't exit - let Bun server continue in degraded mode
36
79
  fi
37
-
38
- ATTEMPT=$((ATTEMPT + 1))
39
- echo "[Startup] Waiting for Jupyter... (attempt $ATTEMPT/$MAX_ATTEMPTS)"
40
- sleep 1
41
- done
80
+ ) &
42
81
 
43
- if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
44
- echo "[Startup] ERROR: Jupyter failed to start within 30 seconds"
45
- echo "[Startup] Jupyter logs:"
46
- cat /tmp/jupyter.log
47
- exit 1
48
- fi
49
-
50
- # Start the main Bun server
51
- echo "[Startup] Starting Bun server..."
52
- exec bun index.ts
82
+ # Wait for Bun server (main process)
83
+ wait $BUN_PID
@@ -0,0 +1,129 @@
1
+ // src/errors.ts
2
+ var SandboxError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = this.constructor.name;
6
+ if (Error.captureStackTrace) {
7
+ Error.captureStackTrace(this, this.constructor);
8
+ }
9
+ }
10
+ };
11
+ var JupyterNotReadyError = class extends SandboxError {
12
+ code = "JUPYTER_NOT_READY";
13
+ retryAfter;
14
+ progress;
15
+ constructor(message, options) {
16
+ super(
17
+ message || "Jupyter is still initializing. Please retry in a few seconds."
18
+ );
19
+ this.retryAfter = options?.retryAfter || 5;
20
+ this.progress = options?.progress;
21
+ }
22
+ };
23
+ var ContextNotFoundError = class extends SandboxError {
24
+ code = "CONTEXT_NOT_FOUND";
25
+ contextId;
26
+ constructor(contextId) {
27
+ super(`Context ${contextId} not found`);
28
+ this.contextId = contextId;
29
+ }
30
+ };
31
+ var CodeExecutionError = class extends SandboxError {
32
+ code = "CODE_EXECUTION_ERROR";
33
+ executionError;
34
+ constructor(message, executionError) {
35
+ super(message);
36
+ this.executionError = executionError;
37
+ }
38
+ };
39
+ var ContainerNotReadyError = class extends SandboxError {
40
+ code = "CONTAINER_NOT_READY";
41
+ constructor(message) {
42
+ super(
43
+ message || "Container is not ready. Please wait for initialization to complete."
44
+ );
45
+ }
46
+ };
47
+ var SandboxNetworkError = class extends SandboxError {
48
+ code = "NETWORK_ERROR";
49
+ statusCode;
50
+ statusText;
51
+ constructor(message, statusCode, statusText) {
52
+ super(message);
53
+ this.statusCode = statusCode;
54
+ this.statusText = statusText;
55
+ }
56
+ };
57
+ var ServiceUnavailableError = class extends SandboxError {
58
+ code = "SERVICE_UNAVAILABLE";
59
+ retryAfter;
60
+ constructor(message, retryAfter) {
61
+ super(message || "Service temporarily unavailable");
62
+ this.retryAfter = retryAfter;
63
+ }
64
+ };
65
+ function isJupyterNotReadyError(error) {
66
+ return error instanceof JupyterNotReadyError;
67
+ }
68
+ function isSandboxError(error) {
69
+ return error instanceof SandboxError;
70
+ }
71
+ function isRetryableError(error) {
72
+ if (error instanceof JupyterNotReadyError || error instanceof ContainerNotReadyError || error instanceof ServiceUnavailableError) {
73
+ return true;
74
+ }
75
+ if (error instanceof SandboxNetworkError) {
76
+ return error.statusCode ? [502, 503, 504].includes(error.statusCode) : false;
77
+ }
78
+ return false;
79
+ }
80
+ async function parseErrorResponse(response) {
81
+ let data;
82
+ try {
83
+ data = await response.json();
84
+ } catch {
85
+ return new SandboxNetworkError(
86
+ `Request failed with status ${response.status}`,
87
+ response.status,
88
+ response.statusText
89
+ );
90
+ }
91
+ if (response.status === 503) {
92
+ if (data.status === "circuit_open") {
93
+ return new ServiceUnavailableError(
94
+ "Service temporarily unavailable",
95
+ parseInt(response.headers.get("Retry-After") || "30")
96
+ );
97
+ }
98
+ if (data.status === "initializing") {
99
+ return new JupyterNotReadyError(data.error, {
100
+ retryAfter: parseInt(response.headers.get("Retry-After") || "5"),
101
+ progress: data.progress
102
+ });
103
+ }
104
+ }
105
+ if (response.status === 404 && data.error?.includes("Context") && data.error?.includes("not found")) {
106
+ const contextId = data.error.match(/Context (\S+) not found/)?.[1] || "unknown";
107
+ return new ContextNotFoundError(contextId);
108
+ }
109
+ return new SandboxNetworkError(
110
+ data.error || `Request failed with status ${response.status}`,
111
+ response.status,
112
+ response.statusText
113
+ );
114
+ }
115
+
116
+ export {
117
+ SandboxError,
118
+ JupyterNotReadyError,
119
+ ContextNotFoundError,
120
+ CodeExecutionError,
121
+ ContainerNotReadyError,
122
+ SandboxNetworkError,
123
+ ServiceUnavailableError,
124
+ isJupyterNotReadyError,
125
+ isSandboxError,
126
+ isRetryableError,
127
+ parseErrorResponse
128
+ };
129
+ //# sourceMappingURL=chunk-LALY4SFU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Standard error response from the sandbox API\n */\nexport interface SandboxErrorResponse {\n error?: string;\n status?: string;\n progress?: number;\n}\n\n/**\n * Base error class for all Sandbox-related errors\n */\nexport class SandboxError extends Error {\n constructor(message: string) {\n super(message);\n this.name = this.constructor.name;\n\n // Maintains proper stack trace for where our error was thrown (only available on V8)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/**\n * Error thrown when Jupyter functionality is requested but the service is still initializing.\n *\n * Note: With the current implementation, requests wait for Jupyter to be ready.\n * This error is only thrown when:\n * 1. The request times out waiting for Jupyter (default: 30 seconds)\n * 2. Jupyter initialization actually fails\n *\n * Most requests will succeed after a delay, not throw this error.\n */\nexport class JupyterNotReadyError extends SandboxError {\n public readonly code = \"JUPYTER_NOT_READY\";\n public readonly retryAfter: number;\n public readonly progress?: number;\n\n constructor(\n message?: string,\n options?: { retryAfter?: number; progress?: number }\n ) {\n super(\n message || \"Jupyter is still initializing. Please retry in a few seconds.\"\n );\n this.retryAfter = options?.retryAfter || 5;\n this.progress = options?.progress;\n }\n}\n\n/**\n * Error thrown when a context is not found\n */\nexport class ContextNotFoundError extends SandboxError {\n public readonly code = \"CONTEXT_NOT_FOUND\";\n public readonly contextId: string;\n\n constructor(contextId: string) {\n super(`Context ${contextId} not found`);\n this.contextId = contextId;\n }\n}\n\n/**\n * Error thrown when code execution fails\n */\nexport class CodeExecutionError extends SandboxError {\n public readonly code = \"CODE_EXECUTION_ERROR\";\n public readonly executionError?: {\n ename?: string;\n evalue?: string;\n traceback?: string[];\n };\n\n constructor(message: string, executionError?: any) {\n super(message);\n this.executionError = executionError;\n }\n}\n\n/**\n * Error thrown when the sandbox container is not ready\n */\nexport class ContainerNotReadyError extends SandboxError {\n public readonly code = \"CONTAINER_NOT_READY\";\n\n constructor(message?: string) {\n super(\n message ||\n \"Container is not ready. Please wait for initialization to complete.\"\n );\n }\n}\n\n/**\n * Error thrown when a network request to the sandbox fails\n */\nexport class SandboxNetworkError extends SandboxError {\n public readonly code = \"NETWORK_ERROR\";\n public readonly statusCode?: number;\n public readonly statusText?: string;\n\n constructor(message: string, statusCode?: number, statusText?: string) {\n super(message);\n this.statusCode = statusCode;\n this.statusText = statusText;\n }\n}\n\n/**\n * Error thrown when service is temporarily unavailable (e.g., circuit breaker open)\n */\nexport class ServiceUnavailableError extends SandboxError {\n public readonly code = \"SERVICE_UNAVAILABLE\";\n public readonly retryAfter?: number;\n\n constructor(message?: string, retryAfter?: number) {\n // Simple, user-friendly message without implementation details\n super(message || \"Service temporarily unavailable\");\n this.retryAfter = retryAfter;\n }\n}\n\n/**\n * Type guard to check if an error is a JupyterNotReadyError\n */\nexport function isJupyterNotReadyError(\n error: unknown\n): error is JupyterNotReadyError {\n return error instanceof JupyterNotReadyError;\n}\n\n/**\n * Type guard to check if an error is any SandboxError\n */\nexport function isSandboxError(error: unknown): error is SandboxError {\n return error instanceof SandboxError;\n}\n\n/**\n * Helper to determine if an error is retryable\n */\nexport function isRetryableError(error: unknown): boolean {\n if (\n error instanceof JupyterNotReadyError ||\n error instanceof ContainerNotReadyError ||\n error instanceof ServiceUnavailableError\n ) {\n return true;\n }\n\n if (error instanceof SandboxNetworkError) {\n // Retry on 502, 503, 504 (gateway/service unavailable errors)\n return error.statusCode\n ? [502, 503, 504].includes(error.statusCode)\n : false;\n }\n\n return false;\n}\n\n/**\n * Parse error response from the sandbox API and return appropriate error instance\n */\nexport async function parseErrorResponse(\n response: Response\n): Promise<SandboxError> {\n let data: SandboxErrorResponse;\n\n try {\n data = (await response.json()) as SandboxErrorResponse;\n } catch {\n // If JSON parsing fails, return a generic network error\n return new SandboxNetworkError(\n `Request failed with status ${response.status}`,\n response.status,\n response.statusText\n );\n }\n\n // Check for specific error types based on response\n if (response.status === 503) {\n // Circuit breaker error\n if (data.status === \"circuit_open\") {\n return new ServiceUnavailableError(\n \"Service temporarily unavailable\",\n parseInt(response.headers.get(\"Retry-After\") || \"30\")\n );\n }\n\n // Jupyter initialization error\n if (data.status === \"initializing\") {\n return new JupyterNotReadyError(data.error, {\n retryAfter: parseInt(response.headers.get(\"Retry-After\") || \"5\"),\n progress: data.progress,\n });\n }\n }\n\n // Check for context not found\n if (\n response.status === 404 &&\n data.error?.includes(\"Context\") &&\n data.error?.includes(\"not found\")\n ) {\n const contextId =\n data.error.match(/Context (\\S+) not found/)?.[1] || \"unknown\";\n return new ContextNotFoundError(contextId);\n }\n\n // Default network error\n return new SandboxNetworkError(\n data.error || `Request failed with status ${response.status}`,\n response.status,\n response.statusText\n );\n}\n"],"mappings":"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","names":[]}