@lensmcp/cluster 1.14.0 → 1.16.0
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/executors/gateway/runtime/lifecycle.d.ts.map +1 -1
- package/executors/gateway/runtime/lifecycle.js +61 -4
- package/executors/gateway/runtime/proxy.d.ts.map +1 -1
- package/executors/gateway/runtime/proxy.js +34 -4
- package/executors/gateway/runtime/types.d.ts +21 -0
- package/executors/gateway/runtime/types.d.ts.map +1 -1
- package/executors/gateway/runtime/types.js +13 -0
- package/executors/serve/serve.impl.d.ts.map +1 -1
- package/executors/serve/serve.impl.js +12 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -12,6 +12,31 @@ const fs = tslib_1.__importStar(require("node:fs"));
|
|
|
12
12
|
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
|
+
const service_keys_1 = require("./service-keys");
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
/**
|
|
18
|
+
* The east-west env a pod needs to call ANOTHER service's `internal.<host>` route (memberships, settings,
|
|
19
|
+
* plan): (1) its OWN per-service key — presented as `x-api-key`; the gateway maps it back to the caller and
|
|
20
|
+
* stamps `x-api-key-id` (a service can't forge another's identity). (2) `NODE_EXTRA_CA_CERTS` = the dev
|
|
21
|
+
* gateway's self-signed CA, so the pod's OUTBOUND `fetch('https://internal.<host>')` clears TLS. Without
|
|
22
|
+
* these the call fails (connect/TLS or a gateway 401) and tenant-token minting can't resolve the role.
|
|
23
|
+
* NOT `INTERNAL_API_KEY`/`LENSMCP_INTERNAL_TOKEN` (those would flip ON the inbound gateway-trust guard) —
|
|
24
|
+
* a DEDICATED name a service opts into for OUTBOUND only, leaving inbound dev-standalone behavior unchanged.
|
|
25
|
+
*/
|
|
26
|
+
function eastWestEnv(rt, project) {
|
|
27
|
+
const env = {};
|
|
28
|
+
const key = rt.serviceKeys[project];
|
|
29
|
+
if (key)
|
|
30
|
+
env.LENSMCP_SERVICE_KEY = key;
|
|
31
|
+
if (rt.https) {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy like server.ts (cert deps)
|
|
33
|
+
const basicSsl = require('../../../basic-ssl');
|
|
34
|
+
const caPath = basicSsl.caCertPath((0, service_keys_1.gatewayCacheDir)(rt.root));
|
|
35
|
+
if (fs.existsSync(caPath))
|
|
36
|
+
env.NODE_EXTRA_CA_CERTS = caPath;
|
|
37
|
+
}
|
|
38
|
+
return env;
|
|
39
|
+
}
|
|
15
40
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
41
|
function createServiceLayer(rt, obs) {
|
|
17
42
|
const { emit } = obs;
|
|
@@ -35,8 +60,9 @@ function createServiceLayer(rt, obs) {
|
|
|
35
60
|
// daemon (a DIFFERENT group), so the later process-group kill reaps only
|
|
36
61
|
// the `yarn nx` shim and orphans the devserver + pod tree — the classic
|
|
37
62
|
// "idle-kill leaves live pods" leak. (--childCount as a CLI arg already
|
|
38
|
-
// avoids the env-loss the daemon was relied on for.)
|
|
39
|
-
env
|
|
63
|
+
// avoids the env-loss the daemon was relied on for.) NX_DAEMON=false also
|
|
64
|
+
// means env vars DO reach the pod, so the east-west key + CA propagate.
|
|
65
|
+
env: { ...process.env, NX_DAEMON: 'false', ...eastWestEnv(rt, svc.project) },
|
|
40
66
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
41
67
|
});
|
|
42
68
|
svc.lastErrors = [];
|
|
@@ -46,11 +72,20 @@ function createServiceLayer(rt, obs) {
|
|
|
46
72
|
for (const line of text.split('\n')) {
|
|
47
73
|
if (/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(line)) {
|
|
48
74
|
// eslint-disable-next-line no-control-regex -- strip ANSI color codes from child output
|
|
49
|
-
|
|
75
|
+
const clean = line.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
76
|
+
svc.lastErrors.push(clean);
|
|
50
77
|
if (svc.lastErrors.length > 12)
|
|
51
78
|
svc.lastErrors.shift();
|
|
52
79
|
svc.buildFailed = true;
|
|
53
|
-
|
|
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
|
+
}
|
|
54
89
|
}
|
|
55
90
|
if (/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(line))
|
|
56
91
|
svc.buildFailed = false;
|
|
@@ -67,6 +102,28 @@ function createServiceLayer(rt, obs) {
|
|
|
67
102
|
return; // gateway is tearing down — its dying services' exit chatter is noise
|
|
68
103
|
console.log(`[gateway] ${target} exited (${code})`);
|
|
69
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);
|
|
70
127
|
});
|
|
71
128
|
svc.proc = proc;
|
|
72
129
|
};
|
|
@@ -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;
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.16.0",
|
|
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.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.
|
|
68
|
+
"@lensmcp/node-instrumentation": "1.16.0",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.16.0",
|
|
70
70
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
71
71
|
"glob": "^11.0.0",
|
|
72
72
|
"http-proxy": "^1.18.0",
|