@cloudflare/sandbox 0.12.0 → 0.12.1

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.
@@ -719,6 +719,53 @@ function createErrorFromResponse(errorResponse, options) {
719
719
  }
720
720
  }
721
721
 
722
+ //#endregion
723
+ //#region src/response-retry.ts
724
+ const DEFAULT_INITIAL_RETRY_DELAY_MS = 3e3;
725
+ const DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
726
+ const RETRYABLE_WEBSOCKET_UPGRADE_STATUSES = new Set([
727
+ 500,
728
+ 502,
729
+ 503,
730
+ 504
731
+ ]);
732
+ function isRetryableWebSocketUpgradeResponse(response) {
733
+ return RETRYABLE_WEBSOCKET_UPGRADE_STATUSES.has(response.status);
734
+ }
735
+ /**
736
+ * Retry Response-returning operations while their response remains retryable.
737
+ * The retry budget covers the whole operation; each attempt owns any
738
+ * per-request timeout inside the caller-provided `fetchResponse` function.
739
+ */
740
+ async function fetchWithResponseRetry(fetchResponse, options) {
741
+ const startTime = Date.now();
742
+ let attempt = 0;
743
+ while (true) {
744
+ const response = await fetchResponse();
745
+ if (!options.shouldRetry(response)) return response;
746
+ const elapsed = Date.now() - startTime;
747
+ const remaining = options.retryTimeoutMs - elapsed;
748
+ if (remaining <= options.minTimeForRetryMs) {
749
+ options.onRetryExhausted?.({
750
+ attempts: attempt + 1,
751
+ elapsedMs: elapsed,
752
+ response
753
+ });
754
+ return response;
755
+ }
756
+ const delay = Math.min(DEFAULT_INITIAL_RETRY_DELAY_MS * 2 ** attempt, DEFAULT_MAX_RETRY_DELAY_MS);
757
+ options.logger.info(options.retryLogMessage, {
758
+ status: response.status,
759
+ attempt: attempt + 1,
760
+ delayMs: delay,
761
+ remainingSec: Math.floor(remaining / 1e3),
762
+ ...options.getRetryLogContext?.(response)
763
+ });
764
+ await new Promise((resolve) => setTimeout(resolve, delay));
765
+ attempt++;
766
+ }
767
+ }
768
+
722
769
  //#endregion
723
770
  //#region src/clients/transport/base-transport.ts
724
771
  /**
@@ -754,30 +801,17 @@ var BaseTransport = class {
754
801
  * transport-specific doFetch() with retry logic for container startup.
755
802
  */
756
803
  async fetch(path$1, options) {
757
- const startTime = Date.now();
758
- let attempt = 0;
759
- while (true) {
760
- const response = await this.doFetch(path$1, options);
761
- if (response.status === 503) {
762
- const elapsed = Date.now() - startTime;
763
- const remaining = this.retryTimeoutMs - elapsed;
764
- if (remaining > MIN_TIME_FOR_RETRY_MS$1) {
765
- const delay = Math.min(3e3 * 2 ** attempt, 3e4);
766
- this.logger.info("Container not ready, retrying", {
767
- status: response.status,
768
- attempt: attempt + 1,
769
- delayMs: delay,
770
- remainingSec: Math.floor(remaining / 1e3),
771
- mode: this.getMode()
772
- });
773
- await this.sleep(delay);
774
- attempt++;
775
- continue;
776
- }
777
- this.logger.error("Container failed to become ready", /* @__PURE__ */ new Error(`Failed after ${attempt + 1} attempts over ${Math.floor(elapsed / 1e3)}s`));
804
+ return fetchWithResponseRetry(() => this.doFetch(path$1, options), {
805
+ retryTimeoutMs: this.retryTimeoutMs,
806
+ minTimeForRetryMs: MIN_TIME_FOR_RETRY_MS$1,
807
+ logger: this.logger,
808
+ retryLogMessage: "Container not ready, retrying",
809
+ shouldRetry: (response) => response.status === 503,
810
+ getRetryLogContext: () => ({ mode: this.getMode() }),
811
+ onRetryExhausted: ({ attempts, elapsedMs }) => {
812
+ this.logger.error("Container failed to become ready", /* @__PURE__ */ new Error(`Failed after ${attempts} attempts over ${Math.floor(elapsedMs / 1e3)}s`));
778
813
  }
779
- return response;
780
- }
814
+ });
781
815
  }
782
816
  /**
783
817
  * Build a URL targeting the container's HTTP server.
@@ -817,12 +851,6 @@ var BaseTransport = class {
817
851
  if (!response.body) throw new Error("No response body for streaming");
818
852
  return response.body;
819
853
  }
820
- /**
821
- * Sleep utility for retry delays
822
- */
823
- sleep(ms) {
824
- return new Promise((resolve) => setTimeout(resolve, ms));
825
- }
826
854
  };
