@cloudflare/sandbox 0.6.1 → 0.6.3

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createLogger, c as Execution, d as getEnvString, i as shellEscape, l as ResultImpl, n as isProcess, o as createNoOpLogger, r as isProcessStatus, s as TraceContext, t as isExecResult, u as GitLogger } from "./dist-gVyG2H2h.js";
1
+ import { a as shellEscape, c as TraceContext, d as GitLogger, f as getEnvString, i as isTerminalStatus, l as Execution, n as isProcess, o as createLogger, r as isProcessStatus, s as createNoOpLogger, t as isExecResult, u as ResultImpl } from "./dist-D0sZt0AD.js";
2
2
  import { Container, getContainer, switchPort } from "@cloudflare/containers";
3
3
 
4
4
  //#region ../shared/dist/errors/codes.js
@@ -51,6 +51,8 @@ const ErrorCode = {
51
51
  CONTEXT_NOT_FOUND: "CONTEXT_NOT_FOUND",
52
52
  CODE_EXECUTION_ERROR: "CODE_EXECUTION_ERROR",
53
53
  PYTHON_NOT_AVAILABLE: "PYTHON_NOT_AVAILABLE",
54
+ PROCESS_READY_TIMEOUT: "PROCESS_READY_TIMEOUT",
55
+ PROCESS_EXITED_BEFORE_READY: "PROCESS_EXITED_BEFORE_READY",
54
56
  VALIDATION_FAILED: "VALIDATION_FAILED",
55
57
  INVALID_JSON_RESPONSE: "INVALID_JSON_RESPONSE",
56
58
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
@@ -96,6 +98,8 @@ const ERROR_STATUS_MAP = {
96
98
  [ErrorCode.GIT_NETWORK_ERROR]: 502,
97
99
  [ErrorCode.PYTHON_NOT_AVAILABLE]: 501,
98
100
  [ErrorCode.INTERPRETER_NOT_READY]: 503,
101
+ [ErrorCode.PROCESS_READY_TIMEOUT]: 408,
102
+ [ErrorCode.PROCESS_EXITED_BEFORE_READY]: 500,
99
103
  [ErrorCode.NO_SPACE]: 500,
100
104
  [ErrorCode.TOO_MANY_FILES]: 500,
101
105
  [ErrorCode.TOO_MANY_LINKS]: 500,
@@ -570,6 +574,48 @@ var ValidationFailedError = class extends SandboxError {
570
574
  return this.context.validationErrors;
571
575
  }
572
576
  };
577
+ /**
578
+ * Error thrown when a process does not become ready within the timeout period
579
+ */
580
+ var ProcessReadyTimeoutError = class extends SandboxError {
581
+ constructor(errorResponse) {
582
+ super(errorResponse);
583
+ this.name = "ProcessReadyTimeoutError";
584
+ }
585
+ get processId() {
586
+ return this.context.processId;
587
+ }
588
+ get command() {
589
+ return this.context.command;
590
+ }
591
+ get condition() {
592
+ return this.context.condition;
593
+ }
594
+ get timeout() {
595
+ return this.context.timeout;
596
+ }
597
+ };
598
+ /**
599
+ * Error thrown when a process exits before becoming ready
600
+ */
601
+ var ProcessExitedBeforeReadyError = class extends SandboxError {
602
+ constructor(errorResponse) {
603
+ super(errorResponse);
604
+ this.name = "ProcessExitedBeforeReadyError";
605
+ }
606
+ get processId() {
607
+ return this.context.processId;
608
+ }
609
+ get command() {
610
+ return this.context.command;
611
+ }
612
+ get condition() {
613
+ return this.context.condition;
614
+ }
615
+ get exitCode() {
616
+ return this.context.exitCode;
617
+ }
618
+ };
573
619
 
574
620
  //#endregion
575
621
  //#region src/errors/adapter.ts
@@ -1371,6 +1417,20 @@ var PortClient = class extends BaseHttpClient {
1371
1417
  throw error;
1372
1418
  }
1373
1419
  }
