#!/usr/bin/env node

// ../../packages/relay/dist/chunk-YVJLTAM3.js
import { request as httpRequest } from "http";
import { request as httpsRequest } from "https";
import { setTimeout as delay } from "timers/promises";
var BACKOFF_BASE_MS = 1500;
var BACKOFF_MAX_MS = 3e4;
var REQUEST_TIMEOUT_MS = 25e3;
var FAST_REQUEST_TIMEOUT_MS = 8e3;
var FAST_MODE_DECAY_POLLS = 5;
var STOPPED_TICK_MS = 250;
function isConnectionLevelError(err) {
  const code = err?.code;
  if (code && CONNECTION_LEVEL_CODES.has(code)) return true;
  const msg = err instanceof Error ? err.message : String(err);
  return /\b(ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ECONNRESET|EPIPE)\b/.test(msg);
}
var CONNECTION_LEVEL_CODES = /* @__PURE__ */ new Set([
  "ECONNREFUSED",
  "ENOTFOUND",
  "EHOSTUNREACH",
  "ECONNRESET",
  "EPIPE"
]);
var CloudBoxPoller = class {
  constructor(deps) {
    this.deps = deps;
    this.currentPreviewUrl = deps.previewUrl;
  }
  deps;
  stopped = false;
  cursor = 0;
  currentBackoffMs = 0;
  loopPromise = null;
  /**
   * Counts down from {@link FAST_MODE_DECAY_POLLS} after each 504. While > 0
   * the next poll uses {@link FAST_REQUEST_TIMEOUT_MS}. Successful polls
   * decrement, so a flaky edge converges back to the default timeout
   * within ~5 successful round-trips.
   */
  fastModePolls = 0;
  /**
   * Mutable copy of `deps.previewUrl`. We don't read `deps.previewUrl`
   * directly after construction because `recoverPreviewUrl` may hand us a
   * new URL (e.g. Hetzner reopens its SSH ControlMaster and the `-L`
   * forward gets a new ephemeral local port).
   */
  currentPreviewUrl;
  /** Guards against recovery storms — at most one recovery attempt in flight. */
  recovering = null;
  start() {
    if (this.loopPromise) return;
    this.loopPromise = this.run().catch((err) => {
      this.log(`poller crashed: ${err instanceof Error ? err.message : String(err)}`);
    });
  }
  async stop() {
    this.stopped = true;
    if (this.loopPromise) await this.loopPromise;
  }
  /**
   * Post a `HostActionResult` back to the in-sandbox relay's
   * `/bridge/action-result`. The matching parked `/rpc` Promise resolves
   * and the in-box agent finally sees the answer.
   */
  async respond(actionId, result) {
    const base = this.currentPreviewUrl.replace(/\/+$/, "");
    const url = new URL(`${base}/bridge/action-result`);
    const isHttps = url.protocol === "https:";
    const transport = isHttps ? httpsRequest : httpRequest;
    const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
    const body = {
      id: actionId,
      exitCode: result.exitCode,
      stdout: result.stdout,
      stderr: result.stderr
    };
    const payload = JSON.stringify(body);
    await new Promise((resolve, reject) => {
      const headers = {
        "Content-Type": "application/json",
        "Content-Length": Buffer.byteLength(payload).toString(),
        Authorization: `Bearer ${this.deps.bridgeToken}`
      };
      if (this.deps.previewToken) {
        headers["x-daytona-preview-token"] = this.deps.previewToken;
      }
      const req = transport(
        {
          host: url.hostname,
          port,
          method: "POST",
          path: url.pathname,
          headers,
          timeout: REQUEST_TIMEOUT_MS
        },
        (res) => {
          res.resume();
          if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve();
          else reject(new Error(`bridge/action-result \u2192 ${String(res.statusCode)}`));
        }
      );
      req.on("error", reject);
      req.on("timeout", () => {
        req.destroy();
        reject(new Error("bridge/action-result timeout"));
      });
      req.write(payload);
      req.end();
    });
  }
  log(line) {
    this.deps.logger?.(`[cloud-poller ${this.deps.boxId}] ${line}`);
  }
  async run() {
    while (!this.stopped) {
      try {
        const body = await this.pollOnce();
        this.currentBackoffMs = 0;
        if (body.events.length > 0) this.deps.onEvents?.(body.events);
        if (body.status != null) this.deps.onStatus?.(body.status);
        for (const action of body.actions) {
          try {
            const respond = (r) => this.respond(action.id, r);
            if (this.deps.onAction) {
              await this.deps.onAction(action, respond);
            } else {
              await respond({
                exitCode: 1,
                stdout: "",
                stderr: `host has no executor for action '${action.method}'
`
              });
            }
          } catch (err) {
            this.log(`action handler error: ${err instanceof Error ? err.message : String(err)}`);
            try {
              await this.respond(action.id, {
                exitCode: 1,
                stdout: "",
                stderr: `host action handler crashed: ${err instanceof Error ? err.message : String(err)}
`
              });
            } catch {
            }
          }
        }
        if (typeof body.cursor === "number" && body.cursor > this.cursor) {
          this.cursor = body.cursor;
        }
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (msg.includes("\u2192 504") || msg.includes("504:")) {
          this.fastModePolls = FAST_MODE_DECAY_POLLS;
        }
        this.log(`poll error: ${msg}`);
        if (this.deps.recoverPreviewUrl && isConnectionLevelError(err)) {
          await this.tryRecoverPreviewUrl();
        }
        await this.backoff();
      }
      if (this.currentBackoffMs === 0 && this.fastModePolls > 0) {
        this.fastModePolls -= 1;
      }
      if (this.currentBackoffMs === 0) await delay(STOPPED_TICK_MS);
    }
  }
  async backoff() {
    this.currentBackoffMs = this.currentBackoffMs === 0 ? BACKOFF_BASE_MS : Math.min(this.currentBackoffMs * 2, BACKOFF_MAX_MS);
    await delay(this.currentBackoffMs);
  }
  async tryRecoverPreviewUrl() {
    if (!this.deps.recoverPreviewUrl) return;
    if (this.recovering) {
      await this.recovering;
      return;
    }
    this.recovering = (async () => {
      try {
        const next = await this.deps.recoverPreviewUrl();
        if (typeof next === "string" && next.length > 0 && next !== this.currentPreviewUrl) {
          this.log(`preview URL recovered: ${this.currentPreviewUrl} \u2192 ${next}`);
          this.currentPreviewUrl = next;
          this.currentBackoffMs = 0;
        } else if (typeof next === "string" && next === this.currentPreviewUrl) {
          this.log("preview URL recovered (unchanged)");
          this.currentBackoffMs = 0;
        }
      } catch (err) {
        this.log(`preview URL recover failed: ${err instanceof Error ? err.message : String(err)}`);
      } finally {
        this.recovering = null;
      }
    })();
    await this.recovering;
  }
  async pollOnce() {
    const base = this.currentPreviewUrl.replace(/\/+$/, "");
    const url = new URL(`${base}/bridge/poll?since=${String(this.cursor)}`);
    const isHttps = url.protocol === "https:";
    const transport = isHttps ? httpsRequest : httpRequest;
    const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
    const timeoutMs = this.fastModePolls > 0 ? FAST_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
    return new Promise((resolve, reject) => {
      const headers = {
        Authorization: `Bearer ${this.deps.bridgeToken}`,
        Accept: "application/json"
      };
      if (this.deps.previewToken) {
        headers["x-daytona-preview-token"] = this.deps.previewToken;
      }
      const req = transport(
        {
          host: url.hostname,
          port,
          method: "GET",
          path: `${url.pathname}${url.search}`,
          headers,
          timeout: timeoutMs
        },
        (res) => {
          const status = res.statusCode ?? 0;
          const chunks = [];
          res.on("data", (c) => chunks.push(c));
          res.on("end", () => {
            const text = Buffer.concat(chunks).toString("utf8");
            if (status < 200 || status >= 300) {
              reject(new Error(`bridge/poll \u2192 ${String(status)}: ${text.slice(0, 200)}`));
              return;
            }
            try {
              const parsed = JSON.parse(text);
              resolve({
                actions: Array.isArray(parsed.actions) ? parsed.actions : [],
                events: Array.isArray(parsed.events) ? parsed.events : [],
                status: parsed.status ?? null,
                cursor: typeof parsed.cursor === "number" ? parsed.cursor : this.cursor
              });
            } catch (err) {
              reject(err instanceof Error ? err : new Error(String(err)));
            }
          });
          res.on("error", reject);
        }
      );
      req.on("error", reject);
      req.on("timeout", () => {
        req.destroy();
        reject(new Error("bridge/poll timeout"));
      });
      req.end();
    });
  }
};
var CloudBoxPollers = class {
  map = /* @__PURE__ */ new Map();
  /** Idempotent: replacing an existing poller for a box stops the old one first. */
  start(boxId, deps) {
    const existing = this.map.get(boxId);
    if (existing) {
      void existing.stop();
    }
    const poller = new CloudBoxPoller(deps);
    this.map.set(boxId, poller);
    poller.start();
  }
  async stop(boxId) {
    const p = this.map.get(boxId);
    if (!p) return;
    this.map.delete(boxId);
    await p.stop();
  }
  async stopAll() {
    const ps = [...this.map.values()];
    this.map.clear();
    await Promise.all(ps.map((p) => p.stop()));
  }
  size() {
    return this.map.size;
  }
};

export {
  CloudBoxPoller,
  CloudBoxPollers
};
//# sourceMappingURL=chunk-G3H2L3O2.js.map