@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/index.js CHANGED
@@ -22093,9 +22093,9 @@ function attachWebSocketClientPing(ws, intervalMs) {
22093
22093
  return clear;
22094
22094
  }
22095
22095
  function buildCliWebSocketClientOptions(wsUrl) {
22096
- const wsOptions = { perMessageDeflate: false, family: 4 };
22096
+ const wsOptions = { perMessageDeflate: false };
22097
22097
  if (wsUrl.startsWith("wss://")) {
22098
- wsOptions.agent = new https.Agent({ rejectUnauthorized: false, family: 4 });
22098
+ wsOptions.agent = new https.Agent({ rejectUnauthorized: false });
22099
22099
  }
22100
22100
  return wsOptions;
22101
22101
  }
@@ -22130,11 +22130,47 @@ function safeSendWebSocketBinary(ws, data) {
22130
22130
  }
22131
22131
  }
22132
22132
 
22133
+ // src/connection/parse-compact-heartbeat-ack.ts
22134
+ function tryParseCompactHeartbeatAck(raw) {
22135
+ let str = null;
22136
+ if (typeof raw === "string") {
22137
+ str = raw;
22138
+ } else if (Buffer.isBuffer(raw)) {
22139
+ str = raw.toString("utf8");
22140
+ } else if (raw instanceof ArrayBuffer) {
22141
+ str = Buffer.from(raw).toString("utf8");
22142
+ } else {
22143
+ return null;
22144
+ }
22145
+ if (str.length > 64 || !str.includes('"ha"')) return null;
22146
+ try {
22147
+ const parsed = JSON.parse(str);
22148
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
22149
+ const o = parsed;
22150
+ if (o.t !== "ha") return null;
22151
+ const s = o.s;
22152
+ if (typeof s !== "number" || !Number.isFinite(s)) return null;
22153
+ return Math.trunc(s);
22154
+ } catch {
22155
+ return null;
22156
+ }
22157
+ }
22158
+
22133
22159
  // src/connection/create-ws-bridge.ts
22134
22160
  var BRIDGE_AUTH_ERROR_HEADER = "x-bridge-auth-error";
22135
22161
  var BRIDGE_AUTH_ERROR_TOKEN_INVALID = "token_invalid";
22136
22162
  function createWsBridge(options) {
22137
- const { url: url2, onMessage, onOpen, onClose, onError: onError2, onAuthInvalid, clientPingIntervalMs } = options;
22163
+ const {
22164
+ url: url2,
22165
+ onMessage,
22166
+ onOpen,
22167
+ onClose,
22168
+ onError: onError2,
22169
+ onAuthInvalid,
22170
+ clientPingIntervalMs,
22171
+ onCompactHeartbeatAck,
22172
+ isActiveSocket
22173
+ } = options;
22138
22174
  applyCliOutboundNetworkPreferences();
22139
22175
  const ws = new wrapper_default(url2, buildCliWebSocketClientOptions(url2));
22140
22176
  let clearClientPing = null;
@@ -22158,7 +22194,13 @@ function createWsBridge(options) {
22158
22194
  onOpen?.();
22159
22195
  });
22160
22196
  ws.on("message", (raw) => {
22197
+ const ackSeq = tryParseCompactHeartbeatAck(raw);
22198
+ if (ackSeq != null) {
22199
+ onCompactHeartbeatAck?.(ackSeq);
22200
+ return;
22201
+ }
22161
22202
  setImmediate(() => {
22203
+ if (isActiveSocket && !isActiveSocket(ws)) return;
22162
22204
  try {
22163
22205
  let data;
22164
22206
  if (typeof raw === "string") {
@@ -22169,9 +22211,9 @@ function createWsBridge(options) {
22169
22211
  } else {
22170
22212
  data = raw;
22171
22213
  }
22172
- onMessage?.(data);
22214
+ onMessage?.(data, ws);
22173
22215
  } catch {
22174
- onMessage?.(raw);
22216
+ onMessage?.(raw, ws);
22175
22217
  }
22176
22218
  });
22177
22219
  });
@@ -24076,7 +24118,7 @@ function installBridgeProcessResilience() {
24076
24118
  }
24077
24119
 
24078
24120
  // src/cli-version.ts
24079
- var CLI_VERSION = "0.1.39".length > 0 ? "0.1.39" : "0.0.0-dev";
24121
+ var CLI_VERSION = "0.1.40".length > 0 ? "0.1.40" : "0.0.0-dev";
24080
24122
 
24081
24123
  // src/connection/heartbeat/constants.ts
24082
24124
  var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
@@ -24965,6 +25007,61 @@ function scheduleMainBridgeReconnect(state, connect, log2, closeMeta) {
24965
25007
  });