827
855
 
828
856
  //#endregion
@@ -991,24 +1019,13 @@ var WebSocketTransport = class extends BaseTransport {
991
1019
  else await this.connectViaWebSocket();
992
1020
  }
993
1021
  async fetchUpgradeWithRetry(attemptUpgrade) {
994
- const retryTimeoutMs = this.getRetryTimeoutMs();
995
- const startTime = Date.now();
996
- let attempt = 0;
997
- while (true) {
998
- const response = await attemptUpgrade();
999
- if (response.status !== 503) return response;
1000
- const remaining = retryTimeoutMs - (Date.now() - startTime);
1001
- if (remaining <= MIN_TIME_FOR_CONNECT_RETRY_MS) return response;
1002
- const delay = Math.min(3e3 * 2 ** attempt, 3e4);
1003
- this.logger.info("WebSocket container not ready, retrying", {
1004
- status: response.status,
1005
- attempt: attempt + 1,
1006
- delayMs: delay,
1007
- remainingSec: Math.floor(remaining / 1e3)
1008
- });
1009
- await this.sleep(delay);
1010
- attempt++;
1011
- }
1022
+ return fetchWithResponseRetry(attemptUpgrade, {
1023
+ retryTimeoutMs: this.getRetryTimeoutMs(),
1024
+ minTimeForRetryMs: MIN_TIME_FOR_CONNECT_RETRY_MS,
1025
+ logger: this.logger,
1026
+ retryLogMessage: "WebSocket upgrade returned retryable status, retrying",
1027
+ shouldRetry: isRetryableWebSocketUpgradeResponse
1028
+ });
1012
1029
  }
1013
1030
  /**
1014
1031
  * Connect using fetch-based WebSocket (Cloudflare Workers style)
@@ -2377,7 +2394,7 @@ var SandboxClient = class {
2377
2394
  this.watch = new WatchClient(clientOptions);
2378
2395
  }
2379
2396
  /**
2380
- * Update the 503 retry budget on all transports without recreating the client.
2397
+ * Update the transport retry budget without recreating the client.
2381
2398
  *
2382
2399
  * In WebSocket mode a single shared transport is used, so one update covers
2383
2400
  * every sub-client. In HTTP mode each sub-client owns its own transport, so
@@ -2464,7 +2481,6 @@ const DISABLE_SESSION_TOKEN = "__DISABLE_SESSION__";
2464
2481
  const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
2465
2482
  const DEFAULT_RETRY_TIMEOUT_MS = 12e4;
2466
2483
  const MIN_TIME_FOR_RETRY_MS = 15e3;
2467
- const MAX_RETRY_BACKOFF_MS = 3e4;
2468
2484
  /**
2469
2485
  * Manages a capnweb WebSocket RPC session to the container.
2470
2486
  *
@@ -2542,7 +2558,7 @@ var ContainerControlConnection = class {
2542
2558
  this.connectPromise = null;
2543
2559
  }
2544
2560
  /**
2545
- * Update the 503 retry budget without recreating the connection. Takes
2561
+ * Update the upgrade retry budget without recreating the connection. Takes
2546
2562
  * effect on the next `connect()`; an in-flight connect uses the value
2547
2563
  * captured at start. Mirrors `WebSocketTransport.setRetryTimeoutMs`.
2548
2564
  */
@@ -2606,30 +2622,18 @@ var ContainerControlConnection = class {
2606
2622
  }
2607
2623
  }
2608
2624
  /**
2609
- * Issue WebSocket upgrade fetches, retrying transient 503 responses with
2610
- * exponential backoff (3s 6s 12s → … capped at 30s) until either
2611
- * the upgrade succeeds, a non-503 status is returned, or the retry budget
2612
- * runs out. Mirrors `WebSocketTransport.fetchUpgradeWithRetry` so both
2613
- * transports behave the same way during container startup.
2625
+ * Issue WebSocket upgrade fetches, retrying transient control-plane
2626
+ * unavailability responses until either the upgrade succeeds, a
2627
+ * non-retryable status is returned, or the retry budget runs out.
2614
2628
  */
