@maintainer-pro/ai-bridge 0.1.35 → 0.1.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-bridge",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "Local bridge daemon that pairs a machine to Maintainer Pro and configures multiple client sandboxes.",
5
5
  "keywords": [
6
6
  "maintainer-pro",
package/src/daemon.mjs CHANGED
@@ -81,6 +81,11 @@ const WS_RECONNECT_MAX_MS = 30_000;
81
81
  const WS_CONNECT_TIMEOUT_MS = 20_000;
82
82
  const WS_WATCHDOG_MS = 5_000;
83
83
 
84
+ // Temporary: default debug in prod so we can catch leftover issues. Revisit later.
85
+ if (!process.env.LOG_LEVEL?.trim() && !process.env.AI_LOG_LEVEL?.trim()) {
86
+ process.env.LOG_LEVEL = "debug";
87
+ }
88
+
84
89
  /** @type {import("@maintainer-pro/ai-cli").Logger} */
85
90
  let logger = createLogger("ai-bridge");
86
91
  const SECRET_KEY_RE =
@@ -142,6 +147,8 @@ const intakeServers = new Map();
142
147
 
143
148
  /** @type {(payload: Record<string, unknown>) => boolean} */
144
149
  let bridgeSend = () => false;
150
+ /** @type {() => Promise<void>} */
151
+ let waitForBridgeDrain = async () => {};
145
152
 
146
153
  function activity(sandboxId, level, message) {
147
154
  const text = String(message || "").trim();
@@ -374,7 +381,7 @@ Flags:
374
381
  --help
375
382
 
376
383
  Logging (env):
377
- LOG_LEVEL=info|debug|warn|error|silent (default: info)
384
+ LOG_LEVEL=info|debug|warn|error|silent (default: debug)
378
385
  LOG_PRETTY=0 disable pretty TTY output
379
386
  `);
380
387
  }
@@ -2542,7 +2549,8 @@ const embeddedChat = new Map();
2542
2549
  /** @type {Map<string, Promise<object | null>>} */
2543
2550
  const embeddedChatStarting = new Map();
2544
2551
 
2545
- const PROXY_CHUNK_BYTES = 256 * 1024;
2552
+ const PROXY_CHUNK_BYTES = 64 * 1024;
2553
+ const PROXY_SEND_HIGH_WATER = 512 * 1024;
2546
2554
  /** @type {Map<string, { req: import("node:http").ClientRequest, rewrite?: boolean, chunks?: Buffer[], headers?: Record<string, string>, portUrls?: Record<string, string> } | import("node:http").ClientRequest>} */
2547
2555
  const proxyHttpReqs = new Map();
2548
2556
  /** Reuse sockets to the local app — Next/Vite fetch many files per page. */
@@ -2632,6 +2640,10 @@ function replyProxyHttp(id, status, headers, body) {
2632
2640
  out[String(key)] = String(value);
2633
2641
  }
2634
2642
  bridgeSend({ type: "proxy.http.start", id, stream, status, headers: out });
2643
+ void sendProxyHttpChunks(id, stream, buf);
2644
+ }
2645
+
2646
+ async function sendProxyHttpChunks(id, stream, buf) {
2635
2647
  for (let offset = 0; offset < buf.length; offset += PROXY_CHUNK_BYTES) {
2636
2648
  const end = Math.min(offset + PROXY_CHUNK_BYTES, buf.length);
2637
2649
  bridgeSend({
@@ -2641,6 +2653,7 @@ function replyProxyHttp(id, status, headers, body) {
2641
2653
  data: buf.subarray(offset, end).toString("base64"),
2642
2654
  eof: false,
2643
2655
  });
2656
+ await waitForBridgeDrain();
2644
2657
  }
2645
2658
  bridgeSend({ type: "proxy.http.chunk", id, stream, data: "", eof: true });
2646
2659
  }
@@ -2803,6 +2816,18 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
2803
2816
  );
2804
2817
 
2805
2818
  if (pathname === "/ai-ui.iife.js") {
2819
+ if (method === "GET" || method === "HEAD") {
2820
+ const cached = lookupShareResponse({
2821
+ publicBase: ctx.publicBase,
2822
+ path: reqPath,
2823
+ ifNoneMatch: ctx.ifNoneMatch,
2824
+ acceptEncoding: ctx.acceptEncoding,
2825
+ });
2826
+ if (cached) {
2827
+ replyProxyHttp(id, cached.status, cached.headers, cached.body);
2828
+ return;
2829
+ }
2830
+ }
2806
2831
  const file = findIife();
2807
2832
  if (!file || !fs.existsSync(file)) {
2808
2833
  sendProcessedProxyHttp(
@@ -3149,7 +3174,9 @@ function handleProxyHttpFromAdmin(msg) {
3149
3174
  return;
3150
3175
  }
3151
3176
  if (isAiServerAppId(appId, ws)) {
3152
- void handleAiProxyHttpFromAdmin(msg, ws);
3177
+ setImmediate(() => {
3178
+ void handleAiProxyHttpFromAdmin(msg, ws);
3179
+ });
3153
3180
  return;
3154
3181
  }
3155
3182
  const existing = proxyHttpReqs.get(id);
@@ -3261,7 +3288,7 @@ function handleProxyHttpFromAdmin(msg) {
3261
3288
  }
3262
3289
  const status = res.statusCode || 502;
3263
3290
  applyShareCacheHeaders(outHeaders, path);
3264
- if (shouldProcessShareResponse(outHeaders)) {
3291
+ if (shouldProcessShareResponse(outHeaders, path)) {
3265
3292
  /** @type {Buffer[]} */
3266
3293
  const chunks = [];
3267
3294
  res.on("data", (chunk) => {
@@ -3294,14 +3321,22 @@ function handleProxyHttpFromAdmin(msg) {
3294
3321
  offset += PROXY_CHUNK_BYTES
3295
3322
  ) {
3296
3323
  const end = Math.min(offset + PROXY_CHUNK_BYTES, buf.length);
3297
- bridgeSend({
3324
+ const queued = bridgeSend({
3298
3325
  type: "proxy.http.chunk",
3299
3326
  id,
3300
3327
  stream,
3301
3328
  data: Buffer.from(buf.subarray(offset, end)).toString("base64"),
3302
3329
  eof: false,
3303
3330
  });
3331
+ if (queued === false) res.pause();
3304
3332
  }
3333
+ void waitForBridgeDrain().then(() => {
3334
+ try {
3335
+ res.resume();
3336
+ } catch {
3337
+ /* ignore */
3338
+ }
3339
+ });
3305
3340
  });
3306
3341
  res.on("end", () => {
3307
3342
  proxyHttpReqs.delete(id);
@@ -5926,7 +5961,7 @@ async function main() {
5926
5961
  }
5927
5962
 
5928
5963
  logger.info(
5929
- { versions: PACKAGE_VERSIONS },
5964
+ { versions: PACKAGE_VERSIONS, logLevel: logger.level },
5930
5965
  `bridge v${PACKAGE_VERSION} starting (${formatPackageVersions()})`
5931
5966
  );
5932
5967
  if (usingWsFallback) {
@@ -6029,6 +6064,7 @@ async function main() {
6029
6064
  return false;
6030
6065
  }
6031
6066
  sendChain = sendChain
6067
+ .then(() => waitSocketDrain(current))
6032
6068
  .then(() => {
6033
6069
  if (!socket || socket !== current || socket.readyState !== 1) return;
6034
6070
  socket.send(text);
@@ -6036,7 +6072,25 @@ async function main() {
6036
6072
  .catch(() => {});
6037
6073
  return true;
6038
6074
  };
6075
+ const waitSocketDrain = (current) =>
6076
+ new Promise((resolve) => {
6077
+ const tick = () => {
6078
+ if (!current || current.readyState !== 1) {
6079
+ resolve();
6080
+ return;
6081
+ }
6082
+ const buffered =
6083
+ typeof current.bufferedAmount === "number" ? current.bufferedAmount : 0;
6084
+ if (buffered < PROXY_SEND_HIGH_WATER) {
6085
+ setImmediate(resolve);
6086
+ return;
6087
+ }
6088
+ setTimeout(tick, 25);
6089
+ };
6090
+ tick();
6091
+ });
6039
6092
  bridgeSend = sendJson;
6093
+ waitForBridgeDrain = () => waitSocketDrain(socket);
6040
6094
 
6041
6095
  const clearHeartbeatTimer = () => {
6042
6096
  if (heartbeatTimer) {
@@ -140,7 +140,9 @@ export function shouldRewriteBody(headers) {
140
140
  }
141
141
 
142
142
  /** Redirects have no Content-Type, but Location still has to stay under the share prefix. */
143
- export function shouldProcessShareResponse(headers) {
143
+ export function shouldProcessShareResponse(headers, path = "") {
144
+ // Widget bundle is self-contained — rewriting it as text blocks the bridge.
145
+ if (isLongCacheScriptPath(path)) return false;
144
146
  if (headerGet(headers, "location")) return true;
145
147
  if (shouldRewriteBody(headers)) return true;
146
148
  const ct = headerGet(headers, "content-type").toLowerCase();
@@ -1656,6 +1658,22 @@ export function processShareHttpResponse(opts) {
1656
1658
 
1657
1659
  rewriteHeaderUrls(headers, publicBase, port, portUrls);
1658
1660
 
1661
+ if (isLongCacheScriptPath(path)) {
1662
+ delete headers.etag;
1663
+ delete headers["last-modified"];
1664
+ const mode = applyShareCacheHeaders(headers, path);
1665
+ const etag = etagFor(payload);
1666
+ if (mode !== "no-store") headers.etag = etag;
1667
+ const cachedKey = rewriteCacheKey(publicBase, path);
1668
+ if (cachedKey) rememberRewrite(cachedKey, payload, status, headers, etag);
1669
+ if (mode !== "no-store" && etagMatches(ifNoneMatch, etag)) {
1670
+ return notModifiedResult(headers, etag);
1671
+ }
1672
+ payload = gzipIfRequested(payload, headers, acceptEncoding);
1673
+ headers["content-length"] = String(payload.length);
1674
+ return { status, headers, body: payload };
1675
+ }
1676
+
1659
1677
  const cachedKey = rewriteCacheKey(publicBase, path);
1660
1678
  const hit =
1661
1679
  cachedKey && Date.now() - (rewriteCache.get(cachedKey)?.at || 0) < REWRITE_CACHE_MS