@blaxel/core 0.3.24 → 0.3.25-preview.306

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.
@@ -30,8 +30,8 @@ function missingCredentialsMessage() {
30
30
  return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
31
31
  }
32
32
  // Build info - these placeholders are replaced at build time by build:replace-imports
33
- const BUILD_VERSION = "0.3.24";
34
- const BUILD_COMMIT = "d3dcd1599047a3cadaacac6805f29e210e57aaad";
33
+ const BUILD_VERSION = "0.3.25-preview.306";
34
+ const BUILD_COMMIT = "a12f4f5f86967d77d3176dfab134ab800d3fe2f5";
35
35
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
36
36
  const BLAXEL_API_VERSION = "2026-09-22";
37
37
  // Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
@@ -51,24 +51,6 @@ const snapshot_js_1 = require("./snapshot.js");
51
51
  const session_js_1 = require("./session.js");
52
52
  const system_js_1 = require("./system.js");
53
53
  const types_js_1 = require("./types.js");
54
- const NON_REUSABLE_SANDBOX_STATUSES = new Set([
55
- "FAILED",
56
- "TERMINATED",
57
- "TERMINATING",
58
- "DELETING",
59
- "DEACTIVATING",
60
- ]);
61
- // Statuses that resolve on their own (a delete or deactivation in flight). The control
62
- // plane keeps answering 409 to creates while the record is in one of these, so retrying
63
- // instantly burns the whole attempt budget inside the window. Terminal statuses
64
- // (FAILED, TERMINATED) accept a create immediately and are not listed here.
65
- const TRANSIENT_SANDBOX_STATUSES = new Set([
66
- "TERMINATING",
67
- "DELETING",
68
- "DEACTIVATING",
69
- ]);
70
- const TRANSIENT_STATUS_MAX_WAIT_MS = 30_000;
71
- const TRANSIENT_STATUS_POLL_MS = 500;
72
54
  // Archiving a filesystem, and restoring it, take as long as that filesystem is
73
55
  // big — minutes for a few gigabytes.
74
56
  const ARCHIVE_MAX_WAIT_MS = 1_800_000;
@@ -679,71 +661,17 @@ class SandboxInstance {
679
661
  const instance = new SandboxInstance(data);
680
662
  return SandboxInstance.attachH2Session(instance);
681
663
  }
664
+ /**
665
+ * Create the sandbox, or return the one already holding this name.
666
+ *
667
+ * The control plane owns the reconciliation: an alive sandbox is returned as
668
+ * is, a FAILED/TERMINATED one is replaced, and a deletion or concurrent
669
+ * creation still in flight is waited for server-side. A 409 therefore only
670
+ * surfaces when the name really cannot be used, and is thrown as is.
671
+ * `options` (e.g. `timeout`) are forwarded to `create`.
672
+ */
682
673
  static async createIfNotExists(sandbox, options = {}) {
683
- const ATTEMPTS = 3;
684
- let lastStatus = "unknown";
685
- // The 'vanished' window (create 409s while get 404s) is driven by the same
686
- // transient causes as 'dying': an in-flight delete finishing, or a
687
- // concurrent create whose row is not readable yet. Give it the same time
688
- // budget as waitWhileSandboxDying instead of burning the attempt budget
689
- // 500ms apart (~1s total), which threw on deletes taking >1s (ENG-3667).
690
- const vanishedDeadline = Date.now() + TRANSIENT_STATUS_MAX_WAIT_MS;
691
- for (let i = 0; i < ATTEMPTS; ++i) {
692
- const finalAttempt = i === ATTEMPTS - 1;
693
- try {
694
- return await this.create(sandbox, { ...options, createIfNotExist: true });
695
- }
696
- catch (e) {
697
- if (typeof e === "object" && e !== null && "code" in e && (e.code === 409 || e.code === 'SANDBOX_ALREADY_EXISTS')) {
698
- const name = 'name' in sandbox ? sandbox.name : sandbox.metadata.name;
699
- if (!name) {
700
- throw new Error("Sandbox name is required");
701
- }
702
- // The controlplane tags the creation-lock 409 with
703
- // reason=CREATION_IN_PROGRESS when the conflict comes from a
704
- // concurrent create still in flight (ENG-3776). The field is absent
705
- // on older controlplanes and on real row conflicts, so it only
706
- // sharpens the give-up error; the polling behavior is the same.
707
- const creationInProgress = e.reason === "CREATION_IN_PROGRESS";
708
- // Get the existing sandbox to check its status
709
- let sandboxInstance;
710
- try {
711
- sandboxInstance = await this.get(name);
712
- }
713
- catch (getError) {
714
- if (isSandboxNotFound(getError)) {
715
- // The record vanished between the create conflict and this status check.
716
- lastStatus = creationInProgress ? "creation in progress" : "vanished";
717
- if (Date.now() < vanishedDeadline) {
718
- // Inside the transient window: poll without consuming attempts.
719
- await new Promise((resolve) => setTimeout(resolve, TRANSIENT_STATUS_POLL_MS));
720
- --i;
721
- }
722
- else if (!finalAttempt) {
723
- await new Promise((resolve) => setTimeout(resolve, TRANSIENT_STATUS_POLL_MS));
724
- }
725
- continue;
726
- }
727
- throw getError;
728
- }
729
- // Recreate instead of returning sandbox records that cannot be reused.
730
- if (!NON_REUSABLE_SANDBOX_STATUSES.has(sandboxInstance.status ?? "")) {
731
- return sandboxInstance;
732
- }
733
- // A delete or deactivation in flight rejects creates until it finishes;
734
- // wait it out instead of burning the remaining attempts inside the window.
735
- // No point waiting after the last attempt: nothing will use the result.
736
- lastStatus = sandboxInstance.status ?? "unknown";
737
- if (TRANSIENT_SANDBOX_STATUSES.has(lastStatus) && !finalAttempt) {
738
- await this.waitWhileSandboxDying(name);
739
- }
740
- // Retry creation. We want the same error handling on the retry as creates can race.
741
- continue;
742
- }
743
- throw e;
744
- }
745
- }
746
- throw new Error(`Unable to create sandbox after ${ATTEMPTS} attempts. Last conflicting status: ${lastStatus}.`);
674
+ return this.create(sandbox, { ...options, createIfNotExist: true });
747
675
  }
