@cloudflare/sandbox 1.0.0-rc.2 → 1.0.0

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.d.mts CHANGED
@@ -41,7 +41,7 @@ interface DirectoryBackupOptions {
41
41
  interface DirectoryRestoreOptions {
42
42
  /** Absolute path to restore into instead of the record's `dir`. */
43
43
  dir?: string;
44
- /** Cancels the operation without imposing a timeout. */
44
+ /** Cancels the restore until the download is verified, without imposing a timeout. */
45
45
  signal?: AbortSignal;
46
46
  }
47
47
  interface DirectoryBackupDeleteOptions {
@@ -145,9 +145,12 @@ declare class DirectoryBackup {
145
145
  backup(options: DirectoryBackupOptions): Promise<DirectoryBackupRecord>;
146
146
  /**
147
147
  * Replaces `options.dir`, or the record's `dir`, with the backup's contents. The directory is
148
- * extracted beside the target and swapped in only after the download is verified, so a failed
149
- * or aborted restore leaves the target as it was. The target's parent must exist; the target
150
- * need not.
148
+ * extracted beside the target and swapped in only after the download is verified. A
149
+ * `SandboxFileError` or `SandboxBackupError` means the target was not replaced, and so does
150
+ * an abort before the download is verified; after that, aborting no longer ends the call. If the
151
+ * connection to the Container is lost, as when the Durable Object restarts, the target may or
152
+ * may not have been replaced, and restoring again settles it. The target's parent must exist;
153
+ * the target need not.
151
154
  *
152
155
  * @throws {SandboxFileError} The target's parent is missing (`ENOENT`), the target isn't a
153
156
  * directory (`ENOTDIR`), is a mount point or has one inside it (`EBUSY`), the swap fails (for
package/dist/index.mjs CHANGED
@@ -367,6 +367,10 @@ var ShimSession = class ShimSession {
367
367
  waitFor(operation) {
368
368
  return this.#abort.waitFor(operation);
369
369
  }
370
+ /** Stops following the caller's signal: later waits no longer reject when it aborts. */
371
+ stopFollowingSignal() {
372
+ this.#abort.dispose();
373
+ }
370
374
  finish() {
371
375
  if (this.#settled) return;
372
376
  this.#settled = true;
@@ -490,6 +494,7 @@ const errorSchema = z.object({
490
494
  detail: z.string()
491
495
  });
492
496
  const lockedSchema = z.strictObject({ kind: z.literal("locked") });
497
+ const verifiedSchema = z.strictObject({ kind: z.literal("verified") });
493
498
  /**
494
499
  * Runs one `directory-backup` shim exchange: wait for the lock, register the grant, acknowledge,
495
500
  * wait for the result, then deny and close stdin, in that order on every path. The shim holds
@@ -497,7 +502,10 @@ const lockedSchema = z.strictObject({ kind: z.literal("locked") });
497
502
  * operation's deny.
498
503
  *
499
504
  * An abort closes stdin rather than killing the shim, which then removes whatever it had
500
- * partly written, and rejects at once with the signal's reason.
505
+ * partly written, and rejects with the signal's reason without waiting for the shim. A restore
506
+ * reports `verified` before it swaps and swaps only once acknowledged, so the exchange
507
+ * acknowledges it only while the signal hasn't aborted, and from then on reports the swap's
508
+ * outcome instead of the abort.
501
509
  */
502
510
  async function runShimExchange(container, exchange) {
503
511
  const { signal } = exchange;
@@ -535,6 +543,13 @@ async function runShimExchange(container, exchange) {
535
543
  granting = exchange.grant();
536
544
  await session.waitFor(granting);
537
545
  await session.waitFor(input.write(ACKNOWLEDGEMENT));
546
+ if (exchange.command === "restore") {
547
+ const verified = await readMessage(control, operation, exchange.path);
548
+ if (!verifiedSchema.safeParse(verified).success) throw protocolError("sandbox-shim did not report a verified restore");
549
+ signal?.throwIfAborted();
550
+ session.stopFollowingSignal();
551
+ await input.write(ACKNOWLEDGEMENT);
552
+ }
538
553
  const result = await readMessage(control, operation, exchange.path);
539
554
  const done = exchange.done.safeParse(result);
540
555
  if (!done.success) throw protocolError("sandbox-shim returned an invalid backup result");
@@ -704,9 +719,12 @@ var DirectoryBackup = class {
704
719
  }
705
720
  /**
706
721
  * Replaces `options.dir`, or the record's `dir`, with the backup's contents. The directory is
707
- * extracted beside the target and swapped in only after the download is verified, so a failed
708
- * or aborted restore leaves the target as it was. The target's parent must exist; the target
709
- * need not.
722
+ * extracted beside the target and swapped in only after the download is verified. A
723
+ * `SandboxFileError` or `SandboxBackupError` means the target was not replaced, and so does
724
+ * an abort before the download is verified; after that, aborting no longer ends the call. If the
725
+ * connection to the Container is lost, as when the Durable Object restarts, the target may or
726
+ * may not have been replaced, and restoring again settles it. The target's parent must exist;
727
+ * the target need not.
710
728
  *
711
729
  * @throws {SandboxFileError} The target's parent is missing (`ENOENT`), the target isn't a
712
730
  * directory (`ENOTDIR`), is a mount point or has one inside it (`EBUSY`), the swap fails (for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0",
4
4
  "description": "Structured workspace file operations for Cloudflare sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {