@buildautomaton/cli 0.1.39 → 0.1.40

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/cli.js CHANGED
@@ -25064,7 +25064,7 @@ var {
25064
25064
  } = import_index.default;
25065
25065
 
25066
25066
  // src/cli-version.ts
25067
- var CLI_VERSION = "0.1.39".length > 0 ? "0.1.39" : "0.0.0-dev";
25067
+ var CLI_VERSION = "0.1.40".length > 0 ? "0.1.40" : "0.0.0-dev";
25068
25068
 
25069
25069
  // src/cli/defaults.ts
25070
25070
  var DEFAULT_API_URL = process.env.BUILDAUTOMATON_API_URL ?? "https://api.buildautomaton.com";
@@ -25542,9 +25542,9 @@ function attachWebSocketClientPing(ws, intervalMs) {
25542
25542
  return clear;
25543
25543
  }
25544
25544
  function buildCliWebSocketClientOptions(wsUrl) {
25545
- const wsOptions = { perMessageDeflate: false, family: 4 };
25545
+ const wsOptions = { perMessageDeflate: false };
25546
25546
  if (wsUrl.startsWith("wss://")) {
25547
- wsOptions.agent = new https.Agent({ rejectUnauthorized: false, family: 4 });
25547
+ wsOptions.agent = new https.Agent({ rejectUnauthorized: false });
25548
25548
  }
25549
25549
  return wsOptions;
25550
25550
  }
@@ -25579,11 +25579,47 @@ function safeSendWebSocketBinary(ws, data) {
25579
25579
  }
25580
25580
  }
25581
25581
 
25582
+ // src/connection/parse-compact-heartbeat-ack.ts
25583
+ function tryParseCompactHeartbeatAck(raw) {
25584
+ let str = null;
25585
+ if (typeof raw === "string") {
25586
+ str = raw;
25587
+ } else if (Buffer.isBuffer(raw)) {
25588
+ str = raw.toString("utf8");
25589
+ } else if (raw instanceof ArrayBuffer) {
25590
+ str = Buffer.from(raw).toString("utf8");
25591
+ } else {
25592
+ return null;
25593
+ }
25594
+ if (str.length > 64 || !str.includes('"ha"')) return null;
25595
+ try {
25596
+ const parsed = JSON.parse(str);
25597
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
25598
+ const o = parsed;
25599
+ if (o.t !== "ha") return null;
25600
+ const s = o.s;
25601
+ if (typeof s !== "number" || !Number.isFinite(s)) return null;
25602
+ return Math.trunc(s);
25603
+ } catch {
25604
+ return null;
25605
+ }
25606
+ }
25607
+
25582
25608
  // src/connection/create-ws-bridge.ts
25583
25609
  var BRIDGE_AUTH_ERROR_HEADER = "x-bridge-auth-error";
25584
25610
  var BRIDGE_AUTH_ERROR_TOKEN_INVALID = "token_invalid";
25585
25611
  function createWsBridge(options) {
25586
- const { url: url2, onMessage, onOpen, onClose, onError: onError2, onAuthInvalid, clientPingIntervalMs } = options;
25612
+ const {
25613
+ url: url2,
25614
+ onMessage,
25615
+ onOpen,
25616
+ onClose,
25617
+ onError: onError2,
25618
+ onAuthInvalid,
25619
+ clientPingIntervalMs,
25620
+ onCompactHeartbeatAck,
25621
+ isActiveSocket
25622
+ } = options;
25587
25623
  applyCliOutboundNetworkPreferences();
25588
25624
  const ws = new wrapper_default(url2, buildCliWebSocketClientOptions(url2));
25589
25625
  let clearClientPing = null;
@@ -25607,7 +25643,13 @@ function createWsBridge(options) {
25607
25643
  onOpen?.();
25608
25644
  });
25609
25645
  ws.on("message", (raw) => {
25646
+ const ackSeq = tryParseCompactHeartbeatAck(raw);
25647
+ if (ackSeq != null) {
25648
+ onCompactHeartbeatAck?.(ackSeq);
25649
+ return;
25650
+ }
25610
25651
  setImmediate(() => {
25652
+ if (isActiveSocket && !isActiveSocket(ws)) return;
25611
25653
  try {
25612
25654
  let data;
25613
25655
  if (typeof raw === "string") {
@@ -25618,9 +25660,9 @@ function createWsBridge(options) {
25618
25660
  } else {
25619
25661
  data = raw;
25620
25662
  }
25621
- onMessage?.(data);
25663
+ onMessage?.(data, ws);
25622
25664
  } catch {
25623
- onMessage?.(raw);
25665
+ onMessage?.(raw, ws);
25624
25666
  }
25625
25667
  });
25626
25668
  });
@@ -26527,6 +26569,61 @@ function scheduleMainBridgeReconnect(state, connect, log2, closeMeta) {
26527
26569
  });
26528
26570
  }
26529
26571
 
26572
+ // src/connection/reconnect/duplicate-bridge-connection-detect.ts
26573
+ var BRIDGE_SUPERSEDED_CLOSE_REASON_SNIPPET = "superseded";
26574
+ var DUPLICATE_BRIDGE_SHORT_SESSION_MS = 25e3;
26575
+ var DUPLICATE_BRIDGE_FLAP_MIN_COUNT = 2;
26576
+ var DUPLICATE_BRIDGE_FLAP_WINDOW_MS = 9e4;
26577
+ var DUPLICATE_BRIDGE_STABLE_SESSION_MS = BRIDGE_APP_HEARTBEAT_INTERVAL_MS * BRIDGE_HEARTBEAT_MISSED_ACKS_BEFORE_RECONNECT + 5e3;
26578
+ var DUPLICATE_BRIDGE_WARNING_MESSAGE = "[Bridge service] It looks like two bridges are using the same token and disconnecting each other. Stop any duplicate @buildautomaton/cli process for this workspace/token (only one bridge should run).";
26579
+ function createEmptyDuplicateBridgeFlapTracker() {
26580
+ return {
26581
+ sessionOpenedAtMs: null,
26582
+ shortDisconnectAtMs: []
26583
+ };
26584
+ }
26585
+ function isSupersededByNewBridgeClose(code, reason) {
26586
+ if (code !== 1e3) return false;
26587
+ return reason.toLowerCase().includes(BRIDGE_SUPERSEDED_CLOSE_REASON_SNIPPET);
26588
+ }
26589
+ function pruneShortDisconnects(tracker, nowMs) {
26590
+ const cutoff = nowMs - DUPLICATE_BRIDGE_FLAP_WINDOW_MS;
26591
+ tracker.shortDisconnectAtMs = tracker.shortDisconnectAtMs.filter((t) => t >= cutoff);
26592
+ }
26593
+ function recordBridgeSessionOpened(tracker, nowMs) {
26594
+ tracker.sessionOpenedAtMs = nowMs;
26595
+ }
26596
+ function evaluateDuplicateBridgeDisconnect(tracker, nowMs, code, reason) {
26597
+ if (isSupersededByNewBridgeClose(code, reason)) {
26598
+ return { kind: "warn", trigger: "superseded_close" };
26599
+ }
26600
+ const openedAt = tracker.sessionOpenedAtMs;
26601
+ tracker.sessionOpenedAtMs = null;
26602
+ if (openedAt == null) {
26603
+ return { kind: "none" };
26604
+ }
26605
+ const sessionMs = nowMs - openedAt;
26606
+ if (sessionMs >= DUPLICATE_BRIDGE_STABLE_SESSION_MS) {
26607
+ tracker.shortDisconnectAtMs = [];
26608
+ return { kind: "none" };
26609
+ }
26610
+ if (sessionMs < DUPLICATE_BRIDGE_SHORT_SESSION_MS) {
26611
+ pruneShortDisconnects(tracker, nowMs);
26612
+ tracker.shortDisconnectAtMs.push(nowMs);
26613
+ if (tracker.shortDisconnectAtMs.length >= DUPLICATE_BRIDGE_FLAP_MIN_COUNT) {
26614
+ return { kind: "warn", trigger: "rapid_short_disconnects" };
26615
+ }
26616
+ }
26617
+ return { kind: "none" };
26618
+ }
26619
+ function logDuplicateBridgeWarning(log2, result) {
26620
+ if (result.kind !== "warn") return;
26621
+ try {
26622
+ log2(DUPLICATE_BRIDGE_WARNING_MESSAGE);
26623
+ } catch {
26624
+ }
26625
+ }
26626
+
26530
26627
  // src/connection/reconnect/firehose-reconnect.ts