1420
+ /**
1421
+ * Check if a port is ready to accept connections
1422
+ * @param request - Port check configuration
1423
+ */
1424
+ async checkPortReady(request) {
1425
+ try {
1426
+ return await this.post("/api/port-check", request);
1427
+ } catch (error) {
1428
+ return {
1429
+ ready: false,
1430
+ error: error instanceof Error ? error.message : "Port check failed"
1431
+ };
1432
+ }
1433
+ }
1374
1434
  };
1375
1435
 
1376
1436
  //#endregion
@@ -2063,7 +2123,7 @@ function resolveS3fsOptions(provider, userOptions) {
2063
2123
  * This file is auto-updated by .github/changeset-version.ts during releases
2064
2124
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
2065
2125
  */
2066
- const SDK_VERSION = "0.6.1";
2126
+ const SDK_VERSION = "0.6.3";
2067
2127
 
2068
2128
  //#endregion
2069
2129
  //#region src/sandbox.ts
@@ -2645,9 +2705,209 @@ var Sandbox = class extends Container {
2645
2705
  stdout: logs.stdout,
2646
2706
  stderr: logs.stderr
2647
2707
  };
2708
+ },
2709
+ waitForLog: async (pattern, timeout) => {
2710
+ return this.waitForLogPattern(data.id, data.command, pattern, timeout);
2711
+ },
2712
+ waitForPort: async (port, options) => {
2713
+ await this.waitForPortReady(data.id, data.command, port, options);
2648
2714
  }
2649
2715
  };
2650
2716
  }