24966
25008
  }
24967
25009
 
25010
+ // src/connection/reconnect/duplicate-bridge-connection-detect.ts
25011
+ var BRIDGE_SUPERSEDED_CLOSE_REASON_SNIPPET = "superseded";
25012
+ var DUPLICATE_BRIDGE_SHORT_SESSION_MS = 25e3;
25013
+ var DUPLICATE_BRIDGE_FLAP_MIN_COUNT = 2;
25014
+ var DUPLICATE_BRIDGE_FLAP_WINDOW_MS = 9e4;
25015
+ var DUPLICATE_BRIDGE_STABLE_SESSION_MS = BRIDGE_APP_HEARTBEAT_INTERVAL_MS * BRIDGE_HEARTBEAT_MISSED_ACKS_BEFORE_RECONNECT + 5e3;
25016
+ 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).";
25017
+ function createEmptyDuplicateBridgeFlapTracker() {
25018
+ return {
25019
+ sessionOpenedAtMs: null,
25020
+ shortDisconnectAtMs: []
25021
+ };
25022
+ }
25023
+ function isSupersededByNewBridgeClose(code, reason) {
25024
+ if (code !== 1e3) return false;
25025
+ return reason.toLowerCase().includes(BRIDGE_SUPERSEDED_CLOSE_REASON_SNIPPET);
25026
+ }
25027
+ function pruneShortDisconnects(tracker, nowMs) {
25028
+ const cutoff = nowMs - DUPLICATE_BRIDGE_FLAP_WINDOW_MS;
25029
+ tracker.shortDisconnectAtMs = tracker.shortDisconnectAtMs.filter((t) => t >= cutoff);
25030
+ }
25031
+ function recordBridgeSessionOpened(tracker, nowMs) {
25032
+ tracker.sessionOpenedAtMs = nowMs;
25033
+ }
25034
+ function evaluateDuplicateBridgeDisconnect(tracker, nowMs, code, reason) {
25035
+ if (isSupersededByNewBridgeClose(code, reason)) {
25036
+ return { kind: "warn", trigger: "superseded_close" };
25037
+ }
25038
+ const openedAt = tracker.sessionOpenedAtMs;
25039
+ tracker.sessionOpenedAtMs = null;
25040
+ if (openedAt == null) {
25041
+ return { kind: "none" };
25042
+ }
25043
+ const sessionMs = nowMs - openedAt;
25044
+ if (sessionMs >= DUPLICATE_BRIDGE_STABLE_SESSION_MS) {
25045
+ tracker.shortDisconnectAtMs = [];
25046
+ return { kind: "none" };
25047
+ }
25048
+ if (sessionMs < DUPLICATE_BRIDGE_SHORT_SESSION_MS) {
25049
+ pruneShortDisconnects(tracker, nowMs);
25050
+ tracker.shortDisconnectAtMs.push(nowMs);
25051
+ if (tracker.shortDisconnectAtMs.length >= DUPLICATE_BRIDGE_FLAP_MIN_COUNT) {
25052
+ return { kind: "warn", trigger: "rapid_short_disconnects" };
25053
+ }
25054
+ }
25055
+ return { kind: "none" };
25056
+ }
25057
+ function logDuplicateBridgeWarning(log2, result) {
25058
+ if (result.kind !== "warn") return;
25059
+ try {
25060
+ log2(DUPLICATE_BRIDGE_WARNING_MESSAGE);
25061
+ } catch {
25062
+ }
25063
+ }
25064
+
24968
25065
  // src/connection/reconnect/firehose-reconnect.ts
