@lensmcp/cluster 1.16.2 → 1.16.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAWvC,OAAO,KAAK,EAAE,cAAc,EAAW,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,cAAc,CAAC;IACnB,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC,eAAe,CA2IrE"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAWvC,OAAO,KAAK,EAAE,cAAc,EAAW,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,cAAc,CAAC;IACnB,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC,eAAe,CAoJrE"}
@@ -49,6 +49,15 @@ function createHandler(deps) {
49
49
  : 'No gateway route for this hostname.');
50
50
  };
51
51
  const runPipeline = async (req, res) => {
52
+ // 0. HTTP/2 carries the host in the `:authority` pseudo-header, NOT `host` — the
53
+ // http2 compat layer exposes it as `req.authority` and omits `host` from `req.headers`.
54
+ // Every downstream step (route match, trace, CORS) reads `req.headers.host`, so
55
+ // normalize it here before anything else. No-op for HTTP/1.1 (host is already set).
56
+ if (!req.headers.host) {
57
+ const authority = req.authority ?? req.headers[':authority'];
58
+ if (authority)
59
+ req.headers.host = authority;
60
+ }
52
61
  // 1. STRIP client-supplied trust headers before anything else: the gateway is
53
62
  // the ONLY party allowed to assert `x-internal-token` (came through me) and
54
63
  // `x-api-key-id` (validated caller identity). Without this, a keyless/TCP route
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA0BrD,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CAoKvF"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA0BrD,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CA4MvF"}
@@ -219,14 +219,55 @@ function createServiceLayer(rt, obs) {
219
219
  svc.pool.scannedAt = 0;
220
220
  }, 1200).unref?.();
221
221
  };