2615
2629
  async fetchUpgradeWithRetry() {
2616
- const retryTimeoutMs = this.retryTimeoutMs;
2617
- const startTime = Date.now();
2618
- let attempt = 0;
2619
- while (true) {
2620
- const response = await this.fetchUpgradeAttempt();
2621
- if (response.status !== 503) return response;
2622
- const remaining = retryTimeoutMs - (Date.now() - startTime);
2623
- if (remaining <= MIN_TIME_FOR_RETRY_MS) return response;
2624
- const delay = Math.min(3e3 * 2 ** attempt, MAX_RETRY_BACKOFF_MS);
2625
- this.logger.info("ContainerControlConnection upgrade returned 503, retrying", {
2626
- attempt: attempt + 1,
2627
- delayMs: delay,
2628
- remainingSec: Math.floor(remaining / 1e3)
2629
- });
2630
- await this.sleep(delay);
2631
- attempt++;
2632
- }
2630
+ return fetchWithResponseRetry(() => this.fetchUpgradeAttempt(), {
2631
+ retryTimeoutMs: this.retryTimeoutMs,
2632
+ minTimeForRetryMs: MIN_TIME_FOR_RETRY_MS,
2633
+ logger: this.logger,
2634
+ retryLogMessage: "ContainerControlConnection upgrade returned retryable status, retrying",
2635
+ shouldRetry: isRetryableWebSocketUpgradeResponse
2636
+ });
2633
2637
  }
2634
2638
  /**
2635
2639
  * Single WebSocket-upgrade fetch attempt. Owns its own AbortController so
@@ -2653,9 +2657,6 @@ var ContainerControlConnection = class {
2653
2657
  clearTimeout(timeout);
2654
2658
  }
2655
2659
  }
2656
- sleep(ms) {
2657
- return new Promise((resolve) => setTimeout(resolve, ms));
2658
- }
2659
2660
  };
2660
2661
  /**
2661
2662
  * RPC transport that queues sends and blocks receives until a WebSocket
@@ -3032,7 +3033,7 @@ var ContainerControlClient = class {
3032
3033
  return wrapStub(this.getConnection().rpc().interpreter, this.renewActivity);
3033
3034
  }
3034
3035
  /**
3035
- * Update the 503 upgrade-retry budget. Applies to the current connection
3036
+ * Update the upgrade retry budget. Applies to the current connection
3036
3037
  * (if any) and is remembered for any future connections created after the
3037
3038
  * client is torn down and reconnected.
3038
3039
  */
@@ -5833,7 +5834,7 @@ async function pruneTunnelsForRestart(storage) {
5833
5834
  * This file is auto-updated by .github/changeset-version.ts during releases
5834
5835
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
5835
5836
  */
5836
- const SDK_VERSION = "0.12.0";
5837
+ const SDK_VERSION = "0.12.1";
5837
5838
 
5838
5839
  //#endregion
5839
5840
  //#region src/sandbox.ts
@@ -9984,4 +9985,4 @@ var Sandbox = class Sandbox extends Container {
9984
9985
 
9985
9986
  //#endregion
9986
9987
  export { FileClient as A, RPCTransportError as B, collectFile as C, ProcessClient as D, UtilityClient as E, BackupNotFoundError as F, BackupRestoreError as I, InvalidBackupConfigError as L, BackupClient as M, BackupCreateError as N, PortClient as O, BackupExpiredError as P, ProcessExitedBeforeReadyError as R, validateTunnelName as S, SandboxClient as T, SessionTerminatedError as V, responseToAsyncIterable as _, PREVIEW_PROXY_HEADER as a, sanitizeSandboxId as b, PREVIEW_PROXY_SANDBOX_ID_HEADER as c, BucketUnmountError as d, InvalidMountConfigError as f, parseSSEStream as g, asyncIterableToSSEStream as h, proxyTerminal as i, CommandClient as j, GitClient as k, PREVIEW_PROXY_TOKEN_HEADER as l, S3FSMountError as m, Sandbox as n, PREVIEW_PROXY_HEADERS as o, MissingCredentialsError as p, getSandbox as r, PREVIEW_PROXY_PORT_HEADER as s, ContainerProxy$1 as t, BucketMountError as u, CodeInterpreter as v, streamFile as w, validatePort as x, SandboxSecurityError as y, ProcessReadyTimeoutError as z };
9987
- //# sourceMappingURL=sandbox-Duj2gvUC.js.map
9988
+ //# sourceMappingURL=sandbox-DKG3H156.js.map