26531
26628
  function beginFirehoseDeferredDisconnect(ctx, code, reason, log2) {
26532
26629
  beginTieredSilentReconnectDisconnect({
@@ -40612,8 +40709,10 @@ function normalizeInboundBridgeWebSocketJson(data) {
40612
40709
  }
40613
40710
  return data;
40614
40711
  }
40615
- function handleBridgeMessage(data, deps) {
40616
- if (!deps.getWs()) return;
40712
+ function handleBridgeMessage(data, deps, sourceWs) {
40713
+ const active = deps.getWs();
40714
+ if (!active) return;
40715
+ if (sourceWs != null && sourceWs !== active) return;
40617
40716
  const msg = parseApiToBridgeMessage(normalizeInboundBridgeWebSocketJson(data), deps.log);
40618
40717
  if (!msg) return;
40619
40718
  dispatchBridgeMessage(msg, deps);
@@ -40665,6 +40764,7 @@ function createMainBridgeWebSocketLifecycle(params) {
40665
40764
  } = params;
40666
40765
  let authRefreshInFlight = false;
40667
40766
  function handleOpen() {
40767
+ recordBridgeSessionOpened(state.duplicateBridgeFlap, Date.now());
40668
40768
  const logOpenAsPostRefreshReconnect = state.logBridgeOpenAsReconnect;
40669
40769
  clearMainBridgeReconnectQuietOnOpen(state, logFn);
40670
40770
  state.reconnectAttempt = 0;
@@ -40699,6 +40799,15 @@ function createMainBridgeWebSocketLifecycle(params) {
40699
40799
  state.currentWs = null;
40700
40800
  if (was) was.removeAllListeners();
40701
40801
  const willReconnect = !state.closedByUser;
40802
+ if (willReconnect) {
40803
+ const duplicateResult = evaluateDuplicateBridgeDisconnect(
40804
+ state.duplicateBridgeFlap,
40805
+ Date.now(),
40806
+ code,
40807
+ reason
40808
+ );
40809
+ logDuplicateBridgeWarning(logFn, duplicateResult);
40810
+ }
40702
40811
  beginMainBridgeDeferredDisconnect(state, code, reason, logFn, willReconnect);
40703
40812
  if (willReconnect) {
40704
40813
  state.lastReconnectCloseMeta = { code, reason };
@@ -40748,6 +40857,10 @@ function createMainBridgeWebSocketLifecycle(params) {
40748
40857
  state.currentWs = createWsBridge({
40749
40858
  url: url2,
40750
40859
  clientPingIntervalMs: CLI_WEBSOCKET_CLIENT_PING_MS,
40860
+ isActiveSocket: (socket) => state.currentWs === socket,
40861
+ onCompactHeartbeatAck: (seq) => {
40862
+ messageDeps.onBridgeHeartbeatAck?.(seq);
40863
+ },
40751
40864
  onAuthInvalid: () => {
40752
40865
  if (authRefreshInFlight) return;
40753
40866
  void (async () => {
@@ -40801,9 +40914,9 @@ function createMainBridgeWebSocketLifecycle(params) {
40801
40914
  } catch {
40802
40915
  }
40803
40916
  },
40804
- onMessage: (data) => {
40917
+ onMessage: (data, sourceWs) => {
40805
40918
  try {
40806
- handleBridgeMessage(data, messageDeps);
40919
+ handleBridgeMessage(data, messageDeps, sourceWs);
40807
40920
  } catch {
40808
40921
  }
40809
40922
  }
@@ -41168,6 +41281,7 @@ async function createBridgeConnection(options) {
41168
41281
  currentWs: null,
41169
41282
  mainQuiet: createEmptyReconnectQuietSlot(),
41170
41283
  mainOutage: createEmptyReconnectOutageTracker(),
41284
+ duplicateBridgeFlap: createEmptyDuplicateBridgeFlapTracker(),
41171
41285
  firehoseHandle: null,
41172
41286
  lastFirehoseParams: null,
41173
41287
  firehoseReconnectTimeout: null,