24969
25066
  function beginFirehoseDeferredDisconnect(ctx, code, reason, log2) {
24970
25067
  beginTieredSilentReconnectDisconnect({
@@ -37310,8 +37407,10 @@ function normalizeInboundBridgeWebSocketJson(data) {
37310
37407
  }
37311
37408
  return data;
37312
37409
  }
37313
- function handleBridgeMessage(data, deps) {
37314
- if (!deps.getWs()) return;
37410
+ function handleBridgeMessage(data, deps, sourceWs) {
37411
+ const active = deps.getWs();
37412
+ if (!active) return;
37413
+ if (sourceWs != null && sourceWs !== active) return;
37315
37414
  const msg = parseApiToBridgeMessage(normalizeInboundBridgeWebSocketJson(data), deps.log);
37316
37415
  if (!msg) return;
37317
37416
  dispatchBridgeMessage(msg, deps);
@@ -37363,6 +37462,7 @@ function createMainBridgeWebSocketLifecycle(params) {
37363
37462
  } = params;
37364
37463
  let authRefreshInFlight = false;
37365
37464
  function handleOpen() {
37465
+ recordBridgeSessionOpened(state.duplicateBridgeFlap, Date.now());
37366
37466
  const logOpenAsPostRefreshReconnect = state.logBridgeOpenAsReconnect;
37367
37467
  clearMainBridgeReconnectQuietOnOpen(state, logFn);
37368
37468
  state.reconnectAttempt = 0;
@@ -37397,6 +37497,15 @@ function createMainBridgeWebSocketLifecycle(params) {
37397
37497
  state.currentWs = null;
37398
37498
  if (was) was.removeAllListeners();
37399
37499
  const willReconnect = !state.closedByUser;
37500
+ if (willReconnect) {
37501
+ const duplicateResult = evaluateDuplicateBridgeDisconnect(
37502
+ state.duplicateBridgeFlap,
37503
+ Date.now(),
37504
+ code,
37505
+ reason
37506
+ );
37507
+ logDuplicateBridgeWarning(logFn, duplicateResult);
37508
+ }
37400
37509
  beginMainBridgeDeferredDisconnect(state, code, reason, logFn, willReconnect);
37401
37510
  if (willReconnect) {
37402
37511
  state.lastReconnectCloseMeta = { code, reason };
@@ -37446,6 +37555,10 @@ function createMainBridgeWebSocketLifecycle(params) {
37446
37555
  state.currentWs = createWsBridge({
37447
37556
  url: url2,
37448
37557
  clientPingIntervalMs: CLI_WEBSOCKET_CLIENT_PING_MS,
37558
+ isActiveSocket: (socket) => state.currentWs === socket,
37559
+ onCompactHeartbeatAck: (seq) => {
37560
+ messageDeps.onBridgeHeartbeatAck?.(seq);
37561
+ },
37449
37562
  onAuthInvalid: () => {
37450
37563
  if (authRefreshInFlight) return;
37451
37564
  void (async () => {
@@ -37499,9 +37612,9 @@ function createMainBridgeWebSocketLifecycle(params) {
37499
37612
  } catch {
37500
37613
  }
37501
37614
  },
37502
- onMessage: (data) => {
37615
+ onMessage: (data, sourceWs) => {
37503
37616
  try {
37504
- handleBridgeMessage(data, messageDeps);
37617
+ handleBridgeMessage(data, messageDeps, sourceWs);
37505
37618
  } catch {
37506
37619
  }
37507
37620
  }
@@ -37945,6 +38058,7 @@ async function createBridgeConnection(options) {
37945
38058
  currentWs: null,
37946
38059
  mainQuiet: createEmptyReconnectQuietSlot(),
37947
38060
  mainOutage: createEmptyReconnectOutageTracker(),
38061
+ duplicateBridgeFlap: createEmptyDuplicateBridgeFlapTracker(),
37948
38062
  firehoseHandle: null,
37949
38063
  lastFirehoseParams: null,
37950
38064
  firehoseReconnectTimeout: null,