748
676
  // Poll the record after a create was cut by the edge with a 504 while the
749
677
  // control plane is still deploying it. Resolves with the sandbox once it
@@ -774,25 +702,6 @@ class SandboxInstance {
774
702
  }
775
703
  throw createError;
776
704
  }
777
- // Poll the record until an in-flight delete/deactivation settles (or the record
778
- // disappears), bounded by TRANSIENT_STATUS_MAX_WAIT_MS. Errors from get (e.g. 404
779
- // once the record is gone) end the wait: the caller's create retry decides next.
780
- static async waitWhileSandboxDying(name) {
781
- const deadline = Date.now() + TRANSIENT_STATUS_MAX_WAIT_MS;
782
- while (Date.now() < deadline) {
783
- await new Promise((resolve) => setTimeout(resolve, TRANSIENT_STATUS_POLL_MS));
784
- try {
785
- const current = await this.get(name);
786
- if (!TRANSIENT_SANDBOX_STATUSES.has(current.status ?? "")) {
787
- return;
788
- }
789
- logger_js_1.logger.debug(`Sandbox ${name} still ${current.status}; waiting for the record to settle before recreating`);
790
- }
791
- catch {
792
- return;
793
- }
794
- }
795
- }
796
705
  /* eslint-disable */
797
706
  static async fromSession(session) {
798
707
  // Create a minimal sandbox configuration for session-based access
@@ -260,8 +260,16 @@ export declare class SandboxInstance {
260
260
  static updateTtl(sandboxName: string, ttl: string | null): Promise<SandboxInstance>;
261
261
  static updateLifecycle(sandboxName: string, lifecycle: SandboxLifecycle | null): Promise<SandboxInstance>;
262
262
  static updateNetwork(sandboxName: string, network: SandboxUpdateNetwork): Promise<SandboxInstance>;
263
+ /**
264
+ * Create the sandbox, or return the one already holding this name.
265
+ *
266
+ * The control plane owns the reconciliation: an alive sandbox is returned as
267
+ * is, a FAILED/TERMINATED one is replaced, and a deletion or concurrent
268
+ * creation still in flight is waited for server-side. A 409 therefore only
269
+ * surfaces when the name really cannot be used, and is thrown as is.
270
+ * `options` (e.g. `timeout`) are forwarded to `create`.
271
+ */
263
272
  static createIfNotExists(sandbox: SandboxModel | SandboxCreateConfiguration, options?: Omit<SandboxCreateOptions, "createIfNotExist">): Promise<SandboxInstance>;
264
273
  private static waitAfterCreateGatewayTimeout;
265
- private static waitWhileSandboxDying;
266
274
  static fromSession(session: SessionWithToken): Promise<SandboxInstance>;
267
275
  }