@my-life-buddies/cli 0.11.0 → 0.13.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.
@@ -43,6 +43,7 @@ var PreviewController = class {
43
43
  #simulator;
44
44
  #startController;
45
45
  #startPromise;
46
+ #stopPromise;
46
47
  #messages = [];
47
48
  #simulatorState = Object.freeze({ messages: Object.freeze([]) });
48
49
  #lastActiveRunId;
@@ -140,7 +141,17 @@ var PreviewController = class {
140
141
  this.#startPromise = startPromise;
141
142
  return startPromise;
142
143
  }
143
- async stop() {
144
+ stop() {
145
+ if (this.#stopPromise) return this.#stopPromise;
146
+ const stopping = this.#stop();
147
+ this.#stopPromise = stopping;
148
+ const clear = () => {
149
+ if (this.#stopPromise === stopping) this.#stopPromise = void 0;
150
+ };
151
+ void stopping.then(clear, clear);
152
+ return stopping;
153
+ }
154
+ async #stop() {
144
155
  if (this.#state.phase === "idle") return this.#state;
145
156
  if (this.#state.phase === "stopping") return this.#state;
146
157
  this.#setState({ phase: "stopping" });
@@ -1673,9 +1684,149 @@ var MlbSimulatorClient = class {
1673
1684
  return value;
1674
1685
  }
1675
1686
  };
1687
+
1688
+ // apps/preview/src/bridge.ts
1689
+ import { hostname } from "node:os";
1690
+ import { randomBytes as randomBytes2 } from "node:crypto";
1691
+ import { setTimeout as delay } from "node:timers/promises";
1692
+ async function connectPreviewBridge(options) {
1693
+ const connectionId = randomBytes2(32).toString("base64url");
1694
+ const shutdown = new AbortController();
1695
+ const signal = AbortSignal.any([options.signal, shutdown.signal]);
1696
+ const running = /* @__PURE__ */ new Map();
1697
+ const replies = /* @__PURE__ */ new Map();
1698
+ const completed = /* @__PURE__ */ new Set();
1699
+ let lastContact = Date.now();
1700
+ let stopping;
1701
+ let interrupted = false;
1702
+ async function call(path, token, payload, requestSignal = signal) {
1703
+ const response = await fetch(new URL(path, options.webUrl), {
1704
+ method: "POST",
1705
+ redirect: "error",
1706
+ signal: AbortSignal.any([requestSignal, AbortSignal.timeout(4e3)]),
1707
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
1708
+ body: JSON.stringify(payload ?? {})
1709
+ });
1710
+ const value = await response.json();
1711
+ if (!response.ok) throw Object.assign(new Error(value.error || "\u540E\u53F0\u8FDE\u63A5\u5931\u8D25"), { status: response.status });
1712
+ return value;
1713
+ }
1714
+ const attach = async () => {
1715
+ const result = await call("api/preview/connect", await options.token(), {
1716
+ connectionId,
1717
+ buddyId: options.buddyId,
1718
+ projectRoot: options.controller.projectRoot,
1719
+ machineName: hostname(),
1720
+ platformOrigin: options.platformOrigin
1721
+ });
1722
+ lastContact = Date.now();
1723
+ return result;
1724
+ };
1725
+ const bootstrap = await fetch(options.localServer.bootstrapUrl, { redirect: "manual", signal });
1726
+ const cookie = bootstrap.headers.get("set-cookie")?.split(";", 1)[0];
1727
+ if (bootstrap.status !== 303 || !cookie) throw new Error("\u672C\u5730\u8C03\u8BD5\u670D\u52A1\u521D\u59CB\u5316\u5931\u8D25");
1728
+ const initial = await attach();
1729
+ if (initial.reused) return { reused: true, completion: Promise.resolve(), async close() {
1730
+ } };
1731
+ const watchdog = setInterval(() => {
1732
+ if (Date.now() - lastContact < 1e4 || stopping || interrupted) return;
1733
+ interrupted = true;
1734
+ stopping = options.controller.stop();
1735
+ void stopping.catch(() => shutdown.abort());
1736
+ options.onStatus?.("\u540E\u53F0\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u505C\u6B62 DEV\uFF1B\u8FDE\u63A5\u6062\u590D\u540E\u53EF\u5728\u540E\u53F0\u91CD\u65B0\u542F\u52A8\u3002");
1737
+ }, 500);
1738
+ watchdog.unref();
1739
+ async function execute(command) {
1740
+ try {
1741
+ if (!command.path.startsWith("/api/") || command.path === "/api/events" || command.path.includes("..")) throw new Error("\u8C03\u8BD5\u8DEF\u5F84\u65E0\u6548");
1742
+ const target = new URL(command.path, options.localServer.origin);
1743
+ if (target.origin !== options.localServer.origin.origin) throw new Error("\u8C03\u8BD5\u8DEF\u5F84\u65E0\u6548");
1744
+ if ((await options.controller.project()).buddyId !== options.buddyId) throw new Error("\u5DE5\u7A0B buddyId \u5DF2\u6539\u53D8\uFF0C\u8BF7\u9000\u51FA CLI \u540E\u91CD\u65B0\u8FDE\u63A5\u5BF9\u5E94\u642D\u5B50");
1745
+ signal.throwIfAborted();
1746
+ if (interrupted) throw new Error("\u672C\u5730\u8FDE\u63A5\u5DF2\u4E2D\u65AD");
1747
+ const response = await fetch(target, {
1748
+ method: command.method,
1749
+ redirect: "error",
1750
+ signal: AbortSignal.any([signal, AbortSignal.timeout(28e3)]),
1751
+ headers: { Cookie: cookie, ...command.body ? { "Content-Type": "application/json" } : {} },
1752
+ ...command.body ? { body: command.body } : {}
1753
+ });
1754
+ const chunks = [];
1755
+ let size = 0;
1756
+ for await (const chunk of response.body ?? []) {
1757
+ size += chunk.length;
1758
+ if (size > 8 * 1024 * 1024) throw new Error("\u8C03\u8BD5\u54CD\u5E94\u8FC7\u5927");
1759
+ chunks.push(Buffer.from(chunk));
1760
+ }
1761
+ replies.set(command.id, { id: command.id, status: response.status, contentType: response.headers.get("content-type") ?? "application/json", body: Buffer.concat(chunks).toString("base64") });
1762
+ } catch {
1763
+ replies.set(command.id, { id: command.id, status: 502, contentType: "application/json", body: Buffer.from(JSON.stringify({ error: { code: "PREVIEW_LOCAL_FAILED", message: "\u672C\u5730\u8C03\u8BD5\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7EC8\u7AEF\u53CA\u5DE5\u7A0B buddyId" } })).toString("base64") });
1764
+ } finally {
1765
+ running.delete(command.id);
1766
+ }
1767
+ }
1768
+ const completion = (async () => {
1769
+ try {
1770
+ while (!signal.aborted) {
1771
+ try {
1772
+ if (stopping) {
1773
+ await stopping;
1774
+ stopping = void 0;
1775
+ }
1776
+ const result = await call("api/preview/poll", connectionId, { replies: [...replies.values()].slice(0, 1) });
1777
+ lastContact = Date.now();
1778
+ const recovered = interrupted;
1779
+ if (stopping) {
1780
+ await stopping;
1781
+ stopping = void 0;
1782
+ }
1783
+ if (recovered) {
1784
+ interrupted = false;
1785
+ options.onStatus?.("\u5DF2\u91CD\u65B0\u8FDE\u63A5\u540E\u53F0\uFF0C\u8BF7\u5728\u5F00\u53D1\u8C03\u8BD5\u9875\u542F\u52A8 DEV\u3002");
1786
+ }
1787
+ for (const id of result.acknowledged ?? []) {
1788
+ replies.delete(id);
1789
+ completed.add(id);
1790
+ }
1791
+ while (completed.size > 256) completed.delete(completed.values().next().value);
1792
+ for (const command of result.commands ?? []) {
1793
+ if (recovered && !running.has(command.id) && !replies.has(command.id) && !completed.has(command.id)) {
1794
+ replies.set(command.id, { id: command.id, status: 409, contentType: "application/json", body: Buffer.from(JSON.stringify({ error: { code: "PREVIEW_RECONNECTED", message: "\u672C\u5730\u8FDE\u63A5\u5DF2\u6062\u590D\uFF0C\u8BF7\u91CD\u65B0\u64CD\u4F5C" } })).toString("base64") });
1795
+ }
1796
+ if (!running.has(command.id) && !replies.has(command.id) && !completed.has(command.id) && running.size < 32) running.set(command.id, execute(command));
1797
+ }
1798
+ } catch (cause) {
1799
+ if (signal.aborted) break;
1800
+ const status = cause.status;
1801
+ if (status === 410) {
1802
+ await options.controller.stop();
1803
+ if ((await attach()).reused) throw new Error("\u5F53\u524D\u5DE5\u7A0B\u5DF2\u6709\u5176\u4ED6 CLI \u8FDE\u63A5\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C preview \u6253\u5F00\u540E\u53F0");
1804
+ } else if (status && status >= 400 && status < 500 && status !== 429) throw cause;
1805
+ }
1806
+ await delay(replies.size ? 20 : 500, void 0, { signal }).catch(() => void 0);
1807
+ }
1808
+ } finally {
1809
+ clearInterval(watchdog);
1810
+ }
1811
+ })();
1812
+ void completion.catch(() => void 0);
1813
+ return {
1814
+ reused: false,
1815
+ completion,
1816
+ async close() {
1817
+ shutdown.abort();
1818
+ await completion.catch(() => void 0);
1819
+ await Promise.allSettled(running.values());
1820
+ await stopping;
1821
+ await options.controller.stop();
1822
+ await call("api/preview/disconnect", connectionId, {}, AbortSignal.timeout(2e3)).catch(() => void 0);
1823
+ }
1824
+ };
1825
+ }
1676
1826
  export {
1677
1827
  MlbSimulatorClient,
1678
1828
  PreviewController,
1829
+ connectPreviewBridge,
1679
1830
  createBuddyPreviewUrl,
1680
1831
  startPreviewControlServer
1681
1832
  };