@cloudflare/sandbox 0.12.10 → 0.12.11

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.
@@ -1,4 +1,4 @@
1
- import { _ as GitLogger, b as getEnvString, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as DEFAULT_GIT_CLONE_TIMEOUT_MS, h as ResultImpl, i as isWSStreamChunk, l as shellEscape, m as Execution, n as isWSError, p as logCanonicalEvent, r as isWSResponse, t as generateRequestId, u as createLogger, v as extractRepoName, x as partitionEnvVars, y as filterEnvVars } from "./dist-B_eXrP83.js";
1
+ import { _ as GitLogger, b as getEnvString, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as DEFAULT_GIT_CLONE_TIMEOUT_MS, h as ResultImpl, i as isWSStreamChunk, l as shellEscape, m as Execution, n as isWSError, p as logCanonicalEvent, r as isWSResponse, t as generateRequestId, u as createLogger, v as extractRepoName, x as partitionEnvVars, y as filterEnvVars } from "./dist-C3AQh4A0.js";
2
2
  import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-CpkKPbWC.js";
3
3
  import { Container, ContainerProxy, getContainer, switchPort } from "@cloudflare/containers";
4
4
  import { AwsClient } from "aws4fetch";
@@ -1878,11 +1878,13 @@ var FileClient = class extends BaseHttpClient {
1878
1878
  * Returns a ReadableStream of SSE events containing metadata, chunks, and completion
1879
1879
  * @param path - File path to stream
1880
1880
  * @param sessionId - The session ID for this operation
1881
+ * @param options - Optional stream encoding; base64 preserves arbitrary bytes
1881
1882
  */
1882
- async readFileStream(path$1, sessionId) {
1883
+ async readFileStream(path$1, sessionId, options) {
1883
1884
  const data = {
1884
1885
  path: path$1,
1885
- sessionId
1886
+ sessionId,
1887
+ encoding: options?.encoding
1886
1888
  };
1887
1889
  return await this.doStreamFetch("/api/read/stream", data);
1888
1890
  }
@@ -4343,18 +4345,49 @@ async function* parseSSE(stream) {
4343
4345
  * ```
4344
4346
  */
4345
4347
  async function* streamFile(stream) {
4346
- let metadata = null;
4347
- for await (const event of parseSSE(stream)) switch (event.type) {
4348
+ const { chunks } = await openFileStream(stream);
4349
+ return yield* chunks;
4350
+ }
4351
+ /**
4352
+ * Read the metadata event at the head of a file stream and return it with
4353
+ * the decoded chunks that follow. Callers that need the file size before
4354
+ * consuming the body, such as sizing a FixedLengthStream, use this instead of
4355
+ * streamFile(). Call cancel() to release the stream without reading the rest.
4356
+ */
4357
+ async function openFileStream(stream) {
4358
+ const events = parseSSE(stream);
4359
+ const cancel = async () => {
4360
+ await events.return(void 0);
4361
+ };
4362
+ try {
4363
+ const first = await events.next();
4364
+ if (first.done) throw new Error("Stream ended unexpectedly");
4365
+ const metadata = readMetadataEvent(first.value);
4366
+ return {
4367
+ metadata,
4368
+ chunks: decodeChunks(events, metadata),
4369
+ cancel
4370
+ };
4371
+ } catch (error) {
4372
+ await cancel();
4373
+ throw error;
4374
+ }
4375
+ }
4376
+ function readMetadataEvent(event) {
4377
+ switch (event.type) {
4378
+ case "metadata": return toFileMetadata(event);
4379
+ case "chunk": throw new Error("Received chunk before metadata");
4380
+ case "complete": throw new Error("Stream completed without metadata");
4381
+ case "error": throw new Error(`File streaming error: ${event.error}`);
4382
+ }
4383
+ }
4384
+ async function* decodeChunks(events, initialMetadata) {
4385
+ let metadata = initialMetadata;
4386
+ for await (const event of events) switch (event.type) {
4348
4387
  case "metadata":
4349
- metadata = {
4350
- mimeType: event.mimeType,
4351
- size: event.size,
4352
- isBinary: event.isBinary,
4353
- encoding: event.encoding
4354
- };
4388
+ metadata = toFileMetadata(event);
4355
4389
  break;
4356
4390
  case "chunk":
4357
- if (!metadata) throw new Error("Received chunk before metadata");
4358
4391
  if (metadata.isBinary && metadata.encoding === "base64") {
4359
4392
  const binaryString = atob(event.data);
4360
4393
  const bytes = new Uint8Array(binaryString.length);
@@ -4362,13 +4395,19 @@ async function* streamFile(stream) {
4362
4395
  yield bytes;
4363
4396
  } else yield event.data;
4364
4397
  break;
4365
- case "complete":
4366
- if (!metadata) throw new Error("Stream completed without metadata");
4367
- return metadata;
4398
+ case "complete": return metadata;
4368
4399
  case "error": throw new Error(`File streaming error: ${event.error}`);
4369
4400
  }
4370
4401
  throw new Error("Stream ended unexpectedly");
4371
4402
  }
4403
+ function toFileMetadata(event) {
4404
+ return {
4405
+ mimeType: event.mimeType,
4406
+ size: event.size,
4407
+ isBinary: event.isBinary,
4408
+ encoding: event.encoding
4409
+ };
4410
+ }
4372
4411
  /**
4373
4412
  * Collect an entire file into memory from a stream
4374
4413
  *
@@ -4871,6 +4910,10 @@ const DEFAULT_POLL_INTERVAL_MS = 1e3;
4871
4910
  const DEFAULT_ECHO_SUPPRESS_TTL_MS = 2e3;
4872
4911
  const MAX_BACKOFF_MS = 3e4;
4873
4912
  const SYNC_CONCURRENCY = 5;
4913
+ const STREAM_TO_CONTAINER_THRESHOLD_BYTES = 4 * 1024 * 1024;
4914
+ const UPLOAD_DEBOUNCE_MS = 1500;
4915
+ /** Watch rejections that a retry with the same request cannot fix. */
4916
+ const PERMANENT_WATCH_ERRORS = new Set([ErrorCode.PERMISSION_DENIED, ErrorCode.VALIDATION_FAILED]);
4874
4917
  /**
4875
4918
  * Manages bidirectional sync between an R2 binding and a container directory.
4876
4919
  *
@@ -4888,7 +4931,9 @@ var LocalMountSyncManager = class {
4888
4931
  pollIntervalMs;
4889
4932
  echoSuppressTtlMs;
4890
4933
  snapshot = /* @__PURE__ */ new Map();
4891
- echoSuppressSet = /* @__PURE__ */ new Set();
4934
+ echoSuppressTimers = /* @__PURE__ */ new Map();
4935
+ uploadTimers = /* @__PURE__ */ new Map();
4936
+ containerToR2Queue = Promise.resolve();
4892
4937
  pollTimer = null;
4893
4938
  watchReconnectTimer = null;
4894
4939
  watchAbortController = null;
@@ -4941,8 +4986,11 @@ var LocalMountSyncManager = class {
4941
4986
  this.watchAbortController.abort();
4942
4987
  this.watchAbortController = null;
4943
4988
  }
4989
+ for (const timer of this.uploadTimers.values()) clearTimeout(timer);
4990
+ this.uploadTimers.clear();
4991
+ for (const timer of this.echoSuppressTimers.values()) clearTimeout(timer);
4992
+ this.echoSuppressTimers.clear();
4944
4993
  this.snapshot.clear();
4945
- this.echoSuppressSet.clear();
4946
4994
  this.logger.info("Local mount sync stopped", { mountPath: this.mountPath });
4947
4995
  }
4948
4996
  async fullSyncR2ToContainer() {
@@ -5000,6 +5048,7 @@ var LocalMountSyncManager = class {
5000
5048
  await this.ensureParentDir(containerPath);
5001
5049
  this.suppressEcho(containerPath);
5002
5050
  await this.transferR2ObjectToContainer(key, containerPath);
5051
+ this.suppressEcho(containerPath);
5003
5052
  this.logger.debug("R2 -> Container: synced object", {
5004
5053
  key,
5005
5054
  action
@@ -5041,6 +5090,15 @@ var LocalMountSyncManager = class {
5041
5090
  async transferR2ObjectToContainer(key, containerPath) {
5042
5091
  const obj = await this.bucket.get(key);
5043
5092
  if (!obj) return;
5093
+ if (obj.size > STREAM_TO_CONTAINER_THRESHOLD_BYTES && this.client.getTransportMode() === "rpc") {
5094
+ try {
5095
+ await this.client.files.writeFileStream(containerPath, obj.body, this.sessionId);
5096
+ } catch (error) {
5097
+ await obj.body.cancel().catch(() => {});
5098
+ throw error;
5099
+ }
5100
+ return;
5101
+ }
5044
5102
  const arrayBuffer = await obj.arrayBuffer();
5045
5103
  const base64 = uint8ArrayToBase64(new Uint8Array(arrayBuffer));
5046
5104
  await this.client.files.writeFile(containerPath, base64, this.sessionId, { encoding: "base64" });
@@ -5060,8 +5118,13 @@ var LocalMountSyncManager = class {
5060
5118
  this.scheduleWatchReconnect();
5061
5119
  }).catch((error) => {
5062
5120
  if (!this.running) return;
5121
+ const err = error instanceof Error ? error : new Error(String(error));
5122
+ if (PERMANENT_WATCH_ERRORS.has(Reflect.get(err, "code"))) {
5123
+ this.logger.error("Container rejected the mount watch; changes made in the container will not sync to R2. Update the container image to match the SDK version.", err, { mountPath: this.mountPath });
5124
+ return;
5125
+ }
5063
5126
  this.consecutiveWatchFailures++;
5064
- this.logger.error("Container watch loop failed", error instanceof Error ? error : new Error(String(error)));
5127
+ this.logger.error("Container watch loop failed", err);
5065
5128
  this.scheduleWatchReconnect();
5066
5129
  });
5067
5130
  }
@@ -5091,54 +5154,108 @@ var LocalMountSyncManager = class {
5091
5154
  if (event.type !== "event") continue;
5092
5155
  if (event.isDirectory) continue;
5093
5156
  const containerPath = event.path;
5094
- if (this.echoSuppressSet.has(containerPath)) continue;
5157
+ if (this.echoSuppressTimers.has(containerPath)) continue;
5095
5158
  const r2Key = this.containerPathToR2Key(containerPath);
5096
5159
  if (!r2Key) continue;
5097
- try {
5098
- switch (event.eventType) {
5099
- case "create":
5100
- case "modify":
5101
- case "move_to":
5102
- await this.uploadFileToR2(containerPath, r2Key);
5103
- this.logger.debug("Container -> R2: synced file", {
5104
- path: containerPath,
5105
- key: r2Key,
5106
- action: event.eventType
5107
- });
5108
- break;
5109
- case "delete":
5110
- case "move_from":
5160
+ switch (event.eventType) {
5161
+ case "create":
5162
+ case "modify":
5163
+ case "move_to":
5164
+ this.scheduleUpload(containerPath, r2Key);
5165
+ break;
5166
+ case "delete":
5167
+ case "move_from":
5168
+ this.cancelScheduledUpload(containerPath);
5169
+ this.enqueueContainerToR2(containerPath, async () => {
5111
5170
  await this.bucket.delete(r2Key);
5112
5171
  this.snapshot.delete(r2Key);
5113
5172
  this.logger.debug("Container -> R2: deleted object", {
5114
5173
  path: containerPath,
5115
5174
  key: r2Key
5116
5175
  });
5117
- break;
5118
- }
5176
+ });
5177
+ break;
5178
+ }
5179
+ }
5180
+ }
5181
+ /**
5182
+ * Upload a changed file once its burst of watch events settles. Each new
5183
+ * event for the path restarts the wait.
5184
+ */
5185
+ scheduleUpload(containerPath, r2Key) {
5186
+ this.cancelScheduledUpload(containerPath);
5187
+ const timer = setTimeout(() => {
5188
+ this.uploadTimers.delete(containerPath);
5189
+ this.enqueueContainerToR2(containerPath, async () => {
5190
+ await this.uploadFileToR2(containerPath, r2Key);
5191
+ this.logger.debug("Container -> R2: synced file", {
5192
+ path: containerPath,
5193
+ key: r2Key
5194
+ });
5195
+ });
5196
+ }, UPLOAD_DEBOUNCE_MS);
5197
+ this.uploadTimers.set(containerPath, timer);
5198
+ }
5199
+ cancelScheduledUpload(containerPath) {
5200
+ const timer = this.uploadTimers.get(containerPath);
5201
+ if (timer !== void 0) {
5202
+ clearTimeout(timer);
5203
+ this.uploadTimers.delete(containerPath);
5204
+ }
5205
+ }
5206
+ enqueueContainerToR2(containerPath, operation) {
5207
+ this.containerToR2Queue = this.containerToR2Queue.then(async () => {
5208
+ if (!this.running) return;
5209
+ try {
5210
+ await operation();
5119
5211
  } catch (error) {
5120
5212
  this.logger.error(`Container -> R2 sync failed for ${containerPath}`, error instanceof Error ? error : new Error(String(error)));
5121
5213
  }
5122
- }
5214
+ });
5123
5215
  }
5124
5216
  /**
5125
- * Read a container file and upload it to R2, then update the local
5126
- * snapshot so the next poll cycle doesn't echo the write back.
5217
+ * Stream a container file into R2, then update the local snapshot so the
5218
+ * next poll cycle doesn't echo the write back.
5127
5219
  */
5128
5220
  async uploadFileToR2(containerPath, r2Key) {
5129
- const bytes = base64ToUint8Array((await this.client.files.readFile(containerPath, this.sessionId, { encoding: "base64" })).content);
5130
- await this.bucket.put(r2Key, bytes);
5221
+ const file = await openFileStream(await this.client.files.readFileStream(containerPath, this.sessionId, { encoding: "base64" }));
5222
+ if (file.metadata.isBinary && file.metadata.encoding === "base64") await this.putFileStream(r2Key, file);
5223
+ else {
5224
+ await file.cancel();
5225
+ const result = await this.client.files.readFile(containerPath, this.sessionId, { encoding: "base64" });
5226
+ await this.bucket.put(r2Key, base64ToUint8Array(result.content));
5227
+ }
5131
5228
  const head = await this.bucket.head(r2Key);
5132
5229
  if (head) this.snapshot.set(r2Key, {
5133
5230
  etag: head.etag,
5134
5231
  size: head.size
5135
5232
  });
5136
5233
  }
5234
+ async putFileStream(r2Key, file) {
5235
+ const body = new ReadableStream({
5236
+ async pull(controller) {
5237
+ const next = await file.chunks.next();
5238
+ if (next.done) controller.close();
5239
+ else if (typeof next.value === "string") controller.error(/* @__PURE__ */ new Error("Expected binary file chunks"));
5240
+ else controller.enqueue(next.value);
5241
+ },
5242
+ cancel: () => file.cancel()
5243
+ });
5244
+ const sized = new FixedLengthStream(file.metadata.size);
5245
+ const pipeAbort = new AbortController();
5246
+ body.pipeTo(sized.writable, { signal: pipeAbort.signal }).catch(() => {});
5247
+ try {
5248
+ await this.bucket.put(r2Key, sized.readable);
5249
+ } catch (error) {
5250
+ pipeAbort.abort(error);
5251
+ throw error;
5252
+ }
5253
+ }
5137
5254
  suppressEcho(containerPath) {
5138
- this.echoSuppressSet.add(containerPath);
5139
- setTimeout(() => {
5140
- this.echoSuppressSet.delete(containerPath);
5141
- }, this.echoSuppressTtlMs);
5255
+ clearTimeout(this.echoSuppressTimers.get(containerPath));
5256
+ this.echoSuppressTimers.set(containerPath, setTimeout(() => {
5257
+ this.echoSuppressTimers.delete(containerPath);
5258
+ }, this.echoSuppressTtlMs));
5142
5259
  }
5143
5260
  r2KeyToContainerPath(key) {
5144
5261
  let relativePath = key;
@@ -7185,7 +7302,7 @@ function createTunnelsHandler(host) {
7185
7302
  * This file is auto-updated by .github/changeset-version.ts during releases
7186
7303
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
7187
7304
  */
7188
- const SDK_VERSION = "0.12.10";
7305
+ const SDK_VERSION = "0.12.11";
7189
7306
 
7190
7307
  //#endregion
7191
7308
  //#region src/sandbox.ts
@@ -9323,7 +9440,6 @@ var Sandbox = class Sandbox extends Container {
9323
9440
  logCanonicalEvent(this.logger, {
9324
9441
  event: "sandbox.exec",
9325
9442
  outcome: execError ? "error" : "success",
9326
- command,
9327
9443
  exitCode: execOutcome?.exitCode,
9328
9444
  durationMs: Date.now() - startTime,
9329
9445
  sessionId: this.getPublicExecutionSessionId(sessionId),
@@ -10839,9 +10955,9 @@ var Sandbox = class Sandbox extends Container {
10839
10955
  ...startLines,
10840
10956
  ...waitLines,
10841
10957
  `FAILED=$(( ${exitVars.join(" + ")} ))`,
10842
- `if [ "$FAILED" -ne 0 ]; then rm -f ${shellEscape(tmpPath)}; exit 1; fi`
10958
+ `if [ "$FAILED" -ne 0 ]; then echo "download part exit codes: ${exitVars.join(" ")}" >&2; rm -f ${shellEscape(tmpPath)}; exit 1; fi`
10843
10959
  ].join("; ");
10844
- const result = await this.execWithSession(script, backupSession, {
10960
+ const result = await this.execWithSession(`( ${script} )`, backupSession, {
10845
10961
  timeout: 181e4,
10846
10962
  origin: "internal"
10847
10963
  });
@@ -11566,4 +11682,4 @@ var Sandbox = class Sandbox extends Container {
11566
11682
 
11567
11683
  //#endregion
11568
11684
  export { PortClient as A, InvalidBackupConfigError as B, collectFile as C, SandboxClient as D, isPlatformTransientError as E, BackupCreateError as F, SessionTerminatedError as G, ProcessExitedBeforeReadyError as H, BackupExpiredError as I, BackupNotFoundError as L, FileClient as M, CommandClient as N, UtilityClient as O, BackupClient as P, BackupRestoreError as R, validateTunnelName as S, isDurableObjectCodeUpdateReset as T, ProcessReadyTimeoutError as U, OperationInterruptedError as V, RPCTransportError as W, 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, GitClient as j, ProcessClient 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, ContainerUnavailableError as z };
11569
- //# sourceMappingURL=sandbox-Co2NYKhX.js.map
11685
+ //# sourceMappingURL=sandbox-DKbJiquI.js.map