222
+ // SELF-HEAL a WEDGED service: its gateway-spawned parent is ALIVE but 0 pods are live — an HMR hot-swap
223
+ // (or the proxy wedge/connection evict) unlinked the child socket, and `ensureUp` won't respawn because
224
+ // its `!svc.proc` guard sees the alive parent (the "ghost-socket zombie" — the evict→respawn gap). Kill
225
+ // the wedged tree (killSpawned clears svc.proc + unlinks stale socks) then respawn a fresh pod. A
226
+ // crash-loop cap (shared with the exit-self-heal) stops a genuinely-broken service hot-looping.
227
+ const recycleWedged = (svc) => {
228
+ const now = Date.now();
229
+ svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
230
+ if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
231
+ emit('error', `service crash-looping: ${svc.project} — wedged ${types_1.SERVICE_MAX_RESTARTS}+×/min, not auto-recycling (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
232
+ killSpawned(svc, 'wedged (crash-loop cap) — leaving down for the next request'); // svc.proc cleared → scale-from-zero can retry
233
+ return;
234
+ }
235
+ svc.restarts.push(now);
236
+ console.log(`[gateway] self-heal: recycling WEDGED ${svc.project} (live parent, 0 live pods; ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
237
+ emit('warning', `self-heal: recycling wedged ${svc.project} (0 live pods)`, `cluster-service:${svc.project}`, { kind: 'service-recycle', project: svc.project, attempt: svc.restarts.length });
238
+ killSpawned(svc, 'wedged: 0 live pods — recycling'); // kills the tree + unlinks stale socks (+1200ms) + svc.proc=undefined + state='down'
239
+ // A wedge is unexpected — bring it straight back (don't wait for a request). Respawn AFTER killSpawned's
240
+ // deferred sock cleanup (1200ms) so the fresh child binds a clean socket path.
241
+ setTimeout(() => {
242
+ if (!rt.stopped() && !svc.proc && svc.state === 'down')
243
+ spawnService(svc);
244
+ }, 1500).unref?.();
245
+ };
222
246
  const startSweeper = () => {
223
247
  const sweeper = setInterval(() => {
224
248
  for (const svc of rt.services) {
225
- const idle = svc.decl.idleKillSec;
226
- if (!svc.proc || !idle || svc.inflight > 0)
249
+ if (rt.stopped() || !svc.proc) {
250
+ svc.wedgedSince = undefined;
227
251
  continue;
228
- if (Date.now() - svc.lastUsed > idle * 1000)
252
+ }
253
+ const idle = svc.decl.idleKillSec;
254
+ // Idle kill (unchanged): scale a lazy service to zero after its idle window.
255
+ if (idle && svc.inflight === 0 && Date.now() - svc.lastUsed > idle * 1000) {
229
256
  killSpawned(svc, `idle ${idle}s`);
257
+ svc.wedgedSince = undefined;
258
+ continue;
259
+ }
260
+ // WEDGE detection: a live parent with 0 live pods that is NOT legitimately cold-starting is wedged.
261
+ const coldStarting = svc.state === 'starting' && svc.spawnAt !== undefined && Date.now() - svc.spawnAt < rt.startupTimeoutMs;
262
+ if (scanNow(svc.pool) > 0 || coldStarting) {
263
+ svc.wedgedSince = undefined;
264
+ continue;
265
+ }
266
+ svc.wedgedSince ??= Date.now();
267
+ if (Date.now() - svc.wedgedSince > types_1.WEDGE_RECYCLE_GRACE_MS) {
268
+ svc.wedgedSince = undefined;
269
+ recycleWedged(svc);
270
+ }
230
271
  }
231
272
  }, rt.sweepMs);
232
273
  sweeper.unref();
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,cAAc,EAAW,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAqBhD,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACxH,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACnI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAAG,IAAI,CAAC;IACrG,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,OAAO,YAAY,EAAE,KAAK,CAAC;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,UAAU,CAwItG"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,cAAc,EAAW,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8BhD,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACxH,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACnI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAAG,IAAI,CAAC;IACrG,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,OAAO,YAAY,EAAE,KAAK,CAAC;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,UAAU,CA4StG"}
@@ -27,6 +27,14 @@ function isConnError(err) {
27
27
  code === 'ENOENT' ||
28
28
  /ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(err.message));
29
29
  }
30
+ /** Hop-by-hop headers (RFC 7230 §6.1) — never forwarded upstream, never copied onto the edge
31
+ * response. Doubles as the set of connection-specific headers that are ILLEGAL on an HTTP/2 stream
32
+ * (Node's http2 layer throws on `connection`/`transfer-encoding`/… ), which is why the h2 native
33
+ * forwarder below must strip them where node-http-proxy (H1-only) never had to. */
34
+ const HOP_BY_HOP = new Set([
35
+ 'connection', 'keep-alive', 'proxy-connection', 'proxy-authenticate', 'proxy-authorization',
36
+ 'te', 'trailer', 'transfer-encoding', 'upgrade',
37
+ ]);
30
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
39
  const httpProxy = require('http-proxy');
32
40
  function createProxy(rt, obs, svcLayer) {
@@ -58,6 +66,182 @@ function createProxy(rt, obs, svcLayer) {
58
66
  delete proxyRes.headers['x-powered-by'];
59
67
  proxyRes.headers['server'] = 'LensMCP';
60
68
  });
69
+ // --- native HTTP/2 forwarder ------------------------------------------------
70
+ // node-http-proxy is HTTP/1.1-only: it copies connection-specific headers onto the
71
+ // response, which Node's http2 layer rejects. So an h2 EDGE request (a browser, once
72
+ // the front door speaks h2) is forwarded here with a raw http.request to the H1
73
+ // upstream (pod unix-socket or TCP dev-server) and the upstream response is piped back
74
+ // into the h2 stream. It mirrors the node-http-proxy path's branding (x-lensmcp-pod,
75
+ // drop x-powered-by, server:LensMCP), pod self-heal (evict + wedge timer) and TCP
76
+ // heal-retry — so h2 and h1 behave identically apart from the wire framing.
77
+ const pipeUpstream = (target, req, res, cbs) => {
78
+ const outHeaders = {};
79
+ for (const [k, v] of Object.entries(req.headers)) {
80
+ if (v === undefined || k.startsWith(':') || HOP_BY_HOP.has(k))
81
+ continue; // strip pseudo + hop-by-hop
82
+ outHeaders[k] = v;
83
+ }
84
+ // x-forwarded-* parity with node-http-proxy's xfwd:true.
85
+ const remote = req.socket?.remoteAddress ?? '';
86
+ const priorXff = req.headers['x-forwarded-for'];
87
+ outHeaders['x-forwarded-for'] = priorXff ? `${String(priorXff)}, ${remote}` : remote;
88
+ outHeaders['x-forwarded-proto'] = 'https';
89
+ if (!outHeaders['x-forwarded-host'])
90
+ outHeaders['x-forwarded-host'] = req.headers.host ?? '';
91
+ let mod = http;
92
+ let opts;
93
+ if (typeof target === 'object') {
94
+ // pod (unix socket): no changeOrigin — the pod ignores Host (matches the h1 path).
95
+ opts = { socketPath: target.socketPath, path: req.url, method: req.method, headers: outHeaders, agent: httpAgent };
96
+ }
97
+ else {
98
+ const u = new URL(target);
99
+ const isHttps = u.protocol === 'https:';
100
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
101
+ mod = isHttps ? require('node:https') : http;
102
+ outHeaders['host'] = u.host; // changeOrigin:true parity — a Vite dev server matches on Host
103
+ opts = {
104
+ protocol: u.protocol,
105
+ hostname: u.hostname,
106
+ port: u.port || (isHttps ? 443 : 80),
107
+ path: req.url,
108
+ method: req.method,
109
+ headers: outHeaders,
110
+ agent: agentForTarget(target),
111
+ };
112
+ }
113
+ const upstreamReq = mod.request(opts, (upstreamRes) => {
114
+ cbs.onResponse();
115
+ if (res.headersSent || res.writableEnded || res.destroyed) {
116
+ upstreamRes.destroy();
117
+ return;
118
+ }
119
+ const h = {};
120
+ for (const [k, v] of Object.entries(upstreamRes.headers)) {
121
+ if (v === undefined || HOP_BY_HOP.has(k))
122
+ continue; // hop-by-hop headers are h2-illegal
123
+ h[k] = v;
124
+ }
125
+ delete h['x-powered-by']; // drop the upstream framework leak (parity with proxyRes)
126
+ h['server'] = 'LensMCP'; // brand the hop
127
+ const pod = req.__lensmcpPod;
128
+ if (pod)
129
+ h['x-lensmcp-pod'] = pod;
130
+ try {
131
+ res.writeHead(upstreamRes.statusCode ?? 502, h);
132
+ }
133
+ catch (e) {
134
+ // A header the upstream sent is illegal on an h2 stream (Node throws e.g.
135
+ // ERR_HTTP2_INVALID_HEADER_VALUE). RESET only THIS stream — left unhandled the throw escapes
136
+ // the request callback and can tear down the whole h2 SESSION, failing EVERY sibling stream on
137
+ // the connection at once with ERR_HTTP2_PROTOCOL_ERROR (the "burst of files failed" symptom).
138
+ console.error('[gateway] h2 relay: bad response header, resetting stream:', e.message);
139
+ upstreamRes.destroy();
140
+ if (!res.destroyed)
141
+ res.destroy(e);
142
+ return;
143
+ }
144
+ upstreamRes.pipe(res);
145
+ // The upstream dies MID-BODY (a pod rolling-swap or a Vite restart resets the pooled socket): RST
146
+ // this h2 stream cleanly instead of leaving it half-written — a dangling h2 stream is what Chrome
147
+ // reports as ERR_HTTP2_PROTOCOL_ERROR. (Unhandled, 'error' on upstreamRes would also throw.)
148
+ upstreamRes.on('error', () => { if (!res.writableEnded && !res.destroyed)
149
+ res.destroy(); });
150
+ // The client canceled: stop reading the upstream so its pooled keep-alive socket isn't left flowing
151
+ // (a half-drained socket is corrupt on the next reuse → would break a later request).
152
+ res.on('close', () => { if (!upstreamRes.destroyed)
153
+ upstreamRes.destroy(); });
154
+ });
155
+ upstreamReq.on('error', (err) => cbs.onError(err));
156
+ req.on('error', () => upstreamReq.destroy());
157
+ // GET/HEAD/OPTIONS carry no body — end immediately; else stream the request body upstream.
158
+ if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS')
159
+ upstreamReq.end();
160
+ else
161
+ req.pipe(upstreamReq);
162
+ };
163
+ // The h2 twin of the pool/TCP branches in `forward` below — same pick / cold-start /
164
+ // evict / wedge / heal-retry, driving `pipeUpstream` instead of node-http-proxy.
165
+ const forwardNative = async (route, req, res) => {
166
+ const trace = req.__lensmcpTrace;
167
+ if (route.pool) {
168
+ const ctl = route.svc;
169
+ if (ctl) {
170
+ ctl.lastUsed = Date.now();
171
+ ctl.inflight += 1;
172
+ res.on('close', () => { ctl.inflight -= 1; ctl.lastUsed = Date.now(); });
173
+ }
174
+ let sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
175
+ if (!sock && ctl) {
176
+ traceStep(trace, 'cold-start', { project: route.project });
177
+ if (await svcLayer.ensureUp(ctl))
178
+ sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
179
+ }
180
+ if (ctl)
181
+ svcLayer.scaleUp(ctl);
182
+ if (!sock) {
183
+ const errs = ctl?.lastErrors?.length ? ` Service errors: ${ctl.lastErrors.join('; ')}` : '';
184
+ (0, edge_1.sendError)(res, req, 503, 'unavailable', `Service ${route.project} is not reachable (no pods in ${route.pool.dir}).${errs} The watcher recovers and the next request retries automatically.`);
185
+ return;
186
+ }
187
+ const podName = path.basename(sock, '.sock');
188
+ req.__lensmcpPod = podName;
189
+ traceStep(trace, 'pod-select', { pod: podName, pods: route.pool.socks.length, strategy: 'round-robin' });
190
+ let settled = false;
191
+ const evictPod = (reason) => {
192
+ route.pool.socks = route.pool.socks.filter((s) => s !== sock);
193
+ route.pool.idx = -1;
194
+ try {
195
+ fs.unlinkSync(sock);
196
+ }
197
+ catch { /* already gone */ }
198
+ console.error(`[gateway] pod ${podName} of ${route.project}: ${reason}`);
199
+ };
200
+ const wedgeTimer = setTimeout(() => {
201
+ if (settled || res.headersSent || res.writableEnded)
202
+ return;
203
+ settled = true;
204
+ evictPod(`no response within ${types_1.POD_RESPONSE_TIMEOUT_MS}ms (wedged socket) — evicted + respawning`);
205
+ (0, edge_1.sendError)(res, req, 502, 'unavailable', `Pod ${podName} of ${route.project} was unresponsive — evicted, retry.`);
206
+ }, types_1.POD_RESPONSE_TIMEOUT_MS);
207
+ wedgeTimer.unref?.();
208
+ res.on('close', () => clearTimeout(wedgeTimer));
209
+ pipeUpstream({ socketPath: sock }, req, res, {
210
+ onResponse: () => { clearTimeout(wedgeTimer); settled = true; },
211
+ onError: (err) => {
212
+ clearTimeout(wedgeTimer);
213
+ if (settled)
214
+ return;
215
+ settled = true;
216
+ evictPod(err.message);
217
+ (0, edge_1.sendError)(res, req, 502, 'unavailable', `Pod ${podName} of ${route.project} unavailable — retry.`);
218
+ },
219
+ });
220
+ return;
221
+ }
222
+ const tcpTarget = route.target;
223
+ const tcpDeadline = Date.now() + types_1.UPSTREAM_HEAL_WINDOW_MS;
224
+ const tryTcp = () => {
225
+ pipeUpstream(tcpTarget, req, res, {
226
+ onResponse: () => { },
227
+ onError: (err) => {
228
+ if (isConnError(err) && Date.now() < tcpDeadline && !res.headersSent && !res.writableEnded) {
229
+ setTimeout(tryTcp, types_1.UPSTREAM_HEAL_STEP_MS);
230
+ return;
231
+ }
232
+ // Past the response headers there's nothing to send an error body into — reset the stream.
233
+ if (res.headersSent || res.writableEnded || res.destroyed) {
234
+ if (!res.destroyed)
235
+ res.destroy();
236
+ return;
237
+ }
238
+ console.error(`[gateway] upstream ${route.project} (${String(tcpTarget)}):`, err.message);
239
+ (0, edge_1.sendError)(res, req, 502, 'unavailable', `Upstream ${route.project} unavailable.`);
240
+ },
241
+ });
242
+ };
243
+ tryTcp();
244
+ };
61
245
  const forward = async (route, req, res) => {
62
246
  const startedAt = Date.now();
63
247
  const trace = req.__lensmcpTrace;
@@ -71,6 +255,13 @@ function createProxy(rt, obs, svcLayer) {
71
255
  if (route.prependPrefix && !req.url?.startsWith(route.prependPrefix)) {
72
256
  req.url = route.prependPrefix + (req.url ?? '/');
73
257
  }
258
+ // HTTP/2 edge requests (browsers, once the front door speaks h2) can't ride node-http-proxy
259
+ // — forward them natively. H1 requests (curl, tools, the HMR WebSocket's sibling fetches)
260
+ // keep the proven path below unchanged.
261
+ if ((req.httpVersionMajor ?? 1) >= 2) {
262
+ await forwardNative(route, req, res);
263
+ return;
264
+ }
74
265
  if (route.pool) {
75
266
  const ctl = route.svc;
76
267
  if (ctl) {
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,MAAM,SAAS,CAAC;AAEjH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAmJxB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/server.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAkB,qBAAqB,EAAe,MAAM,SAAS,CAAC;AAEjH,wBAAsB,YAAY,CAChC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAkKxB"}
@@ -120,9 +120,21 @@ async function startGateway(options, context) {
120
120
  const servers = [];
121
121
  const boundPorts = [];
122
122
  for (const port of ports) {
123
+ // HTTP/2 (with allowHTTP1) for the TLS front door. A Vite dev page fetches
124
+ // HUNDREDS of ESM modules; over HTTP/1.1 the browser's ~6-connection-per-origin
125
+ // cap serializes that waterfall — each module sits "Stalled" waiting for a
126
+ // connection slot (measured: ~94% of a module's wall-time is stall, ~6% is the
127
+ // server), which is what makes dev asset loading feel slow through the gateway.
128
+ // h2 multiplexes every module over ONE connection + ONE TLS handshake, erasing
129
+ // both the 6-connection stall and the repeated handshakes. `allowHTTP1: true`
130
+ // keeps H1 clients working — notably Vite's HMR WebSocket, which browsers open
131
+ // as an H1 `Upgrade` on a SEPARATE connection (h2 has no Upgrade) and which still
132
+ // lands on `server.on('upgrade')` below. The forwarder (proxy.ts) branches by
133
+ // protocol: H1 keeps the proven node-http-proxy path; h2 uses a native forwarder
134
+ // because node-http-proxy emits connection-specific headers that are illegal in h2.
123
135
  const server = pem
124
136
  ? // eslint-disable-next-line @typescript-eslint/no-require-imports
125
- require('node:https').createServer({ key: pem, cert: pem }, handler)
137
+ require('node:http2').createSecureServer({ key: pem, cert: pem, allowHTTP1: true }, (req, res) => handler(req, res))
126
138
  : http.createServer(handler);
127
139
  server.on('upgrade', upgrade);
128
140
  server.on('error', (err) => console.error(`[gateway] port ${port}: ${err.code}`));
@@ -122,10 +122,15 @@ export interface ServiceCtl {
122
122
  * we re-emit only when the error line CHANGES or after {@link SERVICE_ERROR_THROTTLE_MS}. */
123
123
  lastErrorEmitSig?: string;
124
124
  lastErrorEmitAt?: number;
125
- /** Self-heal: timestamps (ms) of recent auto-respawns, kept for the crash-loop window. An unexpectedly
126
- * exited pod is respawned so a transient crash recovers without a manual restart; too many in the
127
- * window ⇒ stop hot-looping (the next request still spawns it via scale-from-zero). */
125
+ /** Self-heal: timestamps (ms) of recent auto-respawns/recycles, kept for the crash-loop window. An
126
+ * unexpectedly exited pod is respawned + a WEDGED pod (live parent, 0 pods) is recycled so a transient
127
+ * failure recovers without a manual restart; too many in the window ⇒ stop hot-looping (the next request
128
+ * still spawns it via scale-from-zero). Shared by the exit-self-heal AND the sweeper's wedge-recycle. */
128
129
  restarts?: number[];
130
+ /** Self-heal: when the sweeper FIRST observed this service wedged (live `proc`, 0 live pods, not
131
+ * cold-starting). Cleared once a pod appears; once it exceeds {@link WEDGE_RECYCLE_GRACE_MS} the sweeper
132
+ * recycles the wedged tree. */
133
+ wedgedSince?: number;
129
134
  }
130
135
  /** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
131
136
  export declare const SERVICE_ERROR_THROTTLE_MS = 5000;
@@ -148,6 +153,15 @@ export declare const UPSTREAM_HEAL_STEP_MS = 300;
148
153
  * Only fires BEFORE any response byte (a legit long request streams AFTER headers, unaffected).
149
154
  * Override with `LENSMCP_POD_RESPONSE_TIMEOUT_MS`. */
150
155
  export declare const POD_RESPONSE_TIMEOUT_MS: number;
156
+ /**
157
+ * Self-heal: how long a service may sit WEDGED — its gateway-spawned parent process ALIVE but with ZERO
158
+ * live pods (an HMR hot-swap evicted the child's socket, and `ensureUp` won't respawn because its
159
+ * `!svc.proc` guard sees the alive parent) — before the sweeper force-RECYCLES it (kill the wedged tree →
160
+ * respawn a fresh pod). This closes the evict→respawn gap: `evictPod` (proxy) empties the pool but never
161
+ * touches `svc.proc`, so a "next request scale-from-zero" is structurally blocked. The grace absorbs a
162
+ * legit in-process hot-swap (the socket is briefly gone, then re-bound). A code (build) error is left to
163
+ * the user's fix but still recycled under the crash-loop cap. Override with `LENSMCP_WEDGE_RECYCLE_GRACE_MS`. */
164
+ export declare const WEDGE_RECYCLE_GRACE_MS: number;
151
165
  /** Test-tunable knobs on top of the user-facing schema. */
152
166
  export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
153
167
  /** Traffic-edge flush period (ms). Default 5000. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.POD_RESPONSE_TIMEOUT_MS = exports.UPSTREAM_HEAL_STEP_MS = exports.UPSTREAM_HEAL_WINDOW_MS = exports.SERVICE_RESTART_WINDOW_MS = exports.SERVICE_MAX_RESTARTS = exports.SERVICE_ERROR_THROTTLE_MS = void 0;
3
+ exports.WEDGE_RECYCLE_GRACE_MS = exports.POD_RESPONSE_TIMEOUT_MS = exports.UPSTREAM_HEAL_STEP_MS = exports.UPSTREAM_HEAL_WINDOW_MS = exports.SERVICE_RESTART_WINDOW_MS = exports.SERVICE_MAX_RESTARTS = exports.SERVICE_ERROR_THROTTLE_MS = void 0;
4
4
  /** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
5
5
  exports.SERVICE_ERROR_THROTTLE_MS = 5000;
6
6
  /** Self-heal: an unexpectedly-exited pod (crash, not a clean exit or a build error) is auto-respawned
@@ -25,3 +25,15 @@ exports.POD_RESPONSE_TIMEOUT_MS = (() => {
25
25
  const n = Number(process.env['LENSMCP_POD_RESPONSE_TIMEOUT_MS']);
26
26
  return Number.isFinite(n) && n > 0 ? n : 30_000;
27
27
  })();
28
+ /**
29
+ * Self-heal: how long a service may sit WEDGED — its gateway-spawned parent process ALIVE but with ZERO
30
+ * live pods (an HMR hot-swap evicted the child's socket, and `ensureUp` won't respawn because its
31
+ * `!svc.proc` guard sees the alive parent) — before the sweeper force-RECYCLES it (kill the wedged tree →
32
+ * respawn a fresh pod). This closes the evict→respawn gap: `evictPod` (proxy) empties the pool but never
33
+ * touches `svc.proc`, so a "next request scale-from-zero" is structurally blocked. The grace absorbs a
34
+ * legit in-process hot-swap (the socket is briefly gone, then re-bound). A code (build) error is left to
35
+ * the user's fix but still recycled under the crash-loop cap. Override with `LENSMCP_WEDGE_RECYCLE_GRACE_MS`. */
36
+ exports.WEDGE_RECYCLE_GRACE_MS = (() => {
37
+ const n = Number(process.env['LENSMCP_WEDGE_RECYCLE_GRACE_MS']);
38
+ return Number.isFinite(n) && n > 0 ? n : 15_000;
39
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.16.2",
3
+ "version": "1.16.4",
4
4
  "description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -65,8 +65,8 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@lensmcp/node-instrumentation": "1.16.2",
69
- "@lensmcp/nx-plugin": "1.16.2",
68
+ "@lensmcp/node-instrumentation": "1.16.4",
69
+ "@lensmcp/nx-plugin": "1.16.4",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",