2717
+ /**
2718
+ * Wait for a log pattern to appear in process output
2719
+ */
2720
+ async waitForLogPattern(processId, command, pattern, timeout) {
2721
+ const startTime = Date.now();
2722
+ const conditionStr = this.conditionToString(pattern);
2723
+ let collectedStdout = "";
2724
+ let collectedStderr = "";
2725
+ try {
2726
+ const existingLogs = await this.getProcessLogs(processId);
2727
+ collectedStdout = existingLogs.stdout;
2728
+ if (collectedStdout && !collectedStdout.endsWith("\n")) collectedStdout += "\n";
2729
+ collectedStderr = existingLogs.stderr;
2730
+ if (collectedStderr && !collectedStderr.endsWith("\n")) collectedStderr += "\n";
2731
+ const stdoutResult = this.matchPattern(existingLogs.stdout, pattern);
2732
+ if (stdoutResult) return stdoutResult;
2733
+ const stderrResult = this.matchPattern(existingLogs.stderr, pattern);
2734
+ if (stderrResult) return stderrResult;
2735
+ } catch (error) {
2736
+ this.logger.debug("Could not get existing logs, will stream", {
2737
+ processId,
2738
+ error: error instanceof Error ? error.message : String(error)
2739
+ });
2740
+ }
2741
+ const stream = await this.streamProcessLogs(processId);
2742
+ let timeoutId;
2743
+ let timeoutPromise;
2744
+ if (timeout !== void 0) {
2745
+ const remainingTime = timeout - (Date.now() - startTime);
2746
+ if (remainingTime <= 0) throw this.createReadyTimeoutError(processId, command, conditionStr, timeout);
2747
+ timeoutPromise = new Promise((_, reject) => {
2748
+ timeoutId = setTimeout(() => {
2749
+ reject(this.createReadyTimeoutError(processId, command, conditionStr, timeout));
2750
+ }, remainingTime);
2751
+ });
2752
+ }
2753
+ try {
2754
+ const streamProcessor = async () => {
2755
+ const DEBOUNCE_MS = 50;
2756
+ let lastCheckTime = 0;
2757
+ let pendingCheck = false;
2758
+ const checkPattern = () => {
2759
+ const stdoutResult = this.matchPattern(collectedStdout, pattern);
2760
+ if (stdoutResult) return stdoutResult;
2761
+ const stderrResult = this.matchPattern(collectedStderr, pattern);
2762
+ if (stderrResult) return stderrResult;
2763
+ return null;
2764
+ };
2765
+ for await (const event of parseSSEStream(stream)) {
2766
+ if (event.type === "stdout" || event.type === "stderr") {
2767
+ const data = event.data || "";
2768
+ if (event.type === "stdout") collectedStdout += data;
2769
+ else collectedStderr += data;
2770
+ pendingCheck = true;
2771
+ const now = Date.now();
2772
+ if (now - lastCheckTime >= DEBOUNCE_MS) {
2773
+ lastCheckTime = now;
2774
+ pendingCheck = false;
2775
+ const result = checkPattern();
2776
+ if (result) return result;
2777
+ }
2778
+ }
2779
+ if (event.type === "exit") {
2780
+ if (pendingCheck) {
2781
+ const result = checkPattern();
2782
+ if (result) return result;
2783
+ }
2784
+ throw this.createExitedBeforeReadyError(processId, command, conditionStr, event.exitCode ?? 1);
2785
+ }
2786
+ }
2787
+ if (pendingCheck) {
2788
+ const result = checkPattern();
2789
+ if (result) return result;
2790
+ }
2791
+ throw this.createExitedBeforeReadyError(processId, command, conditionStr, 0);
2792
+ };
2793
+ if (timeoutPromise) return await Promise.race([streamProcessor(), timeoutPromise]);
2794
+ return await streamProcessor();
2795
+ } finally {
2796
+ if (timeoutId) clearTimeout(timeoutId);
2797
+ }
2798
+ }
2799
+ /**
2800
+ * Wait for a port to become available (for process readiness checking)
2801
+ */
2802
+ async waitForPortReady(processId, command, port, options) {
2803
+ const { mode = "http", path = "/", status = {
2804
+ min: 200,
2805
+ max: 399
2806
+ }, timeout, interval = 500 } = options ?? {};
2807
+ const startTime = Date.now();
2808
+ const conditionStr = mode === "http" ? `port ${port} (HTTP ${path})` : `port ${port} (TCP)`;
2809
+ const targetInterval = interval;
2810
+ let checkCount = 0;
2811
+ const checkRequest = {
2812
+ port,
2813
+ mode,
2814
+ path,
2815
+ statusMin: typeof status === "number" ? status : status.min,
2816
+ statusMax: typeof status === "number" ? status : status.max
2817
+ };
2818
+ while (true) {
2819
+ if (timeout !== void 0) {
2820
+ if (timeout - (Date.now() - startTime) <= 0) throw this.createReadyTimeoutError(processId, command, conditionStr, timeout);
2821
+ }
2822
+ const iterationStart = Date.now();
2823
+ if (checkCount % 3 === 0) {
2824
+ const processInfo = await this.getProcess(processId);
2825
+ if (!processInfo || isTerminalStatus(processInfo.status)) throw this.createExitedBeforeReadyError(processId, command, conditionStr, processInfo?.exitCode ?? 1);
2826
+ }
2827
+ try {
2828
+ if ((await this.client.ports.checkPortReady(checkRequest)).ready) return;
2829
+ } catch {}
2830
+ checkCount++;
2831
+ const iterationDuration = Date.now() - iterationStart;
2832
+ const sleepTime = Math.max(0, targetInterval - iterationDuration);
2833
+ if (sleepTime > 0) {
2834
+ if (timeout === void 0 || Date.now() - startTime + sleepTime < timeout) await new Promise((resolve) => setTimeout(resolve, sleepTime));
2835
+ }
2836
+ }
2837
+ }
2838
+ /**
2839
+ * Match a pattern against text
2840
+ */
2841
+ matchPattern(text, pattern) {
2842
+ if (typeof pattern === "string") {
2843
+ if (text.includes(pattern)) {
2844
+ const lines = text.split("\n");
2845
+ for (const line of lines) if (line.includes(pattern)) return { line };
2846
+ return { line: pattern };
2847
+ }
2848
+ } else {
2849
+ const safePattern = new RegExp(pattern.source, pattern.flags.replace("g", ""));
2850
+ const match = text.match(safePattern);
2851
+ if (match) {
2852
+ const lines = text.split("\n");
2853
+ for (const line of lines) {
2854
+ const lineMatch = line.match(safePattern);
2855
+ if (lineMatch) return {
2856
+ line,
2857
+ match: lineMatch
2858
+ };
2859
+ }
2860
+ return {
2861
+ line: match[0],
2862
+ match
2863
+ };
2864
+ }
2865
+ }
2866
+ return null;
2867
+ }
2868
+ /**
2869
+ * Convert a log pattern to a human-readable string
2870
+ */
2871
+ conditionToString(pattern) {
2872
+ if (typeof pattern === "string") return `"${pattern}"`;
2873
+ return pattern.toString();
2874
+ }
2875
+ /**
2876
+ * Create a ProcessReadyTimeoutError
2877
+ */
2878
+ createReadyTimeoutError(processId, command, condition, timeout) {
2879
+ return new ProcessReadyTimeoutError({
2880
+ code: ErrorCode.PROCESS_READY_TIMEOUT,
2881
+ message: `Process did not become ready within ${timeout}ms. Waiting for: ${condition}`,
2882
+ context: {
2883
+ processId,
2884
+ command,
2885
+ condition,
2886
+ timeout
2887
+ },
2888
+ httpStatus: 408,
2889
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2890
+ suggestion: `Check if your process outputs ${condition}. You can increase the timeout parameter.`
2891
+ });
2892
+ }
2893
+ /**
2894
+ * Create a ProcessExitedBeforeReadyError
2895
+ */
2896
+ createExitedBeforeReadyError(processId, command, condition, exitCode) {
2897
+ return new ProcessExitedBeforeReadyError({
2898
+ code: ErrorCode.PROCESS_EXITED_BEFORE_READY,
2899
+ message: `Process exited with code ${exitCode} before becoming ready. Waiting for: ${condition}`,
2900
+ context: {
2901
+ processId,
2902
+ command,
2903
+ condition,
2904
+ exitCode
2905
+ },
2906
+ httpStatus: 500,
2907
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2908
+ suggestion: "Check process logs with getLogs() for error messages"
2909
+ });
2910
+ }
2651
2911
  async startProcess(command, options, sessionId) {
2652
2912
  try {
2653
2913
  const session = sessionId ?? await this.ensureDefaultSession();
@@ -2670,12 +2930,37 @@ var Sandbox = class extends Container {
2670
2930
  exitCode: void 0
2671
2931
  }, session);
2672
2932
  if (options?.onStart) options.onStart(processObj);
2933
+ if (options?.onOutput || options?.onExit) this.startProcessCallbackStream(response.processId, options).catch(() => {});
2673
2934
  return processObj;
2674
2935
  } catch (error) {
2675
2936
  if (options?.onError && error instanceof Error) options.onError(error);
2676
2937
  throw error;
2677
2938
  }
