@lensmcp/cluster 1.15.0 → 1.16.1

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,6 +1,6 @@
1
1
  import { type SpawnChild } from '@lensmcp/nx-plugin/lens-frontend';
2
2
  import type { GatewayRuntime, GatewayRuntimeOptions } from './types';
3
- import type { Observability } from './observability';
3
+ import { type Observability } from './observability';
4
4
  export interface LensChildren {
5
5
  spawnManagedChild: SpawnChild;
6
6
  /** Boot the dashboard + MCP singletons and every `lens:true` app. Pushes the
@@ -1 +1 @@
1
- {"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AACnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CA0HvH"}
1
+ {"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AACnF,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CA0HvH"}
@@ -11,6 +11,7 @@ const node_child_process_1 = require("node:child_process");
11
11
  const manifest_1 = require("../manifest");
12
12
  const lens_frontend_1 = require("@lensmcp/nx-plugin/lens-frontend");
13
13
  const scope_1 = require("./scope");
14
+ const observability_1 = require("./observability");
14
15
  const HEAL_BASE_MS = 1_500; // first restart delay
15
16
  const HEAL_MAX_MS = 30_000; // backoff cap — a true boot-loop settles here, never hot-loops
16
17
  const HEAL_HEALTHY_MS = 20_000; // ran at least this long ⇒ a FRESH crash ⇒ reset the backoff budget
@@ -72,7 +73,7 @@ function createLensChildren(rt, obs, options) {
72
73
  const lensBase = scope.basePath; // e.g. /tetros
73
74
  // Pushed BEFORE the cert SANs are derived (routes.map(r=>r.host)), so lensmcp.local
74
75
  // auto-joins the TLS SANs. Run `gateway:trust` to add it to /etc/hosts.
75
- rt.routes.push({ host: lensHost, prefix: lensBase, project: '(lens-dashboard)', target: `http://localhost:${dashboardPort}` });
76
+ rt.routes.push({ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${dashboardPort}` });
76
77
  (0, manifest_1.sortBySpecificity)(rt.routes);
77
78
  console.log(`[gateway] starting lens dashboard → http://localhost:${dashboardPort} (routed at https://${lensHost}${lensBase}/)`);
78
79
  emit('info', `lens dashboard up: ${lensHost}${lensBase} → :${dashboardPort}`, 'cluster-lens-dashboard', { kind: 'lens-dashboard-up', host: lensHost, port: dashboardPort, base: lensBase });
@@ -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;AACpE,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,CAuIvF"}
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"}
@@ -13,6 +13,7 @@ const http = tslib_1.__importStar(require("node:http"));
13
13
  const path = tslib_1.__importStar(require("node:path"));
14
14
  const discovery_1 = require("./discovery");
15
15
  const service_keys_1 = require("./service-keys");
16
+ const types_1 = require("./types");
16
17
  /**
17
18
  * The east-west env a pod needs to call ANOTHER service's `internal.<host>` route (memberships, settings,
18
19
  * plan): (1) its OWN per-service key — presented as `x-api-key`; the gateway maps it back to the caller and
@@ -71,11 +72,20 @@ function createServiceLayer(rt, obs) {
71
72
  for (const line of text.split('\n')) {
72
73
  if (/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(line)) {
73
74
  // eslint-disable-next-line no-control-regex -- strip ANSI color codes from child output
74
- svc.lastErrors.push(line.replace(/\x1b\[[0-9;]*m/g, '').trim());
75
+ const clean = line.replace(/\x1b\[[0-9;]*m/g, '').trim();
76
+ svc.lastErrors.push(clean);
75
77
  if (svc.lastErrors.length > 12)
76
78
  svc.lastErrors.shift();
77
79
  svc.buildFailed = true;
78
- emit('error', `service error: ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-error', project: svc.project, line: svc.lastErrors[svc.lastErrors.length - 1] });
80
+ // THROTTLE: a crash-looping pod re-prints the same error line on every restart. Emit the
81
+ // 'service error' event only when the error line CHANGES or after the throttle window —
82
+ // otherwise the bus (and the lens log) floods with identical errors many times a second.
83
+ const now = Date.now();
84
+ if (clean !== svc.lastErrorEmitSig || now - (svc.lastErrorEmitAt ?? 0) >= types_1.SERVICE_ERROR_THROTTLE_MS) {
85
+ svc.lastErrorEmitSig = clean;
86
+ svc.lastErrorEmitAt = now;
87
+ emit('error', `service error: ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-error', project: svc.project, line: clean });
88
+ }
79
89
  }
80
90
  if (/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(line))
81
91
  svc.buildFailed = false;
@@ -92,6 +102,28 @@ function createServiceLayer(rt, obs) {
92
102
  return; // gateway is tearing down — its dying services' exit chatter is noise
93
103
  console.log(`[gateway] ${target} exited (${code})`);
94
104
  emit(code === 0 ? 'info' : 'warning', `service down: ${svc.project} (exit ${code})`, `cluster-service:${svc.project}`, { kind: 'service-down', project: svc.project, code });
105
+ // SELF-HEAL: respawn an UNEXPECTEDLY-crashed pod so a transient failure recovers with no manual
106
+ // restart. A clean exit (code 0) or a sniffed BUILD failure (a code error → a fix, not a transient
107
+ // crash) is left down — scale-from-zero (ensureUp) still spawns it on the next request. A crash-loop
108
+ // guard (≤ SERVICE_MAX_RESTARTS within the window, exponential backoff) stops a broken service
109
+ // hot-looping the CPU.
110
+ if (code === 0 || svc.buildFailed)
111
+ return;
112
+ const now = Date.now();
113
+ svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
114
+ if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
115
+ emit('error', `service crash-looping: ${svc.project} — ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
116
+ return;
117
+ }
118
+ svc.restarts.push(now);
119
+ const backoffMs = Math.min(8000, 500 * 2 ** (svc.restarts.length - 1)); // 0.5 → 1 → 2 → 4 → 8s
120
+ setTimeout(() => {
121
+ if (rt.stopped() || svc.proc || svc.state !== 'down')
122
+ return; // a request already respawned it, or teardown
123
+ console.log(`[gateway] self-heal: respawning ${svc.project} (restart ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
124
+ emit('info', `self-heal: respawning ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-respawn', project: svc.project, attempt: svc.restarts.length });
125
+ spawnService(svc);
126
+ }, backoffMs);
95
127
  });
96
128
  svc.proc = proc;
97
129
  };
@@ -1,6 +1,15 @@
1
1
  import type * as http from 'node:http';
2
2
  import type { GwTrace, Route } from './types';
3
3
  export type Severity = 'info' | 'warning' | 'error';
4
+ /**
5
+ * The lens's OWN dashboard route project (the value `lens-children.ts` gives the `lensmcp.local/<key>/`
6
+ * route). Traffic to it is the lens UI polling its own control-plane API (`/<key>/api/cluster|logs|snapshot`)
7
+ * ~1×/s — pure self-observation with NO app-diagnostic value. Its SUCCESSFUL request narration + traffic
8
+ * edges are suppressed from the bus (the same "don't bloat the bus" rationale as header-less traffic); a 5xx
9
+ * still surfaces so a genuine lens-dashboard failure isn't hidden. One home for the literal (imported by
10
+ * `lens-children.ts` for the route + this layer for the filter, so they can't drift).
11
+ */
12
+ export declare const LENS_DASHBOARD_PROJECT = "(lens-dashboard)";
4
13
  export interface Observability {
5
14
  emit(severity: Severity, title: string, fingerprint: string, raw?: Record<string, unknown>, flowCtx?: Record<string, unknown>): void;
6
15
  eid(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/observability.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrI,GAAG,IAAI,MAAM,CAAC;IACd,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3D,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1F,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5G,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5E,0FAA0F;IAC1F,gBAAgB,IAAI,MAAM,CAAC,OAAO,CAAC;CACpC;AAKD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CA6FtG"}
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/observability.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrI,GAAG,IAAI,MAAM,CAAC;IACd,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3D,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1F,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5G,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5E,0FAA0F;IAC1F,gBAAgB,IAAI,MAAM,CAAC,OAAO,CAAC;CACpC;AAKD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAmGtG"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LENS_DASHBOARD_PROJECT = void 0;
3
4
  exports.createObservability = createObservability;
4
5
  const tslib_1 = require("tslib");
5
6
  /**
@@ -16,6 +17,15 @@ const tslib_1 = require("tslib");
16
17
  */
17
18
  const fs = tslib_1.__importStar(require("node:fs"));
18
19
  const path = tslib_1.__importStar(require("node:path"));
20
+ /**
21
+ * The lens's OWN dashboard route project (the value `lens-children.ts` gives the `lensmcp.local/<key>/`
22
+ * route). Traffic to it is the lens UI polling its own control-plane API (`/<key>/api/cluster|logs|snapshot`)
23
+ * ~1×/s — pure self-observation with NO app-diagnostic value. Its SUCCESSFUL request narration + traffic
24
+ * edges are suppressed from the bus (the same "don't bloat the bus" rationale as header-less traffic); a 5xx
25
+ * still surfaces so a genuine lens-dashboard failure isn't hidden. One home for the literal (imported by
26
+ * `lens-children.ts` for the route + this layer for the filter, so they can't drift).
27
+ */
28
+ exports.LENS_DASHBOARD_PROJECT = '(lens-dashboard)';
19
29
  const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
20
30
  function createObservability(eventFile, opts) {
21
31
  const eid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
@@ -69,6 +79,10 @@ function createObservability(eventFile, opts) {
69
79
  if (!t || t.done)
70
80
  return;
71
81
  t.done = true;
82
+ // The lens dashboard's own control-plane self-polling (1 req/s) would spam the bus with no
83
+ // app-diagnostic value — suppress its successful narration, but let a 5xx through (a real lens bug).
84
+ if (project === exports.LENS_DASHBOARD_PROJECT && status < 500)
85
+ return;
72
86
  const durationMs = Date.now() - t.startedAt;
73
87
  emit(status >= 500 ? 'error' : 'info', `gateway ${t.method} ${t.host}${t.url} → ${status} (${durationMs}ms)`, `gateway-request:${project}`, {
74
88
  kind: 'gateway-request', project, host: t.host, method: t.method, url: t.url,
@@ -83,6 +97,10 @@ function createObservability(eventFile, opts) {
83
97
  // The request-derived topology (nodes/edges appear from observed traffic).
84
98
  const edges = new Map();
85
99
  const recordEdge = (route, ms, status, caller) => {
100
+ // Don't accumulate a traffic edge for the lens dashboard's own self-polling (kills the
101
+ // `traffic lensmcp.local → (lens-dashboard)` spam); an error edge (5xx / connect-fail) still records.
102
+ if (route.project === exports.LENS_DASHBOARD_PROJECT && status < 500 && status !== 0)
103
+ return;
86
104
  const host = route.host ?? '(default)';
87
105
  const key = (caller ? caller + '⇒' : '') + host + '→' + route.project;
88
106
  const e = edges.get(key) ?? { host, project: route.project, caller, count: 0, totalMs: 0, errors: 0 };
@@ -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;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,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,CA4FtG"}
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,CA4GtG"}
@@ -13,6 +13,20 @@ const http = tslib_1.__importStar(require("node:http"));
13
13
  const path = tslib_1.__importStar(require("node:path"));
14
14
  const discovery_1 = require("./discovery");
15
15
  const edge_1 = require("./edge");
16
+ const types_1 = require("./types");
17
+ /** A CONNECTION-class upstream error — the pod/dev-server is momentarily unreachable (restarting its
18
+ * internal server on a config change, or warming up), NOT an application response. Only these are
19
+ * retried by the self-heal; a real 5xx from the app has already streamed a response and never lands here. */
20
+ function isConnError(err) {
21
+ const code = err.code ?? '';
22
+ return (code === 'ECONNREFUSED' ||
23
+ code === 'ECONNRESET' ||
24
+ code === 'ETIMEDOUT' ||
25
+ code === 'EPIPE' ||
26
+ code === 'EHOSTUNREACH' ||
27
+ code === 'ENOENT' ||
28
+ /ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(err.message));
29
+ }
16
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
17
31
  const httpProxy = require('http-proxy');
18
32
  function createProxy(rt, obs, svcLayer) {
@@ -99,10 +113,26 @@ function createProxy(rt, obs, svcLayer) {
99
113
  }
100
114
  // autoRewrite ONLY for a string (external URL) target — a non-string target crashes
101
115
  // setRedirectHostRewrite's url.parse(options.target) on a 3xx (see the socketPath case above).
102
- proxy.web(req, res, { target: route.target, autoRewrite: typeof route.target === 'string', changeOrigin: true, agent: agentForTarget(route.target) }, (err) => {
103
- console.error(`[gateway] upstream ${route.project} (${route.target}):`, err.message);
104
- (0, edge_1.sendError)(res, req, 502, 'unavailable', `Upstream ${route.project} unavailable.`);
105
- });
116
+ // SELF-HEAL (a TCP app upstream, e.g. a Vite dev server): on a config change Vite RESTARTS its
117
+ // internal server — the process stays alive, so the port refuses for ~1-3s. Rather than an instant
118
+ // 502 ("Upstream login unavailable"), retry the CONNECTION error with a short backoff until the
119
+ // restart/warmup finishes (the heal window), then finally 502. Retried only BEFORE any byte is
120
+ // written (the err cb fires pre-proxyRes) + only for a connection error (a real app 5xx streams
121
+ // through untouched).
122
+ const tcpTarget = route.target;
123
+ const tcpOpts = { target: tcpTarget, autoRewrite: typeof tcpTarget === 'string', changeOrigin: true, agent: agentForTarget(tcpTarget) };
124
+ const tcpDeadline = Date.now() + types_1.UPSTREAM_HEAL_WINDOW_MS;
125
+ const tryTcp = () => {
126
+ proxy.web(req, res, tcpOpts, (err) => {
127
+ if (isConnError(err) && Date.now() < tcpDeadline && !res.headersSent && !res.writableEnded) {
128
+ setTimeout(tryTcp, types_1.UPSTREAM_HEAL_STEP_MS);
129
+ return;
130
+ }
131
+ console.error(`[gateway] upstream ${route.project} (${String(tcpTarget)}):`, err.message);
132
+ (0, edge_1.sendError)(res, req, 502, 'unavailable', `Upstream ${route.project} unavailable.`);
133
+ });
134
+ };
135
+ tryTcp();
106
136
  };
107
137
  const destroyAgents = () => { httpAgent.destroy(); httpsAgent.destroy(); };
108
138
  const closeProxy = () => { try {
@@ -117,7 +117,28 @@ export interface ServiceCtl {
117
117
  spawnAt?: number;
118
118
  lastErrors?: string[];
119
119
  buildFailed?: boolean;
120
+ /** Throttle state for the 'service error' event: the last-emitted error signature + when. Stops a
121
+ * crash-looping pod (re-printing the same error line every restart) from flooding the event bus —
122
+ * we re-emit only when the error line CHANGES or after {@link SERVICE_ERROR_THROTTLE_MS}. */
123
+ lastErrorEmitSig?: string;
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). */
128
+ restarts?: number[];
120
129
  }
130
+ /** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
131
+ export declare const SERVICE_ERROR_THROTTLE_MS = 5000;
132
+ /** Self-heal: an unexpectedly-exited pod (crash, not a clean exit or a build error) is auto-respawned
133
+ * after a backoff, capped at {@link SERVICE_MAX_RESTARTS} within {@link SERVICE_RESTART_WINDOW_MS} so a
134
+ * genuinely-broken service doesn't hot-loop the CPU. */
135
+ export declare const SERVICE_MAX_RESTARTS = 5;
136
+ export declare const SERVICE_RESTART_WINDOW_MS = 60000;
137
+ /** Self-heal: how long the proxy retries a CONNECTION-class upstream error (a Vite dev server restarting
138
+ * its internal server on a config change → the port refuses for ~1-3s; a pod warming up) before it
139
+ * finally 502s. Retried only BEFORE any response byte is written, and only for a connection error. */
140
+ export declare const UPSTREAM_HEAL_WINDOW_MS = 8000;
141
+ export declare const UPSTREAM_HEAL_STEP_MS = 300;
121
142
  /** Test-tunable knobs on top of the user-facing schema. */
122
143
  export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
123
144
  /** 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,2 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ 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
+ /** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
5
+ exports.SERVICE_ERROR_THROTTLE_MS = 5000;
6
+ /** Self-heal: an unexpectedly-exited pod (crash, not a clean exit or a build error) is auto-respawned
7
+ * after a backoff, capped at {@link SERVICE_MAX_RESTARTS} within {@link SERVICE_RESTART_WINDOW_MS} so a
8
+ * genuinely-broken service doesn't hot-loop the CPU. */
9
+ exports.SERVICE_MAX_RESTARTS = 5;
10
+ exports.SERVICE_RESTART_WINDOW_MS = 60_000;
11
+ /** Self-heal: how long the proxy retries a CONNECTION-class upstream error (a Vite dev server restarting
12
+ * its internal server on a config change → the port refuses for ~1-3s; a pod warming up) before it
13
+ * finally 502s. Retried only BEFORE any response byte is written, and only for a connection error. */
14
+ exports.UPSTREAM_HEAL_WINDOW_MS = 8000;
15
+ exports.UPSTREAM_HEAL_STEP_MS = 300;
@@ -1 +1 @@
1
- {"version":3,"file":"serve.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/serve/serve.impl.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAKpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC5C,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,iBAAgB,aAAa,CAC3B,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAoPxD;AAED,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"serve.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/serve/serve.impl.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAKpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC5C,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,iBAAgB,aAAa,CAC3B,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgQxD;AAED,eAAe,aAAa,CAAC"}
@@ -28,7 +28,8 @@ async function* serveExecutor(options, context) {
28
28
  // Resolve port and serviceName from config YAML
29
29
  const configEnv = options.configEnv || 'development';
30
30
  const configFilePath = path.join(workspaceRoot, 'config', `config.${configEnv}.yaml`);
31
- let port = 3050;
31
+ const DEFAULT_PUBLIC_PORT = 3050;
32
+ let port = DEFAULT_PUBLIC_PORT;
32
33
  let serviceName = context.projectName;
33
34
  let yamlConfig = {};
34
35
  if (fs.existsSync(configFilePath)) {
@@ -36,6 +37,16 @@ async function* serveExecutor(options, context) {
36
37
  port = Number(yamlConfig.port) || port;
37
38
  serviceName = yamlConfig.serviceName || serviceName;
38
39
  }
40
+ else {
41
+ // No per-service config → the devserver's PUBLIC port falls back to the shared default. If TWO
42
+ // services both miss their config, both bind DEFAULT_PUBLIC_PORT and the loser crash-loops with a
43
+ // cryptic `EADDRINUSE :::3050` that the gateway surfaces as a "service error" — masquerading as a
44
+ // service bug. Warn loudly + actionably so the real cause (a missing `config/config.<env>.yaml`) is
45
+ // obvious. Fix: add `config/config.${configEnv}.yaml` with a distinct `port` (mirror its cluster decl).
46
+ console.warn(`[serve] ${context.projectName}: no config file at ${configFilePath} — public devserver port defaults to ${DEFAULT_PUBLIC_PORT}. ` +
47
+ `If another service also lacks its config, both bind ${DEFAULT_PUBLIC_PORT} → EADDRINUSE. ` +
48
+ `Add config/config.${configEnv}.yaml with a distinct \`port:\` (matching this project's cluster.port).`);
49
+ }
39
50
  if (options.serviceName) {
40
51
  serviceName = options.serviceName;
41
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/cluster",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
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.15.0",
69
- "@lensmcp/nx-plugin": "1.15.0",
68
+ "@lensmcp/node-instrumentation": "1.16.1",
69
+ "@lensmcp/nx-plugin": "1.16.1",
70
70
  "fork-ts-checker-webpack-plugin": "^9.0.0",
71
71
  "glob": "^11.0.0",
72
72
  "http-proxy": "^1.18.0",