2678
2939
  }
2940
+ /**
2941
+ * Start background streaming for process callbacks
2942
+ * Opens SSE stream to container and routes events to callbacks
2943
+ */
2944
+ async startProcessCallbackStream(processId, options) {
2945
+ try {
2946
+ const stream = await this.client.processes.streamProcessLogs(processId);
2947
+ for await (const event of parseSSEStream(stream)) switch (event.type) {
2948
+ case "stdout":
2949
+ if (event.data && options.onOutput) options.onOutput("stdout", event.data);
2950
+ break;
2951
+ case "stderr":
2952
+ if (event.data && options.onOutput) options.onOutput("stderr", event.data);
2953
+ break;
2954
+ case "exit":
2955
+ case "complete":
2956
+ if (options.onExit) options.onExit(event.exitCode ?? null);
2957
+ return;
2958
+ }
2959
+ } catch (error) {
2960
+ if (options.onError && error instanceof Error) options.onError(error);
2961
+ this.logger.error("Background process streaming failed", error instanceof Error ? error : new Error(String(error)), { processId });
2962
+ }
2963
+ }
2679
2964
  async listProcesses(sessionId) {
2680
2965
  const session = sessionId ?? await this.ensureDefaultSession();
2681
2966
  return (await this.client.processes.listProcesses()).processes.map((processData) => this.createProcessFromDTO({
@@ -3141,5 +3426,5 @@ async function collectFile(stream) {
3141
3426
  }
3142
3427
 
3143
3428
  //#endregion
3144
- export { BucketMountError, CodeInterpreter, CommandClient, FileClient, GitClient, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyToSandbox, responseToAsyncIterable, streamFile };
3429
+ export { BucketMountError, CodeInterpreter, CommandClient, FileClient, GitClient, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyToSandbox, responseToAsyncIterable, streamFile };
3145
3430
  //# sourceMappingURL=index.js.map